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
265 lines
8.8 KiB
C#
265 lines
8.8 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|