Some checks failed
Build and Test / build (push) Has been cancelled
Analytics Layer (15 components): - TradeRecorder: Full trade lifecycle tracking with partial fills - PerformanceCalculator: Sharpe, Sortino, win rate, profit factor, expectancy - PnLAttributor: Multi-dimensional attribution (grade/regime/time/strategy) - DrawdownAnalyzer: Period detection and recovery metrics - GradePerformanceAnalyzer: Grade-level edge analysis - RegimePerformanceAnalyzer: Regime segmentation and transitions - ConfluenceValidator: Factor validation and weighting optimization - ReportGenerator: Daily/weekly/monthly reporting with export - TradeBlotter: Real-time trade ledger with filtering - ParameterOptimizer: Grid search and walk-forward scaffolding - MonteCarloSimulator: Confidence intervals and risk-of-ruin - PortfolioOptimizer: Multi-strategy allocation and portfolio metrics Test Coverage (90 new tests): - 240+ total tests, 100% pass rate - >85% code coverage - Zero new warnings Project Status: Phase 5 complete (85% overall), ready for NT8 integration
270 lines
9.5 KiB
C#
270 lines
9.5 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using NT8.Core.Logging;
|
|
|
|
namespace NT8.Core.Analytics
|
|
{
|
|
/// <summary>
|
|
/// Calculates aggregate performance metrics for trade sets.
|
|
/// </summary>
|
|
public class PerformanceCalculator
|
|
{
|
|
private readonly ILogger _logger;
|
|
|
|
/// <summary>
|
|
/// Initializes a new calculator instance.
|
|
/// </summary>
|
|
/// <param name="logger">Logger dependency.</param>
|
|
public PerformanceCalculator(ILogger logger)
|
|
{
|
|
if (logger == null)
|
|
throw new ArgumentNullException("logger");
|
|
|
|
_logger = logger;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates all core metrics from trades.
|
|
/// </summary>
|
|
/// <param name="trades">Trade records.</param>
|
|
/// <returns>Performance metrics snapshot.</returns>
|
|
public PerformanceMetrics Calculate(List<TradeRecord> 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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates win rate.
|
|
/// </summary>
|
|
/// <param name="trades">Trade records.</param>
|
|
/// <returns>Win rate in range [0,1].</returns>
|
|
public double CalculateWinRate(List<TradeRecord> 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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates profit factor.
|
|
/// </summary>
|
|
/// <param name="trades">Trade records.</param>
|
|
/// <returns>Profit factor ratio.</returns>
|
|
public double CalculateProfitFactor(List<TradeRecord> 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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates expectancy per trade.
|
|
/// </summary>
|
|
/// <param name="trades">Trade records.</param>
|
|
/// <returns>Expectancy value.</returns>
|
|
public double CalculateExpectancy(List<TradeRecord> 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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates Sharpe ratio.
|
|
/// </summary>
|
|
/// <param name="trades">Trade records.</param>
|
|
/// <param name="riskFreeRate">Risk free return per trade period.</param>
|
|
/// <returns>Sharpe ratio value.</returns>
|
|
public double CalculateSharpeRatio(List<TradeRecord> 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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates Sortino ratio.
|
|
/// </summary>
|
|
/// <param name="trades">Trade records.</param>
|
|
/// <param name="riskFreeRate">Risk free return per trade period.</param>
|
|
/// <returns>Sortino ratio value.</returns>
|
|
public double CalculateSortinoRatio(List<TradeRecord> 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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates maximum drawdown percent from cumulative realized PnL.
|
|
/// </summary>
|
|
/// <param name="trades">Trade records.</param>
|
|
/// <returns>Max drawdown in percent points.</returns>
|
|
public double CalculateMaxDrawdown(List<TradeRecord> 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;
|
|
}
|
|
}
|
|
}
|
|
}
|