Files
nt8-sdk/src/NT8.Core/Analytics/PerformanceCalculator.cs
mo 0e36fe5d23
Some checks failed
Build and Test / build (push) Has been cancelled
feat: Complete Phase 5 Analytics & Reporting implementation
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
2026-02-16 21:30:51 -05:00

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