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
200 lines
7.6 KiB
C#
200 lines
7.6 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
}
|