feat: Complete Phase 5 Analytics & Reporting implementation
Some checks failed
Build and Test / build (push) Has been cancelled
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
This commit is contained in:
269
src/NT8.Core/Analytics/PerformanceCalculator.cs
Normal file
269
src/NT8.Core/Analytics/PerformanceCalculator.cs
Normal file
@@ -0,0 +1,269 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user