using System; using System.Collections.Generic; using System.Linq; using NT8.Core.Logging; namespace NT8.Core.Analytics { /// /// Calculates aggregate performance metrics for trade sets. /// public class PerformanceCalculator { private readonly ILogger _logger; /// /// Initializes a new calculator instance. /// /// Logger dependency. public PerformanceCalculator(ILogger logger) { if (logger == null) throw new ArgumentNullException("logger"); _logger = logger; } /// /// Calculates all core metrics from trades. /// /// Trade records. /// Performance metrics snapshot. public PerformanceMetrics Calculate(List trades) { if (trades == null) throw new ArgumentNullException("trades"); try { var metrics = new PerformanceMetrics(); metrics.TotalTrades = trades.Count; metrics.Wins = trades.Count(t => t.RealizedPnL > 0.0); metrics.Losses = trades.Count(t => t.RealizedPnL < 0.0); metrics.WinRate = CalculateWinRate(trades); metrics.LossRate = metrics.TotalTrades > 0 ? (double)metrics.Losses / metrics.TotalTrades : 0.0; metrics.GrossProfit = trades.Where(t => t.RealizedPnL > 0.0).Sum(t => t.RealizedPnL); metrics.GrossLoss = Math.Abs(trades.Where(t => t.RealizedPnL < 0.0).Sum(t => t.RealizedPnL)); metrics.NetProfit = metrics.GrossProfit - metrics.GrossLoss; metrics.AverageWin = metrics.Wins > 0 ? trades.Where(t => t.RealizedPnL > 0.0).Average(t => t.RealizedPnL) : 0.0; metrics.AverageLoss = metrics.Losses > 0 ? Math.Abs(trades.Where(t => t.RealizedPnL < 0.0).Average(t => t.RealizedPnL)) : 0.0; metrics.ProfitFactor = CalculateProfitFactor(trades); metrics.Expectancy = CalculateExpectancy(trades); metrics.SharpeRatio = CalculateSharpeRatio(trades, 0.0); metrics.SortinoRatio = CalculateSortinoRatio(trades, 0.0); metrics.MaxDrawdownPercent = CalculateMaxDrawdown(trades); metrics.RecoveryFactor = metrics.MaxDrawdownPercent > 0.0 ? metrics.NetProfit / metrics.MaxDrawdownPercent : 0.0; return metrics; } catch (Exception ex) { _logger.LogError("Calculate performance metrics failed: {0}", ex.Message); throw; } } /// /// Calculates win rate. /// /// Trade records. /// Win rate in range [0,1]. public double CalculateWinRate(List trades) { if (trades == null) throw new ArgumentNullException("trades"); try { if (trades.Count == 0) return 0.0; var wins = trades.Count(t => t.RealizedPnL > 0.0); return (double)wins / trades.Count; } catch (Exception ex) { _logger.LogError("CalculateWinRate failed: {0}", ex.Message); throw; } } /// /// Calculates profit factor. /// /// Trade records. /// Profit factor ratio. public double CalculateProfitFactor(List trades) { if (trades == null) throw new ArgumentNullException("trades"); try { var grossProfit = trades.Where(t => t.RealizedPnL > 0.0).Sum(t => t.RealizedPnL); var grossLoss = Math.Abs(trades.Where(t => t.RealizedPnL < 0.0).Sum(t => t.RealizedPnL)); if (grossLoss <= 0.0) return grossProfit > 0.0 ? double.PositiveInfinity : 0.0; return grossProfit / grossLoss; } catch (Exception ex) { _logger.LogError("CalculateProfitFactor failed: {0}", ex.Message); throw; } } /// /// Calculates expectancy per trade. /// /// Trade records. /// Expectancy value. public double CalculateExpectancy(List trades) { if (trades == null) throw new ArgumentNullException("trades"); try { if (trades.Count == 0) return 0.0; var wins = trades.Where(t => t.RealizedPnL > 0.0).ToList(); var losses = trades.Where(t => t.RealizedPnL < 0.0).ToList(); var winRate = (double)wins.Count / trades.Count; var lossRate = (double)losses.Count / trades.Count; var avgWin = wins.Count > 0 ? wins.Average(t => t.RealizedPnL) : 0.0; var avgLoss = losses.Count > 0 ? Math.Abs(losses.Average(t => t.RealizedPnL)) : 0.0; return (winRate * avgWin) - (lossRate * avgLoss); } catch (Exception ex) { _logger.LogError("CalculateExpectancy failed: {0}", ex.Message); throw; } } /// /// Calculates Sharpe ratio. /// /// Trade records. /// Risk free return per trade period. /// Sharpe ratio value. public double CalculateSharpeRatio(List trades, double riskFreeRate) { if (trades == null) throw new ArgumentNullException("trades"); try { if (trades.Count < 2) return 0.0; var returns = trades.Select(t => t.RealizedPnL).ToList(); var mean = returns.Average(); var variance = returns.Sum(r => (r - mean) * (r - mean)) / (returns.Count - 1); var stdDev = Math.Sqrt(variance); if (stdDev <= 0.0) return 0.0; return (mean - riskFreeRate) / stdDev; } catch (Exception ex) { _logger.LogError("CalculateSharpeRatio failed: {0}", ex.Message); throw; } } /// /// Calculates Sortino ratio. /// /// Trade records. /// Risk free return per trade period. /// Sortino ratio value. public double CalculateSortinoRatio(List trades, double riskFreeRate) { if (trades == null) throw new ArgumentNullException("trades"); try { if (trades.Count < 2) return 0.0; var returns = trades.Select(t => t.RealizedPnL).ToList(); var mean = returns.Average(); var downside = returns.Where(r => r < riskFreeRate).ToList(); if (downside.Count == 0) return 0.0; var downsideVariance = downside.Sum(r => (r - riskFreeRate) * (r - riskFreeRate)) / downside.Count; var downsideDev = Math.Sqrt(downsideVariance); if (downsideDev <= 0.0) return 0.0; return (mean - riskFreeRate) / downsideDev; } catch (Exception ex) { _logger.LogError("CalculateSortinoRatio failed: {0}", ex.Message); throw; } } /// /// Calculates maximum drawdown percent from cumulative realized PnL. /// /// Trade records. /// Max drawdown in percent points. public double CalculateMaxDrawdown(List trades) { if (trades == null) throw new ArgumentNullException("trades"); try { if (trades.Count == 0) return 0.0; var ordered = trades.OrderBy(t => t.ExitTime.HasValue ? t.ExitTime.Value : t.EntryTime).ToList(); var equity = 0.0; var peak = 0.0; var maxDrawdown = 0.0; foreach (var trade in ordered) { equity += trade.RealizedPnL; if (equity > peak) peak = equity; var drawdown = peak - equity; if (drawdown > maxDrawdown) maxDrawdown = drawdown; } if (peak <= 0.0) return maxDrawdown; return (maxDrawdown / peak) * 100.0; } catch (Exception ex) { _logger.LogError("CalculateMaxDrawdown failed: {0}", ex.Message); throw; } } } }