using NT8.Core.Common.Models; using NT8.Core.Logging; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace NT8.Core.Risk { /// /// Basic risk manager implementing Tier 1 risk controls /// Thread-safe implementation using locks for state consistency /// public class BasicRiskManager : IRiskManager { private readonly ILogger _logger; private readonly object _lock = new object(); // Risk state - protected by _lock private double _dailyPnL; private double _maxDrawdown; private bool _tradingHalted; private DateTime _lastUpdate = DateTime.UtcNow; private readonly Dictionary _symbolExposure = new Dictionary(); public BasicRiskManager(ILogger logger) { if (logger == null) throw new ArgumentNullException("logger"); _logger = logger; } public RiskDecision ValidateOrder(StrategyIntent intent, StrategyContext context, RiskConfig config) { if (intent == null) throw new ArgumentNullException("intent"); if (context == null) throw new ArgumentNullException("context"); if (config == null) throw new ArgumentNullException("config"); lock (_lock) { // Check if trading is halted if (_tradingHalted) { _logger.LogWarning("Order rejected - trading halted by risk manager"); var haltedMetrics = new Dictionary(); haltedMetrics.Add("halted", true); haltedMetrics.Add("daily_pnl", _dailyPnL); return new RiskDecision( allow: false, rejectReason: "Trading halted by risk manager", modifiedIntent: null, riskLevel: RiskLevel.Critical, riskMetrics: haltedMetrics ); } // Tier 1: Daily loss cap if (_dailyPnL <= -config.DailyLossLimit) { _tradingHalted = true; _logger.LogCritical("Daily loss limit breached: {0:C} <= {1:C}", _dailyPnL, -config.DailyLossLimit); var limitMetrics = new Dictionary(); limitMetrics.Add("daily_pnl", _dailyPnL); limitMetrics.Add("limit", config.DailyLossLimit); return new RiskDecision( allow: false, rejectReason: String.Format("Daily loss limit breached: {0:C}", _dailyPnL), modifiedIntent: null, riskLevel: RiskLevel.Critical, riskMetrics: limitMetrics ); } // Tier 1: Per-trade risk limit var tradeRisk = CalculateTradeRisk(intent, context); if (tradeRisk > config.MaxTradeRisk) { _logger.LogWarning("Trade risk too high: {0:C} > {1:C}", tradeRisk, config.MaxTradeRisk); var riskMetrics = new Dictionary(); riskMetrics.Add("trade_risk", tradeRisk); riskMetrics.Add("limit", config.MaxTradeRisk); return new RiskDecision( allow: false, rejectReason: String.Format("Trade risk too high: {0:C}", tradeRisk), modifiedIntent: null, riskLevel: RiskLevel.High, riskMetrics: riskMetrics ); } // Tier 1: Position limits var currentPositions = GetOpenPositionCount(); if (currentPositions >= config.MaxOpenPositions && context.CurrentPosition.Quantity == 0) { _logger.LogWarning("Max open positions exceeded: {0} >= {1}", currentPositions, config.MaxOpenPositions); var positionMetrics = new Dictionary(); positionMetrics.Add("open_positions", currentPositions); positionMetrics.Add("limit", config.MaxOpenPositions); return new RiskDecision( allow: false, rejectReason: String.Format("Max open positions exceeded: {0}", currentPositions), modifiedIntent: null, riskLevel: RiskLevel.Medium, riskMetrics: positionMetrics ); } // All checks passed - determine risk level var riskLevel = DetermineRiskLevel(config); _logger.LogDebug("Order approved: {0} {1} risk=${2:F2} level={3}", intent.Symbol, intent.Side, tradeRisk, riskLevel); var successMetrics = new Dictionary(); successMetrics.Add("trade_risk", tradeRisk); successMetrics.Add("daily_pnl", _dailyPnL); successMetrics.Add("max_drawdown", _maxDrawdown); successMetrics.Add("open_positions", currentPositions); return new RiskDecision( allow: true, rejectReason: null, modifiedIntent: null, riskLevel: riskLevel, riskMetrics: successMetrics ); } } private static double CalculateTradeRisk(StrategyIntent intent, StrategyContext context) { // Get tick value for symbol - this will be enhanced in later phases var tickValue = GetTickValue(intent.Symbol); return intent.StopTicks * tickValue; } private static double GetTickValue(string symbol) { // Static tick values for Phase 0 - will be configurable in Phase 1 switch (symbol) { case "ES": return 12.50; case "MES": return 1.25; case "NQ": return 5.00; case "MNQ": return 0.50; case "CL": return 10.00; case "GC": return 10.00; default: return 12.50; // Default to ES } } private int GetOpenPositionCount() { // For Phase 0, return simplified count // Will be enhanced with actual position tracking in Phase 1 return _symbolExposure.Count(kvp => Math.Abs(kvp.Value) > 0.01); } private RiskLevel DetermineRiskLevel(RiskConfig config) { var lossPercent = Math.Abs(_dailyPnL) / config.DailyLossLimit; if (lossPercent >= 0.8) return RiskLevel.High; if (lossPercent >= 0.5) return RiskLevel.Medium; return RiskLevel.Low; } public void OnFill(OrderFill fill) { if (fill == null) throw new ArgumentNullException("fill"); lock (_lock) { _lastUpdate = DateTime.UtcNow; // Update symbol exposure var fillValue = fill.Quantity * fill.FillPrice; if (_symbolExposure.ContainsKey(fill.Symbol)) { _symbolExposure[fill.Symbol] += fillValue; } else { _symbolExposure[fill.Symbol] = fillValue; } _logger.LogDebug("Fill processed: {0} {1} @ {2:F2}, Exposure: {3:C}", fill.Symbol, fill.Quantity, fill.FillPrice, _symbolExposure[fill.Symbol]); } } public void OnPnLUpdate(double netPnL, double dayPnL) { lock (_lock) { var oldDailyPnL = _dailyPnL; _dailyPnL = dayPnL; _maxDrawdown = Math.Min(_maxDrawdown, dayPnL); _lastUpdate = DateTime.UtcNow; if (Math.Abs(dayPnL - oldDailyPnL) > 0.01) { _logger.LogDebug("P&L Update: Daily={0:C}, Max DD={1:C}", dayPnL, _maxDrawdown); } // Check for emergency conditions CheckEmergencyConditions(dayPnL); } } private void CheckEmergencyConditions(double dayPnL) { // Emergency halt if daily loss exceeds 90% of limit // Using a default limit of 1000 as this method doesn't have access to config // In Phase 1, this should be improved to use the actual config value if (dayPnL <= -(1000 * 0.9) && !_tradingHalted) { _tradingHalted = true; _logger.LogCritical("Emergency halt triggered at 90% of daily loss limit: {0:C}", dayPnL); } } public async Task EmergencyFlatten(string reason) { if (String.IsNullOrEmpty(reason)) throw new ArgumentException("Reason required", "reason"); lock (_lock) { _tradingHalted = true; _logger.LogCritical("Emergency flatten triggered: {0}", reason); } // In Phase 0, this is a placeholder // Phase 1 will implement actual position flattening via OMS await Task.Delay(100); _logger.LogInformation("Emergency flatten completed"); return true; } public RiskStatus GetRiskStatus() { lock (_lock) { var alerts = new List(); if (_tradingHalted) alerts.Add("Trading halted"); if (_dailyPnL <= -500) // Half of typical daily limit alerts.Add(String.Format("Significant daily loss: {0:C}", _dailyPnL)); if (_maxDrawdown <= -1000) alerts.Add(String.Format("Large drawdown: {0:C}", _maxDrawdown)); return new RiskStatus( tradingEnabled: !_tradingHalted, dailyPnL: _dailyPnL, dailyLossLimit: 1000, // Will come from config in Phase 1 maxDrawdown: _maxDrawdown, openPositions: GetOpenPositionCount(), lastUpdate: _lastUpdate, activeAlerts: alerts ); } } /// /// Reset daily state - typically called at start of new trading day /// public void ResetDaily() { lock (_lock) { _dailyPnL = 0; _maxDrawdown = 0; _tradingHalted = false; _symbolExposure.Clear(); _lastUpdate = DateTime.UtcNow; _logger.LogInformation("Daily risk state reset"); } } } }