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;
}
}
}
}