using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; using NT8.Core.Common.Models; using NT8.Core.Intelligence; using NT8.Core.Logging; namespace NT8.Core.Analytics { /// /// Records and queries complete trade lifecycle information. /// public class TradeRecorder { private readonly ILogger _logger; private readonly object _lock; private readonly Dictionary _trades; private readonly Dictionary> _fillsByTrade; /// /// Initializes a new instance of the trade recorder. /// /// Logger implementation. public TradeRecorder(ILogger logger) { if (logger == null) throw new ArgumentNullException("logger"); _logger = logger; _lock = new object(); _trades = new Dictionary(); _fillsByTrade = new Dictionary>(); } /// /// Records trade entry details. /// /// Trade identifier. /// Strategy intent used for the trade. /// Entry fill event. /// Confluence score at entry. /// Risk mode at entry. public void RecordEntry(string tradeId, StrategyIntent intent, OrderFill fill, ConfluenceScore score, RiskMode mode) { if (string.IsNullOrEmpty(tradeId)) throw new ArgumentNullException("tradeId"); if (intent == null) throw new ArgumentNullException("intent"); if (fill == null) throw new ArgumentNullException("fill"); if (score == null) throw new ArgumentNullException("score"); try { var record = new TradeRecord(); record.TradeId = tradeId; record.Symbol = intent.Symbol; record.StrategyName = ResolveStrategyName(intent); record.EntryTime = fill.FillTime; record.ExitTime = null; record.Side = intent.Side; record.Quantity = fill.Quantity; record.EntryPrice = fill.FillPrice; record.ExitPrice = null; record.RealizedPnL = 0.0; record.UnrealizedPnL = 0.0; record.Grade = score.Grade; record.ConfluenceScore = score.WeightedScore; record.RiskMode = mode; record.VolatilityRegime = ResolveVolatilityRegime(intent, score); record.TrendRegime = ResolveTrendRegime(intent, score); record.StopTicks = intent.StopTicks; record.TargetTicks = intent.TargetTicks.HasValue ? intent.TargetTicks.Value : 0; record.RMultiple = 0.0; record.Duration = TimeSpan.Zero; record.Metadata.Add("entry_fill_id", fill.ExecutionId ?? string.Empty); record.Metadata.Add("entry_commission", fill.Commission); lock (_lock) { _trades[tradeId] = record; if (!_fillsByTrade.ContainsKey(tradeId)) _fillsByTrade.Add(tradeId, new List()); _fillsByTrade[tradeId].Add(fill); } _logger.LogInformation("Trade entry recorded: {0} {1} {2} @ {3:F2}", tradeId, record.Symbol, record.Quantity, record.EntryPrice); } catch (Exception ex) { _logger.LogError("RecordEntry failed for trade {0}: {1}", tradeId, ex.Message); throw; } } /// /// Records full trade exit and finalizes metrics. /// /// Trade identifier. /// Exit fill event. public void RecordExit(string tradeId, OrderFill fill) { if (string.IsNullOrEmpty(tradeId)) throw new ArgumentNullException("tradeId"); if (fill == null) throw new ArgumentNullException("fill"); try { lock (_lock) { if (!_trades.ContainsKey(tradeId)) throw new ArgumentException("Trade not found", "tradeId"); var record = _trades[tradeId]; record.ExitTime = fill.FillTime; record.ExitPrice = fill.FillPrice; record.Duration = record.ExitTime.Value - record.EntryTime; if (!_fillsByTrade.ContainsKey(tradeId)) _fillsByTrade.Add(tradeId, new List()); _fillsByTrade[tradeId].Add(fill); var totalExitQty = _fillsByTrade[tradeId] .Skip(1) .Sum(f => f.Quantity); if (totalExitQty > 0) { var weightedExitPrice = _fillsByTrade[tradeId] .Skip(1) .Sum(f => f.FillPrice * f.Quantity) / totalExitQty; record.ExitPrice = weightedExitPrice; } var signedMove = (record.ExitPrice.HasValue ? record.ExitPrice.Value : record.EntryPrice) - record.EntryPrice; if (record.Side == OrderSide.Sell) signedMove = -signedMove; record.RealizedPnL = signedMove * record.Quantity; record.UnrealizedPnL = 0.0; var stopRisk = record.StopTicks <= 0 ? 0.0 : record.StopTicks; if (stopRisk > 0.0) record.RMultiple = signedMove / stopRisk; record.Metadata["exit_fill_id"] = fill.ExecutionId ?? string.Empty; record.Metadata["exit_commission"] = fill.Commission; } _logger.LogInformation("Trade exit recorded: {0}", tradeId); } catch (Exception ex) { _logger.LogError("RecordExit failed for trade {0}: {1}", tradeId, ex.Message); throw; } } /// /// Records a partial fill event. /// /// Trade identifier. /// Partial fill event. public void RecordPartialFill(string tradeId, OrderFill fill) { if (string.IsNullOrEmpty(tradeId)) throw new ArgumentNullException("tradeId"); if (fill == null) throw new ArgumentNullException("fill"); try { lock (_lock) { if (!_fillsByTrade.ContainsKey(tradeId)) _fillsByTrade.Add(tradeId, new List()); _fillsByTrade[tradeId].Add(fill); if (_trades.ContainsKey(tradeId)) { _trades[tradeId].Metadata["partial_fill_count"] = _fillsByTrade[tradeId].Count; } } } catch (Exception ex) { _logger.LogError("RecordPartialFill failed for trade {0}: {1}", tradeId, ex.Message); throw; } } /// /// Gets a single trade by identifier. /// /// Trade identifier. /// Trade record if found. public TradeRecord GetTrade(string tradeId) { if (string.IsNullOrEmpty(tradeId)) throw new ArgumentNullException("tradeId"); try { lock (_lock) { TradeRecord record; if (!_trades.TryGetValue(tradeId, out record)) return null; return Clone(record); } } catch (Exception ex) { _logger.LogError("GetTrade failed for trade {0}: {1}", tradeId, ex.Message); throw; } } /// /// Gets trades in a time range. /// /// Start timestamp inclusive. /// End timestamp inclusive. /// Trade records in range. public List GetTrades(DateTime start, DateTime end) { try { lock (_lock) { return _trades.Values .Where(t => t.EntryTime >= start && t.EntryTime <= end) .OrderBy(t => t.EntryTime) .Select(Clone) .ToList(); } } catch (Exception ex) { _logger.LogError("GetTrades failed: {0}", ex.Message); throw; } } /// /// Gets trades by grade. /// /// Target grade. /// Trade list. public List GetTradesByGrade(TradeGrade grade) { try { lock (_lock) { return _trades.Values .Where(t => t.Grade == grade) .OrderBy(t => t.EntryTime) .Select(Clone) .ToList(); } } catch (Exception ex) { _logger.LogError("GetTradesByGrade failed: {0}", ex.Message); throw; } } /// /// Gets trades by strategy name. /// /// Strategy name. /// Trade list. public List GetTradesByStrategy(string strategyName) { if (string.IsNullOrEmpty(strategyName)) throw new ArgumentNullException("strategyName"); try { lock (_lock) { return _trades.Values .Where(t => string.Equals(t.StrategyName, strategyName, StringComparison.OrdinalIgnoreCase)) .OrderBy(t => t.EntryTime) .Select(Clone) .ToList(); } } catch (Exception ex) { _logger.LogError("GetTradesByStrategy failed: {0}", ex.Message); throw; } } /// /// Exports all trades to CSV. /// /// CSV text. public string ExportToCsv() { try { var rows = new StringBuilder(); rows.AppendLine("TradeId,Symbol,StrategyName,EntryTime,ExitTime,Side,Quantity,EntryPrice,ExitPrice,RealizedPnL,Grade,RiskMode,VolatilityRegime,TrendRegime,RMultiple"); List trades; lock (_lock) { trades = _trades.Values.OrderBy(t => t.EntryTime).Select(Clone).ToList(); } foreach (var trade in trades) { rows.AppendFormat(CultureInfo.InvariantCulture, "{0},{1},{2},{3:O},{4},{5},{6},{7:F4},{8},{9:F2},{10},{11},{12},{13},{14:F4}", EscapeCsv(trade.TradeId), EscapeCsv(trade.Symbol), EscapeCsv(trade.StrategyName), trade.EntryTime, trade.ExitTime.HasValue ? trade.ExitTime.Value.ToString("O") : string.Empty, trade.Side, trade.Quantity, trade.EntryPrice, trade.ExitPrice.HasValue ? trade.ExitPrice.Value.ToString("F4", CultureInfo.InvariantCulture) : string.Empty, trade.RealizedPnL, trade.Grade, trade.RiskMode, trade.VolatilityRegime, trade.TrendRegime, trade.RMultiple); rows.AppendLine(); } return rows.ToString(); } catch (Exception ex) { _logger.LogError("ExportToCsv failed: {0}", ex.Message); throw; } } /// /// Exports all trades to JSON. /// /// JSON text. public string ExportToJson() { try { List trades; lock (_lock) { trades = _trades.Values.OrderBy(t => t.EntryTime).Select(Clone).ToList(); } var builder = new StringBuilder(); builder.Append("["); for (var i = 0; i < trades.Count; i++) { var trade = trades[i]; if (i > 0) builder.Append(","); builder.Append("{"); builder.AppendFormat(CultureInfo.InvariantCulture, "\"tradeId\":\"{0}\"", EscapeJson(trade.TradeId)); builder.AppendFormat(CultureInfo.InvariantCulture, ",\"symbol\":\"{0}\"", EscapeJson(trade.Symbol)); builder.AppendFormat(CultureInfo.InvariantCulture, ",\"strategyName\":\"{0}\"", EscapeJson(trade.StrategyName)); builder.AppendFormat(CultureInfo.InvariantCulture, ",\"entryTime\":\"{0:O}\"", trade.EntryTime); if (trade.ExitTime.HasValue) builder.AppendFormat(CultureInfo.InvariantCulture, ",\"exitTime\":\"{0:O}\"", trade.ExitTime.Value); else builder.Append(",\"exitTime\":null"); builder.AppendFormat(CultureInfo.InvariantCulture, ",\"side\":\"{0}\"", trade.Side); builder.AppendFormat(CultureInfo.InvariantCulture, ",\"quantity\":{0}", trade.Quantity); builder.AppendFormat(CultureInfo.InvariantCulture, ",\"entryPrice\":{0}", trade.EntryPrice); if (trade.ExitPrice.HasValue) builder.AppendFormat(CultureInfo.InvariantCulture, ",\"exitPrice\":{0}", trade.ExitPrice.Value); else builder.Append(",\"exitPrice\":null"); builder.AppendFormat(CultureInfo.InvariantCulture, ",\"realizedPnL\":{0}", trade.RealizedPnL); builder.AppendFormat(CultureInfo.InvariantCulture, ",\"grade\":\"{0}\"", trade.Grade); builder.AppendFormat(CultureInfo.InvariantCulture, ",\"riskMode\":\"{0}\"", trade.RiskMode); builder.Append("}"); } builder.Append("]"); return builder.ToString(); } catch (Exception ex) { _logger.LogError("ExportToJson failed: {0}", ex.Message); throw; } } private static string ResolveStrategyName(StrategyIntent intent) { object name; if (intent.Metadata != null && intent.Metadata.TryGetValue("strategy_name", out name) && name != null) return name.ToString(); return "Unknown"; } private static VolatilityRegime ResolveVolatilityRegime(StrategyIntent intent, ConfluenceScore score) { object value; if (TryGetMetadataValue(intent, score, "volatility_regime", out value)) { VolatilityRegime parsed; if (Enum.TryParse(value.ToString(), true, out parsed)) return parsed; } return VolatilityRegime.Normal; } private static TrendRegime ResolveTrendRegime(StrategyIntent intent, ConfluenceScore score) { object value; if (TryGetMetadataValue(intent, score, "trend_regime", out value)) { TrendRegime parsed; if (Enum.TryParse(value.ToString(), true, out parsed)) return parsed; } return TrendRegime.Range; } private static bool TryGetMetadataValue(StrategyIntent intent, ConfluenceScore score, string key, out object value) { value = null; if (intent.Metadata != null && intent.Metadata.TryGetValue(key, out value)) return true; if (score.Metadata != null && score.Metadata.TryGetValue(key, out value)) return true; return false; } private static TradeRecord Clone(TradeRecord input) { var clone = new TradeRecord(); clone.TradeId = input.TradeId; clone.Symbol = input.Symbol; clone.StrategyName = input.StrategyName; clone.EntryTime = input.EntryTime; clone.ExitTime = input.ExitTime; clone.Side = input.Side; clone.Quantity = input.Quantity; clone.EntryPrice = input.EntryPrice; clone.ExitPrice = input.ExitPrice; clone.RealizedPnL = input.RealizedPnL; clone.UnrealizedPnL = input.UnrealizedPnL; clone.Grade = input.Grade; clone.ConfluenceScore = input.ConfluenceScore; clone.RiskMode = input.RiskMode; clone.VolatilityRegime = input.VolatilityRegime; clone.TrendRegime = input.TrendRegime; clone.StopTicks = input.StopTicks; clone.TargetTicks = input.TargetTicks; clone.RMultiple = input.RMultiple; clone.Duration = input.Duration; clone.Metadata = new Dictionary(input.Metadata); return clone; } private static string EscapeCsv(string value) { if (value == null) return string.Empty; if (value.Contains(",") || value.Contains("\"") || value.Contains("\n") || value.Contains("\r")) return string.Format("\"{0}\"", value.Replace("\"", "\"\"")); return value; } private static string EscapeJson(string value) { if (value == null) return string.Empty; return value .Replace("\\", "\\\\") .Replace("\"", "\\\"") .Replace("\r", "\\r") .Replace("\n", "\\n"); } } }