Files
nt8-sdk/Specs/SDK/risk_management_package.md
Billy Valentine 92f3732b3d
Some checks failed
Build and Test / build (push) Has been cancelled
Phase 0 completion: NT8 SDK core framework with risk management and position sizing
2025-09-09 17:06:37 -04:00

33 KiB

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

using NT8.Core.Common.Models;
using Microsoft.Extensions.Logging;

namespace NT8.Core.Risk;

/// <summary>
/// Basic risk manager implementing Tier 1 risk controls
/// Thread-safe implementation using locks for state consistency
/// </summary>
public class BasicRiskManager : IRiskManager
{
    private readonly ILogger<BasicRiskManager> _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<string, double> _symbolExposure = new();
    
    public BasicRiskManager(ILogger<BasicRiskManager> 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<bool> 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<string>();
            
            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
            );
        }
    }
    
    /// <summary>
    /// Reset daily state - typically called at start of new trading day
    /// </summary>
    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

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<BasicRiskManager> _logger;
    private readonly BasicRiskManager _riskManager;
    
    public BasicRiskManagerTests()
    {
        _logger = NullLogger<BasicRiskManager>.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<ArgumentException>(() => _riskManager.EmergencyFlatten(null));
        await Assert.ThrowsAsync<ArgumentException>(() => _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<ArgumentNullException>(() => _riskManager.ValidateOrder(null, context, config));
        Assert.Throws<ArgumentNullException>(() => _riskManager.ValidateOrder(intent, null, config));
        Assert.Throws<ArgumentNullException>(() => _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<Task>();
        
        // 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

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;

/// <summary>
/// Comprehensive risk scenario testing
/// These tests validate the risk manager against real-world trading scenarios
/// </summary>
public class RiskScenarioTests
{
    private readonly BasicRiskManager _riskManager;
    
    public RiskScenarioTests()
    {
        _riskManager = new BasicRiskManager(NullLogger<BasicRiskManager>.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

# 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.