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