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:
264
src/NT8.Core/Analytics/TradeBlotter.cs
Normal file
264
src/NT8.Core/Analytics/TradeBlotter.cs
Normal file
@@ -0,0 +1,264 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NT8.Core.Intelligence;
|
||||
using NT8.Core.Logging;
|
||||
|
||||
namespace NT8.Core.Analytics
|
||||
{
|
||||
/// <summary>
|
||||
/// Filterable and sortable trade blotter service.
|
||||
/// </summary>
|
||||
public class TradeBlotter
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly object _lock;
|
||||
private readonly List<TradeRecord> _trades;
|
||||
|
||||
public TradeBlotter(ILogger logger)
|
||||
{
|
||||
if (logger == null)
|
||||
throw new ArgumentNullException("logger");
|
||||
|
||||
_logger = logger;
|
||||
_lock = new object();
|
||||
_trades = new List<TradeRecord>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces blotter trade set.
|
||||
/// </summary>
|
||||
public void SetTrades(List<TradeRecord> trades)
|
||||
{
|
||||
if (trades == null)
|
||||
throw new ArgumentNullException("trades");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_trades.Clear();
|
||||
_trades.AddRange(trades.Select(Clone));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("SetTrades failed: {0}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends one trade and supports real-time update flow.
|
||||
/// </summary>
|
||||
public void AddOrUpdateTrade(TradeRecord trade)
|
||||
{
|
||||
if (trade == null)
|
||||
throw new ArgumentNullException("trade");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var index = _trades.FindIndex(t => t.TradeId == trade.TradeId);
|
||||
if (index >= 0)
|
||||
_trades[index] = Clone(trade);
|
||||
else
|
||||
_trades.Add(Clone(trade));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("AddOrUpdateTrade failed: {0}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters by date range.
|
||||
/// </summary>
|
||||
public List<TradeRecord> FilterByDate(DateTime start, DateTime end)
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _trades
|
||||
.Where(t => t.EntryTime >= start && t.EntryTime <= end)
|
||||
.OrderBy(t => t.EntryTime)
|
||||
.Select(Clone)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("FilterByDate failed: {0}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters by symbol.
|
||||
/// </summary>
|
||||
public List<TradeRecord> FilterBySymbol(string symbol)
|
||||
{
|
||||
if (string.IsNullOrEmpty(symbol))
|
||||
throw new ArgumentNullException("symbol");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _trades
|
||||
.Where(t => string.Equals(t.Symbol, symbol, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderBy(t => t.EntryTime)
|
||||
.Select(Clone)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("FilterBySymbol failed: {0}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters by grade.
|
||||
/// </summary>
|
||||
public List<TradeRecord> FilterByGrade(TradeGrade grade)
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _trades
|
||||
.Where(t => t.Grade == grade)
|
||||
.OrderBy(t => t.EntryTime)
|
||||
.Select(Clone)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("FilterByGrade failed: {0}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters by realized pnl range.
|
||||
/// </summary>
|
||||
public List<TradeRecord> FilterByPnL(double minPnL, double maxPnL)
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _trades
|
||||
.Where(t => t.RealizedPnL >= minPnL && t.RealizedPnL <= maxPnL)
|
||||
.OrderBy(t => t.EntryTime)
|
||||
.Select(Clone)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("FilterByPnL failed: {0}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sorts by named column.
|
||||
/// </summary>
|
||||
public List<TradeRecord> SortBy(string column, SortDirection direction)
|
||||
{
|
||||
if (string.IsNullOrEmpty(column))
|
||||
throw new ArgumentNullException("column");
|
||||
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
IEnumerable<TradeRecord> ordered;
|
||||
var normalized = column.Trim().ToLowerInvariant();
|
||||
|
||||
switch (normalized)
|
||||
{
|
||||
case "time":
|
||||
case "entrytime":
|
||||
ordered = direction == SortDirection.Asc
|
||||
? _trades.OrderBy(t => t.EntryTime)
|
||||
: _trades.OrderByDescending(t => t.EntryTime);
|
||||
break;
|
||||
case "symbol":
|
||||
ordered = direction == SortDirection.Asc
|
||||
? _trades.OrderBy(t => t.Symbol)
|
||||
: _trades.OrderByDescending(t => t.Symbol);
|
||||
break;
|
||||
case "pnl":
|
||||
ordered = direction == SortDirection.Asc
|
||||
? _trades.OrderBy(t => t.RealizedPnL)
|
||||
: _trades.OrderByDescending(t => t.RealizedPnL);
|
||||
break;
|
||||
case "grade":
|
||||
ordered = direction == SortDirection.Asc
|
||||
? _trades.OrderBy(t => t.Grade)
|
||||
: _trades.OrderByDescending(t => t.Grade);
|
||||
break;
|
||||
case "rmultiple":
|
||||
ordered = direction == SortDirection.Asc
|
||||
? _trades.OrderBy(t => t.RMultiple)
|
||||
: _trades.OrderByDescending(t => t.RMultiple);
|
||||
break;
|
||||
case "duration":
|
||||
ordered = direction == SortDirection.Asc
|
||||
? _trades.OrderBy(t => t.Duration)
|
||||
: _trades.OrderByDescending(t => t.Duration);
|
||||
break;
|
||||
default:
|
||||
ordered = direction == SortDirection.Asc
|
||||
? _trades.OrderBy(t => t.EntryTime)
|
||||
: _trades.OrderByDescending(t => t.EntryTime);
|
||||
break;
|
||||
}
|
||||
|
||||
return ordered.Select(Clone).ToList();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("SortBy failed: {0}", ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static TradeRecord Clone(TradeRecord input)
|
||||
{
|
||||
var copy = new TradeRecord();
|
||||
copy.TradeId = input.TradeId;
|
||||
copy.Symbol = input.Symbol;
|
||||
copy.StrategyName = input.StrategyName;
|
||||
copy.EntryTime = input.EntryTime;
|
||||
copy.ExitTime = input.ExitTime;
|
||||
copy.Side = input.Side;
|
||||
copy.Quantity = input.Quantity;
|
||||
copy.EntryPrice = input.EntryPrice;
|
||||
copy.ExitPrice = input.ExitPrice;
|
||||
copy.RealizedPnL = input.RealizedPnL;
|
||||
copy.UnrealizedPnL = input.UnrealizedPnL;
|
||||
copy.Grade = input.Grade;
|
||||
copy.ConfluenceScore = input.ConfluenceScore;
|
||||
copy.RiskMode = input.RiskMode;
|
||||
copy.VolatilityRegime = input.VolatilityRegime;
|
||||
copy.TrendRegime = input.TrendRegime;
|
||||
copy.StopTicks = input.StopTicks;
|
||||
copy.TargetTicks = input.TargetTicks;
|
||||
copy.RMultiple = input.RMultiple;
|
||||
copy.Duration = input.Duration;
|
||||
copy.Metadata = new Dictionary<string, object>(input.Metadata);
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user