Phase 0 completion: NT8 SDK core framework with risk management and position sizing
Some checks failed
Build and Test / build (push) Has been cancelled

This commit is contained in:
Billy Valentine
2025-09-09 17:06:37 -04:00
parent 97e5050d3e
commit 92f3732b3d
109 changed files with 38593 additions and 380 deletions

View File

@@ -0,0 +1,291 @@
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
{
/// <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 _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<string, double> _symbolExposure = new Dictionary<string, double>();
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<string, object>();
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<string, object>();
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<string, object>();
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<string, object>();
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<string, object>();
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<bool> 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<string>();
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
);
}
}
/// <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");
}
}
}
}