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