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