# **Risk Management Package** ## **IMPLEMENTATION INSTRUCTIONS** Implement the BasicRiskManager exactly as specified. This is the most critical component - all trades must pass through risk validation. ### **File 1: `src/NT8.Core/Risk/BasicRiskManager.cs`** ```csharp using NT8.Core.Common.Models; using Microsoft.Extensions.Logging; 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(); // Risk state - protected by _lock private double _dailyPnL; private double _maxDrawdown; private bool _tradingHalted; private DateTime _lastUpdate = DateTime.UtcNow; private readonly Dictionary _symbolExposure = new(); public BasicRiskManager(ILogger logger) { _logger = logger; } public RiskDecision ValidateOrder(StrategyIntent intent, StrategyContext context, RiskConfig config) { if (intent == null) throw new ArgumentNullException(nameof(intent)); if (context == null) throw new ArgumentNullException(nameof(context)); if (config == null) throw new ArgumentNullException(nameof(config)); lock (_lock) { // Check if trading is halted if (_tradingHalted) { _logger.LogWarning("Order rejected - trading halted by risk manager"); return new RiskDecision( Allow: false, RejectReason: "Trading halted by risk manager", ModifiedIntent: null, RiskLevel: RiskLevel.Critical, RiskMetrics: new() { ["halted"] = true, ["daily_pnl"] = _dailyPnL } ); } // Tier 1: Daily loss cap if (_dailyPnL <= -config.DailyLossLimit) { _tradingHalted = true; _logger.LogCritical("Daily loss limit breached: {DailyPnL:C} <= {Limit:C}", _dailyPnL, -config.DailyLossLimit); return new RiskDecision( Allow: false, RejectReason: $"Daily loss limit breached: {_dailyPnL:C}", ModifiedIntent: null, RiskLevel: RiskLevel.Critical, RiskMetrics: new() { ["daily_pnl"] = _dailyPnL, ["limit"] = config.DailyLossLimit } ); } // Tier 1: Per-trade risk limit var tradeRisk = CalculateTradeRisk(intent, context); if (tradeRisk > config.MaxTradeRisk) { _logger.LogWarning("Trade risk too high: {Risk:C} > {Limit:C}", tradeRisk, config.MaxTradeRisk); return new RiskDecision( Allow: false, RejectReason: $"Trade risk too high: {tradeRisk:C}", ModifiedIntent: null, RiskLevel: RiskLevel.High, RiskMetrics: new() { ["trade_risk"] = tradeRisk, ["limit"] = config.MaxTradeRisk } ); } // Tier 1: Position limits var currentPositions = GetOpenPositionCount(); if (currentPositions >= config.MaxOpenPositions && context.CurrentPosition.Quantity == 0) { _logger.LogWarning("Max open positions exceeded: {Current} >= {Limit}", currentPositions, config.MaxOpenPositions); return new RiskDecision( Allow: false, RejectReason: $"Max open positions exceeded: {currentPositions}", ModifiedIntent: null, RiskLevel: RiskLevel.Medium, RiskMetrics: new() { ["open_positions"] = currentPositions, ["limit"] = config.MaxOpenPositions } ); } // All checks passed - determine risk level var riskLevel = DetermineRiskLevel(config); _logger.LogDebug("Order approved: {Symbol} {Side} risk=${Risk:F2} level={Level}", intent.Symbol, intent.Side, tradeRisk, riskLevel); return new RiskDecision( Allow: true, RejectReason: null, ModifiedIntent: null, RiskLevel: riskLevel, RiskMetrics: new() { ["trade_risk"] = tradeRisk, ["daily_pnl"] = _dailyPnL, ["max_drawdown"] = _maxDrawdown, ["open_positions"] = currentPositions } ); } } 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 return symbol switch { "ES" => 12.50, "MES" => 1.25, "NQ" => 5.00, "MNQ" => 0.50, "CL" => 10.00, "GC" => 10.00, _ => 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; return lossPercent switch { >= 0.8 => RiskLevel.High, >= 0.5 => RiskLevel.Medium, _ => RiskLevel.Low }; } public void OnFill(OrderFill fill) { if (fill == null) throw new ArgumentNullException(nameof(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: {Symbol} {Qty} @ {Price:F2}, Exposure: {Exposure: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={DayPnL:C}, Max DD={MaxDD:C}", dayPnL, _maxDrawdown); } // Check for emergency conditions CheckEmergencyConditions(dayPnL); } } private void CheckEmergencyConditions(double dayPnL) { // Emergency halt if daily loss exceeds 90% of limit if (dayPnL <= -(_dailyPnL * 0.9) && !_tradingHalted) { _tradingHalted = true; _logger.LogCritical("Emergency halt triggered at 90% of daily loss limit: {DayPnL:C}", dayPnL); } } public async Task EmergencyFlatten(string reason) { if (string.IsNullOrEmpty(reason)) throw new ArgumentException("Reason required", nameof(reason)); lock (_lock) { _tradingHalted = true; _logger.LogCritical("Emergency flatten triggered: {Reason}", 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($"Significant daily loss: {_dailyPnL:C}"); if (_maxDrawdown <= -1000) alerts.Add($"Large drawdown: {_maxDrawdown:C}"); 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"); } } } ``` ## **COMPREHENSIVE TEST SUITE** ### **File 2: `tests/NT8.Core.Tests/Risk/BasicRiskManagerTests.cs`** ```csharp using NT8.Core.Risk; using NT8.Core.Common.Models; using NT8.Core.Tests.TestHelpers; using Microsoft.Extensions.Logging; using FluentAssertions; using Xunit; using Microsoft.Extensions.Logging.Abstractions; namespace NT8.Core.Tests.Risk; public class BasicRiskManagerTests : IDisposable { private readonly ILogger _logger; private readonly BasicRiskManager _riskManager; public BasicRiskManagerTests() { _logger = NullLogger.Instance; _riskManager = new BasicRiskManager(_logger); } [Fact] public void ValidateOrder_WithinLimits_ShouldAllow() { // Arrange var intent = TestDataBuilder.CreateValidIntent(stopTicks: 8); var context = TestDataBuilder.CreateTestContext(); var config = TestDataBuilder.CreateTestRiskConfig(); // Act var result = _riskManager.ValidateOrder(intent, context, config); // Assert result.Allow.Should().BeTrue(); result.RejectReason.Should().BeNull(); result.RiskLevel.Should().Be(RiskLevel.Low); result.RiskMetrics.Should().ContainKey("trade_risk"); result.RiskMetrics.Should().ContainKey("daily_pnl"); } [Fact] public void ValidateOrder_ExceedsDailyLimit_ShouldReject() { // Arrange var intent = TestDataBuilder.CreateValidIntent(); var context = TestDataBuilder.CreateTestContext(); var config = new RiskConfig( DailyLossLimit: 1000, MaxTradeRisk: 500, MaxOpenPositions: 5, EmergencyFlattenEnabled: true ); // Simulate daily loss exceeding limit _riskManager.OnPnLUpdate(0, -1001); // Act var result = _riskManager.ValidateOrder(intent, context, config); // Assert result.Allow.Should().BeFalse(); result.RejectReason.Should().Contain("Daily loss limit breached"); result.RiskLevel.Should().Be(RiskLevel.Critical); result.RiskMetrics["daily_pnl"].Should().Be(-1001); } [Fact] public void ValidateOrder_ExceedsTradeRisk_ShouldReject() { // Arrange var intent = TestDataBuilder.CreateValidIntent(stopTicks: 100); // High risk trade var context = TestDataBuilder.CreateTestContext(); var config = new RiskConfig( DailyLossLimit: 10000, MaxTradeRisk: 500, // Lower than calculated trade risk MaxOpenPositions: 5, EmergencyFlattenEnabled: true ); // Act var result = _riskManager.ValidateOrder(intent, context, config); // Assert result.Allow.Should().BeFalse(); result.RejectReason.Should().Contain("Trade risk too high"); result.RiskLevel.Should().Be(RiskLevel.High); // Verify risk calculation var expectedRisk = 100 * 12.50; // 100 ticks * ES tick value result.RiskMetrics["trade_risk"].Should().Be(expectedRisk); } [Theory] [InlineData("ES", 8, 100.0)] // ES: 8 ticks * $12.50 = $100 [InlineData("MES", 8, 10.0)] // MES: 8 ticks * $1.25 = $10 [InlineData("NQ", 4, 20.0)] // NQ: 4 ticks * $5.00 = $20 [InlineData("MNQ", 10, 5.0)] // MNQ: 10 ticks * $0.50 = $5 public void ValidateOrder_RiskCalculation_ShouldBeAccurate(string symbol, int stopTicks, double expectedRisk) { // Arrange var intent = TestDataBuilder.CreateValidIntent(symbol: symbol, stopTicks: stopTicks); var context = TestDataBuilder.CreateTestContext(); var config = TestDataBuilder.CreateTestRiskConfig(); // Act var result = _riskManager.ValidateOrder(intent, context, config); // Assert result.Allow.Should().BeTrue(); result.RiskMetrics["trade_risk"].Should().Be(expectedRisk); } [Fact] public void ValidateOrder_MaxPositionsExceeded_ShouldReject() { // Arrange var intent = TestDataBuilder.CreateValidIntent(); var context = TestDataBuilder.CreateTestContext(); var config = new RiskConfig( DailyLossLimit: 10000, MaxTradeRisk: 1000, MaxOpenPositions: 1, // Very low limit EmergencyFlattenEnabled: true ); // Simulate existing position by processing a fill var fill = new OrderFill( OrderId: Guid.NewGuid().ToString(), Symbol: "NQ", Quantity: 2, FillPrice: 15000.0, FillTime: DateTime.UtcNow, Commission: 5.0, ExecutionId: Guid.NewGuid().ToString() ); _riskManager.OnFill(fill); // Act var result = _riskManager.ValidateOrder(intent, context, config); // Assert result.Allow.Should().BeFalse(); result.RejectReason.Should().Contain("Max open positions exceeded"); result.RiskLevel.Should().Be(RiskLevel.Medium); } [Fact] public async Task EmergencyFlatten_ShouldHaltTrading() { // Arrange var reason = "Test emergency halt"; // Act var result = await _riskManager.EmergencyFlatten(reason); var status = _riskManager.GetRiskStatus(); // Assert result.Should().BeTrue(); status.TradingEnabled.Should().BeFalse(); status.ActiveAlerts.Should().Contain("Trading halted"); } [Fact] public async Task EmergencyFlatten_WithNullReason_ShouldThrow() { // Act & Assert await Assert.ThrowsAsync(() => _riskManager.EmergencyFlatten(null)); await Assert.ThrowsAsync(() => _riskManager.EmergencyFlatten("")); } [Fact] public void ValidateOrder_AfterEmergencyFlatten_ShouldRejectAllOrders() { // Arrange var intent = TestDataBuilder.CreateValidIntent(); var context = TestDataBuilder.CreateTestContext(); var config = TestDataBuilder.CreateTestRiskConfig(); // Trigger emergency flatten _riskManager.EmergencyFlatten("Test").Wait(); // Act var result = _riskManager.ValidateOrder(intent, context, config); // Assert result.Allow.Should().BeFalse(); result.RejectReason.Should().Contain("Trading halted"); result.RiskLevel.Should().Be(RiskLevel.Critical); } [Fact] public void OnPnLUpdate_WithLargeDrawdown_ShouldUpdateStatus() { // Arrange var largeLoss = -1500.0; // Act _riskManager.OnPnLUpdate(largeLoss, largeLoss); var status = _riskManager.GetRiskStatus(); // Assert status.DailyPnL.Should().Be(largeLoss); status.MaxDrawdown.Should().Be(largeLoss); status.ActiveAlerts.Should().Contain(alert => alert.Contains("drawdown")); } [Fact] public void OnFill_ShouldUpdateExposure() { // Arrange var fill = new OrderFill( OrderId: Guid.NewGuid().ToString(), Symbol: "ES", Quantity: 2, FillPrice: 4200.0, FillTime: DateTime.UtcNow, Commission: 4.50, ExecutionId: Guid.NewGuid().ToString() ); // Act _riskManager.OnFill(fill); var status = _riskManager.GetRiskStatus(); // Assert status.LastUpdate.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1)); } [Fact] public void ValidateOrder_WithNullParameters_ShouldThrow() { // Arrange var intent = TestDataBuilder.CreateValidIntent(); var context = TestDataBuilder.CreateTestContext(); var config = TestDataBuilder.CreateTestRiskConfig(); // Act & Assert Assert.Throws(() => _riskManager.ValidateOrder(null, context, config)); Assert.Throws(() => _riskManager.ValidateOrder(intent, null, config)); Assert.Throws(() => _riskManager.ValidateOrder(intent, context, null)); } [Fact] public void RiskLevel_ShouldEscalateWithLosses() { // Arrange var intent = TestDataBuilder.CreateValidIntent(); var context = TestDataBuilder.CreateTestContext(); var config = new RiskConfig(1000, 500, 5, true); // $1000 daily limit // Act & Assert - Low risk (no losses) var result1 = _riskManager.ValidateOrder(intent, context, config); result1.RiskLevel.Should().Be(RiskLevel.Low); // Medium risk (50% of daily limit) _riskManager.OnPnLUpdate(-500, -500); var result2 = _riskManager.ValidateOrder(intent, context, config); result2.RiskLevel.Should().Be(RiskLevel.Medium); // High risk (80% of daily limit) _riskManager.OnPnLUpdate(-800, -800); var result3 = _riskManager.ValidateOrder(intent, context, config); result3.RiskLevel.Should().Be(RiskLevel.High); } [Fact] public void ResetDaily_ShouldClearState() { // Arrange - Set up some risk state _riskManager.OnPnLUpdate(-500, -500); var fill = new OrderFill("test", "ES", 2, 4200, DateTime.UtcNow, 4.50, "exec1"); _riskManager.OnFill(fill); // Act _riskManager.ResetDaily(); var status = _riskManager.GetRiskStatus(); // Assert status.DailyPnL.Should().Be(0); status.MaxDrawdown.Should().Be(0); status.TradingEnabled.Should().BeTrue(); status.OpenPositions.Should().Be(0); status.ActiveAlerts.Should().BeEmpty(); } [Fact] public void GetRiskStatus_ShouldReturnCurrentState() { // Arrange var testPnL = -300.0; _riskManager.OnPnLUpdate(testPnL, testPnL); // Act var status = _riskManager.GetRiskStatus(); // Assert status.TradingEnabled.Should().BeTrue(); status.DailyPnL.Should().Be(testPnL); status.MaxDrawdown.Should().Be(testPnL); status.LastUpdate.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1)); status.ActiveAlerts.Should().NotBeNull(); } [Fact] public void ConcurrentAccess_ShouldBeThreadSafe() { // Arrange var intent = TestDataBuilder.CreateValidIntent(); var context = TestDataBuilder.CreateTestContext(); var config = TestDataBuilder.CreateTestRiskConfig(); var tasks = new List(); // Act - Multiple threads accessing simultaneously for (int i = 0; i < 10; i++) { tasks.Add(Task.Run(() => { _riskManager.ValidateOrder(intent, context, config); _riskManager.OnPnLUpdate(-10 * i, -10 * i); })); } Task.WaitAll(tasks.ToArray()); // Assert - Should not throw and should have consistent state var status = _riskManager.GetRiskStatus(); status.Should().NotBeNull(); status.LastUpdate.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1)); } public void Dispose() { // Cleanup if needed } } ``` ## **RISK SCENARIO TEST DATA** ### **File 3: `tests/NT8.Core.Tests/Risk/RiskScenarioTests.cs`** ```csharp using NT8.Core.Risk; using NT8.Core.Common.Models; using NT8.Core.Tests.TestHelpers; using Microsoft.Extensions.Logging.Abstractions; using FluentAssertions; using Xunit; namespace NT8.Core.Tests.Risk; /// /// Comprehensive risk scenario testing /// These tests validate the risk manager against real-world trading scenarios /// public class RiskScenarioTests { private readonly BasicRiskManager _riskManager; public RiskScenarioTests() { _riskManager = new BasicRiskManager(NullLogger.Instance); } [Fact] public void Scenario_TypicalTradingDay_ShouldManageRiskCorrectly() { // Arrange - Typical day configuration var config = new RiskConfig( DailyLossLimit: 1000, MaxTradeRisk: 200, MaxOpenPositions: 3, EmergencyFlattenEnabled: true ); var context = TestDataBuilder.CreateTestContext(); // Act & Assert - Morning trades should be allowed var morningTrade1 = TestDataBuilder.CreateValidIntent(stopTicks: 8); // $100 risk var result1 = _riskManager.ValidateOrder(morningTrade1, context, config); result1.Allow.Should().BeTrue(); result1.RiskLevel.Should().Be(RiskLevel.Low); // Simulate some losses _riskManager.OnPnLUpdate(-150, -150); var morningTrade2 = TestDataBuilder.CreateValidIntent(stopTicks: 12); // $150 risk var result2 = _riskManager.ValidateOrder(morningTrade2, context, config); result2.Allow.Should().BeTrue(); result2.RiskLevel.Should().Be(RiskLevel.Low); // More losses - should escalate risk level _riskManager.OnPnLUpdate(-600, -600); var afternoonTrade = TestDataBuilder.CreateValidIntent(stopTicks: 8); var result3 = _riskManager.ValidateOrder(afternoonTrade, context, config); result3.Allow.Should().BeTrue(); result3.RiskLevel.Should().Be(RiskLevel.Medium); // Should escalate // Near daily limit - high risk _riskManager.OnPnLUpdate(-850, -850); var lateTrade = TestDataBuilder.CreateValidIntent(stopTicks: 8); var result4 = _riskManager.ValidateOrder(lateTrade, context, config); result4.Allow.Should().BeTrue(); result4.RiskLevel.Should().Be(RiskLevel.High); // Exceed daily limit - should halt _riskManager.OnPnLUpdate(-1050, -1050); var deniedTrade = TestDataBuilder.CreateValidIntent(stopTicks: 4); var result5 = _riskManager.ValidateOrder(deniedTrade, context, config); result5.Allow.Should().BeFalse(); result5.RiskLevel.Should().Be(RiskLevel.Critical); } [Fact] public void Scenario_HighRiskTrade_ShouldBeRejected() { // Arrange - Conservative risk settings var config = new RiskConfig( DailyLossLimit: 2000, MaxTradeRisk: 100, // Very conservative MaxOpenPositions: 5, EmergencyFlattenEnabled: true ); var context = TestDataBuilder.CreateTestContext(); // Act - Try to place high-risk trade var highRiskTrade = TestDataBuilder.CreateValidIntent( symbol: "ES", stopTicks: 20 // $250 risk, exceeds $100 limit ); var result = _riskManager.ValidateOrder(highRiskTrade, context, config); // Assert result.Allow.Should().BeFalse(); result.RejectReason.Should().Contain("Trade risk too high"); result.RiskMetrics["trade_risk"].Should().Be(250.0); // 20 * $12.50 result.RiskMetrics["limit"].Should().Be(100.0); } [Fact] public void Scenario_MaxPositions_ShouldLimitNewTrades() { // Arrange - Low position limit var config = new RiskConfig( DailyLossLimit: 5000, MaxTradeRisk: 500, MaxOpenPositions: 2, EmergencyFlattenEnabled: true ); var context = TestDataBuilder.CreateTestContext(); // Fill up position slots var fill1 = new OrderFill("order1", "ES", 1, 4200, DateTime.UtcNow, 2.25, "exec1"); var fill2 = new OrderFill("order2", "NQ", 1, 15000, DateTime.UtcNow, 2.50, "exec2"); _riskManager.OnFill(fill1); _riskManager.OnFill(fill2); // Act - Try to add another position var newTrade = TestDataBuilder.CreateValidIntent(symbol: "CL"); var result = _riskManager.ValidateOrder(newTrade, context, config); // Assert result.Allow.Should().BeFalse(); result.RejectReason.Should().Contain("Max open positions exceeded"); result.RiskLevel.Should().Be(RiskLevel.Medium); } [Fact] public void Scenario_RecoveryAfterReset_ShouldAllowTrading() { // Arrange - Simulate end of bad trading day var config = TestDataBuilder.CreateTestRiskConfig(); var context = TestDataBuilder.CreateTestContext(); // Simulate terrible day with emergency halt _riskManager.OnPnLUpdate(-1500, -1500); _riskManager.EmergencyFlatten("End of day").Wait(); var haltedTrade = TestDataBuilder.CreateValidIntent(); var haltedResult = _riskManager.ValidateOrder(haltedTrade, context, config); haltedResult.Allow.Should().BeFalse(); // Act - Reset for new day _riskManager.ResetDaily(); var newDayTrade = TestDataBuilder.CreateValidIntent(); var newResult = _riskManager.ValidateOrder(newDayTrade, context, config); // Assert - Should be back to normal newResult.Allow.Should().BeTrue(); newResult.RiskLevel.Should().Be(RiskLevel.Low); var status = _riskManager.GetRiskStatus(); status.TradingEnabled.Should().BeTrue(); status.DailyPnL.Should().Be(0); status.ActiveAlerts.Should().BeEmpty(); } [Fact] public void Scenario_VolatileMarket_ShouldHandleMultipleSymbols() { // Arrange - Multi-symbol trading var config = new RiskConfig( DailyLossLimit: 2000, MaxTradeRisk: 300, MaxOpenPositions: 4, EmergencyFlattenEnabled: true ); var context = TestDataBuilder.CreateTestContext(); // Act - Trade multiple symbols var esTrade = TestDataBuilder.CreateValidIntent(symbol: "ES", stopTicks: 16); // $200 risk var nqTrade = TestDataBuilder.CreateValidIntent(symbol: "NQ", stopTicks: 40); // $200 risk var clTrade = TestDataBuilder.CreateValidIntent(symbol: "CL", stopTicks: 20); // $200 risk var esResult = _riskManager.ValidateOrder(esTrade, context, config); var nqResult = _riskManager.ValidateOrder(nqTrade, context, config); var clResult = _riskManager.ValidateOrder(clTrade, context, config); // Assert - All should be allowed esResult.Allow.Should().BeTrue(); nqResult.Allow.Should().BeTrue(); clResult.Allow.Should().BeTrue(); // Verify risk calculations are symbol-specific esResult.RiskMetrics["trade_risk"].Should().Be(200.0); // ES: 16 * $12.50 nqResult.RiskMetrics["trade_risk"].Should().Be(200.0); // NQ: 40 * $5.00 clResult.RiskMetrics["trade_risk"].Should().Be(200.0); // CL: 20 * $10.00 } [Fact] public void Scenario_GradualLossEscalation_ShouldShowRiskProgression() { // Arrange var config = new RiskConfig( DailyLossLimit: 1000, MaxTradeRisk: 200, MaxOpenPositions: 5, EmergencyFlattenEnabled: true ); var context = TestDataBuilder.CreateTestContext(); var intent = TestDataBuilder.CreateValidIntent(stopTicks: 8); // Act & Assert - Track risk level escalation var results = new List<(double loss, RiskLevel level)>(); // Start: No losses var result0 = _riskManager.ValidateOrder(intent, context, config); results.Add((0, result0.RiskLevel)); // 30% loss _riskManager.OnPnLUpdate(-300, -300); var result1 = _riskManager.ValidateOrder(intent, context, config); results.Add((-300, result1.RiskLevel)); // 50% loss _riskManager.OnPnLUpdate(-500, -500); var result2 = _riskManager.ValidateOrder(intent, context, config); results.Add((-500, result2.RiskLevel)); // 80% loss _riskManager.OnPnLUpdate(-800, -800); var result3 = _riskManager.ValidateOrder(intent, context, config); results.Add((-800, result3.RiskLevel)); // Assert escalation pattern results[0].level.Should().Be(RiskLevel.Low); // 0% loss results[1].level.Should().Be(RiskLevel.Low); // 30% loss results[2].level.Should().Be(RiskLevel.Medium); // 50% loss results[3].level.Should().Be(RiskLevel.High); // 80% loss } } ``` ## **VALIDATION SCRIPT** ### **File 4: `tools/validate-risk-implementation.ps1`** ```powershell # Risk Management Validation Script Write-Host "๐Ÿ” Validating Risk Management Implementation..." -ForegroundColor Yellow # Build check Write-Host "๐Ÿ“ฆ Building solution..." -ForegroundColor Blue $buildResult = dotnet build --configuration Release --verbosity quiet if ($LASTEXITCODE -ne 0) { Write-Host "โŒ Build failed" -ForegroundColor Red exit 1 } # Test execution Write-Host "๐Ÿงช Running risk management tests..." -ForegroundColor Blue $testResult = dotnet test tests/NT8.Core.Tests/NT8.Core.Tests.csproj --filter "Category=Risk|FullyQualifiedName~Risk" --configuration Release --verbosity quiet if ($LASTEXITCODE -ne 0) { Write-Host "โŒ Risk tests failed" -ForegroundColor Red exit 1 } # Specific validation scenarios Write-Host "โœ… Running validation scenarios..." -ForegroundColor Blue $validationTests = @( "BasicRiskManagerTests.ValidateOrder_WithinLimits_ShouldAllow", "BasicRiskManagerTests.ValidateOrder_ExceedsDailyLimit_ShouldReject", "BasicRiskManagerTests.ValidateOrder_ExceedsTradeRisk_ShouldReject", "RiskScenarioTests.Scenario_TypicalTradingDay_ShouldManageRiskCorrectly" ) foreach ($test in $validationTests) { $result = dotnet test --filter "FullyQualifiedName~$test" --configuration Release --verbosity quiet if ($LASTEXITCODE -eq 0) { Write-Host " โœ… $test" -ForegroundColor Green } else { Write-Host " โŒ $test" -ForegroundColor Red exit 1 } } Write-Host "๐ŸŽ‰ Risk management validation completed successfully!" -ForegroundColor Green ``` ## **SUCCESS CRITERIA** โœ… **BasicRiskManager.cs implemented exactly as specified** โœ… **All Tier 1 risk controls working (daily limits, trade limits, position limits)** โœ… **Thread-safe implementation using locks** โœ… **Emergency flatten functionality** โœ… **Comprehensive test suite with >90% coverage** โœ… **All validation scenarios pass** โœ… **Risk level escalation working correctly** โœ… **Multi-symbol risk calculations accurate** ## **CRITICAL REQUIREMENTS** 1. **Thread Safety**: All methods must be thread-safe using lock(_lock) 2. **Exact Risk Calculations**: Must match the specified tick values per symbol 3. **State Management**: Daily P&L and position tracking must be accurate 4. **Error Handling**: All null parameters must throw ArgumentNullException 5. **Logging**: All significant events must be logged at appropriate levels **Once this is complete, risk management is fully functional and ready for integration with position sizing.**