using System; using System.Collections.Generic; using System.Linq; using NT8.Core.Intelligence; using NT8.Core.Logging; namespace NT8.Core.Analytics { /// /// Provides PnL attribution analysis across multiple dimensions. /// public class PnLAttributor { private readonly ILogger _logger; /// /// Initializes a new attributor instance. /// /// Logger dependency. public PnLAttributor(ILogger logger) { if (logger == null) throw new ArgumentNullException("logger"); _logger = logger; } /// /// Attributes PnL by trade grade. /// /// Trade records. /// Attribution report. public AttributionReport AttributeByGrade(List trades) { return BuildReport(trades, AttributionDimension.Grade, t => t.Grade.ToString()); } /// /// Attributes PnL by combined volatility and trend regime. /// /// Trade records. /// Attribution report. public AttributionReport AttributeByRegime(List trades) { return BuildReport( trades, AttributionDimension.Regime, t => string.Format("{0}|{1}", t.VolatilityRegime, t.TrendRegime)); } /// /// Attributes PnL by strategy name. /// /// Trade records. /// Attribution report. public AttributionReport AttributeByStrategy(List trades) { return BuildReport(trades, AttributionDimension.Strategy, t => t.StrategyName ?? string.Empty); } /// /// Attributes PnL by time-of-day bucket. /// /// Trade records. /// Attribution report. public AttributionReport AttributeByTimeOfDay(List trades) { return BuildReport(trades, AttributionDimension.Time, GetTimeBucket); } /// /// Attributes PnL by a multi-dimensional combined key. /// /// Trade records. /// Dimensions to combine. /// Attribution report. public AttributionReport AttributeMultiDimensional(List trades, List 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(); 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 trades, AttributionDimension dimension, Func 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; } } } }