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:
199
src/NT8.Core/Analytics/PnLAttributor.cs
Normal file
199
src/NT8.Core/Analytics/PnLAttributor.cs
Normal file
@@ -0,0 +1,199 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NT8.Core.Intelligence;
|
||||
using NT8.Core.Logging;
|
||||
|
||||
namespace NT8.Core.Analytics
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides PnL attribution analysis across multiple dimensions.
|
||||
/// </summary>
|
||||
public class PnLAttributor
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new attributor instance.
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger dependency.</param>
|
||||
public PnLAttributor(ILogger logger)
|
||||
{
|
||||
if (logger == null)
|
||||
throw new ArgumentNullException("logger");
|
||||
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attributes PnL by trade grade.
|
||||
/// </summary>
|
||||
/// <param name="trades">Trade records.</param>
|
||||
/// <returns>Attribution report.</returns>
|
||||
public AttributionReport AttributeByGrade(List<TradeRecord> trades)
|
||||
{
|
||||
return BuildReport(trades, AttributionDimension.Grade, t => t.Grade.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attributes PnL by combined volatility and trend regime.
|
||||
/// </summary>
|
||||
/// <param name="trades">Trade records.</param>
|
||||
/// <returns>Attribution report.</returns>
|
||||
public AttributionReport AttributeByRegime(List<TradeRecord> trades)
|
||||
{
|
||||
return BuildReport(
|
||||
trades,
|
||||
AttributionDimension.Regime,
|
||||
t => string.Format("{0}|{1}", t.VolatilityRegime, t.TrendRegime));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attributes PnL by strategy name.
|
||||
/// </summary>
|
||||
/// <param name="trades">Trade records.</param>
|
||||
/// <returns>Attribution report.</returns>
|
||||
public AttributionReport AttributeByStrategy(List<TradeRecord> trades)
|
||||
{
|
||||
return BuildReport(trades, AttributionDimension.Strategy, t => t.StrategyName ?? string.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attributes PnL by time-of-day bucket.
|
||||
/// </summary>
|
||||
/// <param name="trades">Trade records.</param>
|
||||
/// <returns>Attribution report.</returns>
|
||||
public AttributionReport AttributeByTimeOfDay(List<TradeRecord> trades)
|
||||
{
|
||||
return BuildReport(trades, AttributionDimension.Time, GetTimeBucket);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attributes PnL by a multi-dimensional combined key.
|
||||
/// </summary>
|
||||
/// <param name="trades">Trade records.</param>
|
||||
/// <param name="dimensions">Dimensions to combine.</param>
|
||||
/// <returns>Attribution report.</returns>
|
||||
public AttributionReport AttributeMultiDimensional(List<TradeRecord> trades, List<AttributionDimension> dimensions)
|
||||
{
|
||||
if (dimensions == null)
|
||||
throw new ArgumentNullException("dimensions");
|
||||
if (dimensions.Count == 0)
|
||||
throw new ArgumentException("At least one dimension is required", "dimensions");
|
||||
|
||||
try
|
||||
{
|
||||
return BuildReport(trades, AttributionDimension.Strategy, delegate(TradeRecord trade)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
foreach (var dimension in dimensions)
|
||||
{
|
||||
parts.Add(GetDimensionValue(trade, dimension));
|
||||
}
|
||||
|
||||
return string.Join("|", parts.ToArray());
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("AttributeMultiDimensional failed: {0}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private AttributionReport BuildReport(
|
||||
List<TradeRecord> trades,
|
||||
AttributionDimension dimension,
|
||||
Func<TradeRecord, string> keySelector)
|
||||
{
|
||||
if (trades == null)
|
||||
throw new ArgumentNullException("trades");
|
||||
if (keySelector == null)
|
||||
throw new ArgumentNullException("keySelector");
|
||||
|
||||
try
|
||||
{
|
||||
var report = new AttributionReport();
|
||||
report.Dimension = dimension;
|
||||
report.TotalTrades = trades.Count;
|
||||
report.TotalPnL = trades.Sum(t => t.RealizedPnL);
|
||||
|
||||
var groups = trades.GroupBy(keySelector).ToList();
|
||||
foreach (var group in groups)
|
||||
{
|
||||
var tradeList = group.ToList();
|
||||
var totalPnL = tradeList.Sum(t => t.RealizedPnL);
|
||||
var wins = tradeList.Count(t => t.RealizedPnL > 0.0);
|
||||
var losses = tradeList.Count(t => t.RealizedPnL < 0.0);
|
||||
var grossProfit = tradeList.Where(t => t.RealizedPnL > 0.0).Sum(t => t.RealizedPnL);
|
||||
var grossLoss = Math.Abs(tradeList.Where(t => t.RealizedPnL < 0.0).Sum(t => t.RealizedPnL));
|
||||
|
||||
var slice = new AttributionSlice();
|
||||
slice.DimensionName = dimension.ToString();
|
||||
slice.DimensionValue = group.Key;
|
||||
slice.TotalPnL = totalPnL;
|
||||
slice.TradeCount = tradeList.Count;
|
||||
slice.AvgPnL = tradeList.Count > 0 ? totalPnL / tradeList.Count : 0.0;
|
||||
slice.WinRate = tradeList.Count > 0 ? (double)wins / tradeList.Count : 0.0;
|
||||
slice.ProfitFactor = grossLoss > 0.0
|
||||
? grossProfit / grossLoss
|
||||
: (grossProfit > 0.0 ? double.PositiveInfinity : 0.0);
|
||||
slice.Contribution = report.TotalPnL != 0.0 ? totalPnL / report.TotalPnL : 0.0;
|
||||
|
||||
report.Slices.Add(slice);
|
||||
}
|
||||
|
||||
report.Slices = report.Slices
|
||||
.OrderByDescending(s => s.TotalPnL)
|
||||
.ToList();
|
||||
|
||||
report.Metadata.Add("group_count", report.Slices.Count);
|
||||
report.Metadata.Add("winners", trades.Count(t => t.RealizedPnL > 0.0));
|
||||
report.Metadata.Add("losers", trades.Count(t => t.RealizedPnL < 0.0));
|
||||
|
||||
return report;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("BuildReport failed for dimension {0}: {1}", dimension, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetTimeBucket(TradeRecord trade)
|
||||
{
|
||||
var local = trade.EntryTime;
|
||||
var time = local.TimeOfDay;
|
||||
|
||||
if (time < new TimeSpan(10, 30, 0))
|
||||
return "FirstHour";
|
||||
if (time < new TimeSpan(14, 0, 0))
|
||||
return "MidDay";
|
||||
if (time < new TimeSpan(16, 0, 0))
|
||||
return "LastHour";
|
||||
|
||||
return "AfterHours";
|
||||
}
|
||||
|
||||
private static string GetDimensionValue(TradeRecord trade, AttributionDimension dimension)
|
||||
{
|
||||
switch (dimension)
|
||||
{
|
||||
case AttributionDimension.Strategy:
|
||||
return trade.StrategyName ?? string.Empty;
|
||||
case AttributionDimension.Grade:
|
||||
return trade.Grade.ToString();
|
||||
case AttributionDimension.Regime:
|
||||
return string.Format("{0}|{1}", trade.VolatilityRegime, trade.TrendRegime);
|
||||
case AttributionDimension.Time:
|
||||
return GetTimeBucket(trade);
|
||||
case AttributionDimension.Symbol:
|
||||
return trade.Symbol ?? string.Empty;
|
||||
case AttributionDimension.RiskMode:
|
||||
return trade.RiskMode.ToString();
|
||||
default:
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user