using System; using System.Collections.Generic; using System.Linq; using NT8.Core.Logging; namespace NT8.Core.Analytics { /// /// Confidence interval model. /// public class ConfidenceInterval { public double ConfidenceLevel { get; set; } public double LowerBound { get; set; } public double UpperBound { get; set; } } /// /// Monte Carlo simulation output. /// public class MonteCarloResult { public int NumSimulations { get; set; } public int NumTradesPerSimulation { get; set; } public List FinalPnLDistribution { get; set; } public List MaxDrawdownDistribution { get; set; } public double MeanFinalPnL { get; set; } public MonteCarloResult() { FinalPnLDistribution = new List(); MaxDrawdownDistribution = new List(); } } /// /// Monte Carlo simulator for PnL scenarios. /// public class MonteCarloSimulator { private readonly ILogger _logger; private readonly Random _random; public MonteCarloSimulator(ILogger logger) { if (logger == null) throw new ArgumentNullException("logger"); _logger = logger; _random = new Random(1337); } /// /// Runs Monte Carlo simulation using bootstrap trade sampling. /// public MonteCarloResult Simulate(List historicalTrades, int numSimulations, int numTrades) { if (historicalTrades == null) throw new ArgumentNullException("historicalTrades"); if (numSimulations <= 0) throw new ArgumentException("numSimulations must be positive", "numSimulations"); if (numTrades <= 0) throw new ArgumentException("numTrades must be positive", "numTrades"); if (historicalTrades.Count == 0) throw new ArgumentException("historicalTrades cannot be empty", "historicalTrades"); try { var result = new MonteCarloResult(); result.NumSimulations = numSimulations; result.NumTradesPerSimulation = numTrades; for (var sim = 0; sim < numSimulations; sim++) { var equity = 0.0; var peak = 0.0; var maxDd = 0.0; for (var i = 0; i < numTrades; i++) { var sample = historicalTrades[_random.Next(historicalTrades.Count)]; equity += sample.RealizedPnL; if (equity > peak) peak = equity; var dd = peak - equity; if (dd > maxDd) maxDd = dd; } result.FinalPnLDistribution.Add(equity); result.MaxDrawdownDistribution.Add(maxDd); } result.MeanFinalPnL = result.FinalPnLDistribution.Average(); return result; } catch (Exception ex) { _logger.LogError("Monte Carlo simulate failed: {0}", ex.Message); throw; } } /// /// Calculates risk of ruin as probability max drawdown exceeds threshold. /// public double CalculateRiskOfRuin(List trades, double drawdownThreshold) { if (trades == null) throw new ArgumentNullException("trades"); if (drawdownThreshold <= 0) throw new ArgumentException("drawdownThreshold must be positive", "drawdownThreshold"); try { var result = Simulate(trades, 2000, Math.Max(30, trades.Count)); var ruined = result.MaxDrawdownDistribution.Count(d => d >= drawdownThreshold); return (double)ruined / result.MaxDrawdownDistribution.Count; } catch (Exception ex) { _logger.LogError("CalculateRiskOfRuin failed: {0}", ex.Message); throw; } } /// /// Calculates confidence interval for final PnL distribution. /// public ConfidenceInterval CalculateConfidenceInterval(MonteCarloResult result, double confidenceLevel) { if (result == null) throw new ArgumentNullException("result"); if (confidenceLevel <= 0.0 || confidenceLevel >= 1.0) throw new ArgumentException("confidenceLevel must be in (0,1)", "confidenceLevel"); try { var sorted = result.FinalPnLDistribution.OrderBy(v => v).ToList(); if (sorted.Count == 0) return new ConfidenceInterval { ConfidenceLevel = confidenceLevel, LowerBound = 0.0, UpperBound = 0.0 }; var alpha = 1.0 - confidenceLevel; var lowerIndex = (int)Math.Floor((alpha / 2.0) * (sorted.Count - 1)); var upperIndex = (int)Math.Floor((1.0 - (alpha / 2.0)) * (sorted.Count - 1)); return new ConfidenceInterval { ConfidenceLevel = confidenceLevel, LowerBound = sorted[Math.Max(0, lowerIndex)], UpperBound = sorted[Math.Min(sorted.Count - 1, upperIndex)] }; } catch (Exception ex) { _logger.LogError("CalculateConfidenceInterval failed: {0}", ex.Message); throw; } } } }