S2-B3/S3-05: PortfolioRiskManager, connection loss recovery, long-only lock
Some checks failed
Build and Test / build (push) Has been cancelled
Some checks failed
Build and Test / build (push) Has been cancelled
This commit is contained in:
@@ -53,6 +53,7 @@ namespace NinjaTrader.NinjaScript.Strategies
|
|||||||
private int _ordersSubmittedToday;
|
private int _ordersSubmittedToday;
|
||||||
private DateTime _lastBarTime;
|
private DateTime _lastBarTime;
|
||||||
private bool _killSwitchTriggered;
|
private bool _killSwitchTriggered;
|
||||||
|
private bool _connectionLost;
|
||||||
private ExecutionCircuitBreaker _circuitBreaker;
|
private ExecutionCircuitBreaker _circuitBreaker;
|
||||||
private System.IO.StreamWriter _fileLog;
|
private System.IO.StreamWriter _fileLog;
|
||||||
private readonly object _fileLock = new object();
|
private readonly object _fileLock = new object();
|
||||||
@@ -194,6 +195,7 @@ namespace NinjaTrader.NinjaScript.Strategies
|
|||||||
EnableLongTrades = true;
|
EnableLongTrades = true;
|
||||||
EnableShortTrades = true;
|
EnableShortTrades = true;
|
||||||
_killSwitchTriggered = false;
|
_killSwitchTriggered = false;
|
||||||
|
_connectionLost = false;
|
||||||
}
|
}
|
||||||
else if (State == State.DataLoaded)
|
else if (State == State.DataLoaded)
|
||||||
{
|
{
|
||||||
@@ -220,6 +222,7 @@ namespace NinjaTrader.NinjaScript.Strategies
|
|||||||
}
|
}
|
||||||
else if (State == State.Terminated)
|
else if (State == State.Terminated)
|
||||||
{
|
{
|
||||||
|
PortfolioRiskManager.Instance.UnregisterStrategy(Name);
|
||||||
WriteSessionFooter();
|
WriteSessionFooter();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -261,6 +264,14 @@ namespace NinjaTrader.NinjaScript.Strategies
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Connection loss guard — do not submit new orders if broker is disconnected
|
||||||
|
if (_connectionLost)
|
||||||
|
{
|
||||||
|
if (EnableVerboseLogging)
|
||||||
|
Print(string.Format("[NT8-SDK] Bar skipped — connection lost: {0}", Time[0]));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Log first processable bar and every 100th bar.
|
// Log first processable bar and every 100th bar.
|
||||||
if (CurrentBar == BarsRequiredToTrade || CurrentBar % 100 == 0)
|
if (CurrentBar == BarsRequiredToTrade || CurrentBar % 100 == 0)
|
||||||
{
|
{
|
||||||
@@ -357,9 +368,54 @@ namespace NinjaTrader.NinjaScript.Strategies
|
|||||||
execution.Price,
|
execution.Price,
|
||||||
execution.OrderId));
|
execution.OrderId));
|
||||||
|
|
||||||
|
var fill = new NT8.Core.Common.Models.OrderFill(
|
||||||
|
orderId,
|
||||||
|
execution.Order != null ? execution.Order.Instrument.MasterInstrument.Name : string.Empty,
|
||||||
|
execution.Quantity,
|
||||||
|
execution.Price,
|
||||||
|
time,
|
||||||
|
0.0,
|
||||||
|
executionId);
|
||||||
|
PortfolioRiskManager.Instance.ReportFill(Name, fill);
|
||||||
|
|
||||||
_executionAdapter.ProcessExecution(orderId, executionId, price, quantity, time);
|
_executionAdapter.ProcessExecution(orderId, executionId, price, quantity, time);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles broker connection status changes. Halts new orders on disconnect,
|
||||||
|
/// logs reconnect, and resets the connection flag when restored.
|
||||||
|
/// </summary>
|
||||||
|
protected override void OnConnectionStatusUpdate(
|
||||||
|
Connection connection,
|
||||||
|
ConnectionStatus status,
|
||||||
|
DateTime time)
|
||||||
|
{
|
||||||
|
if (connection == null) return;
|
||||||
|
|
||||||
|
if (status == ConnectionStatus.Connected)
|
||||||
|
{
|
||||||
|
if (_connectionLost)
|
||||||
|
{
|
||||||
|
_connectionLost = false;
|
||||||
|
Print(string.Format("[NT8-SDK] Connection RESTORED at {0} — trading resumed.",
|
||||||
|
time.ToString("HH:mm:ss")));
|
||||||
|
FileLog(string.Format("CONNECTION RESTORED at {0}", time.ToString("HH:mm:ss")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (status == ConnectionStatus.Disconnected ||
|
||||||
|
status == ConnectionStatus.ConnectionLost)
|
||||||
|
{
|
||||||
|
if (!_connectionLost)
|
||||||
|
{
|
||||||
|
_connectionLost = true;
|
||||||
|
Print(string.Format("[NT8-SDK] Connection LOST at {0} — halting new orders. Status={1}",
|
||||||
|
time.ToString("HH:mm:ss"),
|
||||||
|
status));
|
||||||
|
FileLog(string.Format("CONNECTION LOST at {0} Status={1}", time.ToString("HH:mm:ss"), status));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void InitFileLog()
|
private void InitFileLog()
|
||||||
{
|
{
|
||||||
if (!EnableFileLogging)
|
if (!EnableFileLogging)
|
||||||
@@ -418,6 +474,7 @@ namespace NinjaTrader.NinjaScript.Strategies
|
|||||||
RiskPerTrade));
|
RiskPerTrade));
|
||||||
FileLog(string.Format("Sizing : MinContracts={0} MaxContracts={1}", MinContracts, MaxContracts));
|
FileLog(string.Format("Sizing : MinContracts={0} MaxContracts={1}", MinContracts, MaxContracts));
|
||||||
FileLog(string.Format("VerboseLog : {0} FileLog: {1}", EnableVerboseLogging, EnableFileLogging));
|
FileLog(string.Format("VerboseLog : {0} FileLog: {1}", EnableVerboseLogging, EnableFileLogging));
|
||||||
|
FileLog(string.Format("ConnectionLost : {0}", _connectionLost));
|
||||||
FileLog("---");
|
FileLog("---");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,6 +538,8 @@ namespace NinjaTrader.NinjaScript.Strategies
|
|||||||
|
|
||||||
_sdkStrategy.Initialize(_strategyConfig, null, _logger);
|
_sdkStrategy.Initialize(_strategyConfig, null, _logger);
|
||||||
ConfigureStrategyParameters();
|
ConfigureStrategyParameters();
|
||||||
|
PortfolioRiskManager.Instance.RegisterStrategy(Name, _riskConfig);
|
||||||
|
Print(string.Format("[NT8-SDK] Registered with PortfolioRiskManager: {0}", PortfolioRiskManager.Instance.GetStatusSnapshot()));
|
||||||
|
|
||||||
_ordersSubmittedToday = 0;
|
_ordersSubmittedToday = 0;
|
||||||
_lastBarTime = DateTime.MinValue;
|
_lastBarTime = DateTime.MinValue;
|
||||||
@@ -590,6 +649,16 @@ namespace NinjaTrader.NinjaScript.Strategies
|
|||||||
|
|
||||||
private void ProcessStrategyIntent(StrategyIntent intent, StrategyContext context)
|
private void ProcessStrategyIntent(StrategyIntent intent, StrategyContext context)
|
||||||
{
|
{
|
||||||
|
// Portfolio-level risk check — runs before per-strategy risk validation
|
||||||
|
var portfolioDecision = PortfolioRiskManager.Instance.ValidatePortfolioRisk(Name, intent);
|
||||||
|
if (!portfolioDecision.Allow)
|
||||||
|
{
|
||||||
|
Print(string.Format("[SDK] Portfolio blocked: {0}", portfolioDecision.RejectReason));
|
||||||
|
if (_logger != null)
|
||||||
|
_logger.LogWarning("Portfolio risk blocked order: {0}", portfolioDecision.RejectReason);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Direction filter — checked before risk to avoid unnecessary processing
|
// Direction filter — checked before risk to avoid unnecessary processing
|
||||||
if (intent.Side == SdkOrderSide.Buy && !EnableLongTrades)
|
if (intent.Side == SdkOrderSide.Buy && !EnableLongTrades)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ namespace NinjaTrader.NinjaScript.Strategies
|
|||||||
Calculate = Calculate.OnBarClose;
|
Calculate = Calculate.OnBarClose;
|
||||||
BarsRequiredToTrade = 50;
|
BarsRequiredToTrade = 50;
|
||||||
EnableLongTrades = true;
|
EnableLongTrades = true;
|
||||||
|
// Long-only: short trades permanently disabled pending backtest confirmation
|
||||||
EnableShortTrades = false;
|
EnableShortTrades = false;
|
||||||
}
|
}
|
||||||
else if (State == State.Configure)
|
else if (State == State.Configure)
|
||||||
|
|||||||
265
src/NT8.Core/Risk/PortfolioRiskManager.cs
Normal file
265
src/NT8.Core/Risk/PortfolioRiskManager.cs
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
// File: PortfolioRiskManager.cs
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using NT8.Core.Common.Models;
|
||||||
|
using NT8.Core.Logging;
|
||||||
|
|
||||||
|
namespace NT8.Core.Risk
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Portfolio-level risk coordinator. Singleton. Enforces cross-strategy
|
||||||
|
/// daily loss limits, maximum open contract caps, and a portfolio kill switch.
|
||||||
|
/// Must be registered by each strategy on init and unregistered on terminate.
|
||||||
|
/// Thread-safe via a single lock object.
|
||||||
|
/// </summary>
|
||||||
|
public class PortfolioRiskManager
|
||||||
|
{
|
||||||
|
private static readonly object _instanceLock = new object();
|
||||||
|
private static PortfolioRiskManager _instance;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the singleton instance of PortfolioRiskManager.
|
||||||
|
/// </summary>
|
||||||
|
public static PortfolioRiskManager Instance
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_instance == null)
|
||||||
|
{
|
||||||
|
lock (_instanceLock)
|
||||||
|
{
|
||||||
|
if (_instance == null)
|
||||||
|
_instance = new PortfolioRiskManager();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly object _lock = new object();
|
||||||
|
private readonly Dictionary<string, RiskConfig> _registeredStrategies;
|
||||||
|
private readonly Dictionary<string, double> _strategyPnL;
|
||||||
|
private readonly Dictionary<string, int> _strategyOpenContracts;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum combined daily loss across all registered strategies before all trading halts.
|
||||||
|
/// Default: 2000.0
|
||||||
|
/// </summary>
|
||||||
|
public double PortfolioDailyLossLimit { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum total open contracts across all registered strategies simultaneously.
|
||||||
|
/// Default: 6
|
||||||
|
/// </summary>
|
||||||
|
public int MaxTotalOpenContracts { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When true, all new orders across all strategies are blocked immediately.
|
||||||
|
/// Set to true to perform an emergency halt of the entire portfolio.
|
||||||
|
/// </summary>
|
||||||
|
public bool PortfolioKillSwitch { get; set; }
|
||||||
|
|
||||||
|
private PortfolioRiskManager()
|
||||||
|
{
|
||||||
|
_registeredStrategies = new Dictionary<string, RiskConfig>();
|
||||||
|
_strategyPnL = new Dictionary<string, double>();
|
||||||
|
_strategyOpenContracts = new Dictionary<string, int>();
|
||||||
|
PortfolioDailyLossLimit = 2000.0;
|
||||||
|
MaxTotalOpenContracts = 6;
|
||||||
|
PortfolioKillSwitch = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a strategy with the portfolio manager. Called from
|
||||||
|
/// NT8StrategyBase.InitializeSdkComponents() during State.DataLoaded.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="strategyId">Unique strategy identifier (use Name from NT8StrategyBase).</param>
|
||||||
|
/// <param name="config">The strategy's risk configuration.</param>
|
||||||
|
/// <exception cref="ArgumentNullException">strategyId or config is null.</exception>
|
||||||
|
public void RegisterStrategy(string strategyId, RiskConfig config)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(strategyId)) throw new ArgumentNullException("strategyId");
|
||||||
|
if (config == null) throw new ArgumentNullException("config");
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_registeredStrategies[strategyId] = config;
|
||||||
|
if (!_strategyPnL.ContainsKey(strategyId))
|
||||||
|
_strategyPnL[strategyId] = 0.0;
|
||||||
|
if (!_strategyOpenContracts.ContainsKey(strategyId))
|
||||||
|
_strategyOpenContracts[strategyId] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unregisters a strategy. Called from NT8StrategyBase during State.Terminated.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="strategyId">Strategy identifier to unregister.</param>
|
||||||
|
public void UnregisterStrategy(string strategyId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(strategyId)) return;
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_registeredStrategies.Remove(strategyId);
|
||||||
|
_strategyPnL.Remove(strategyId);
|
||||||
|
_strategyOpenContracts.Remove(strategyId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates a new order intent against portfolio-level risk limits.
|
||||||
|
/// Called before per-strategy risk validation in ProcessStrategyIntent().
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="strategyId">The strategy requesting the order.</param>
|
||||||
|
/// <param name="intent">The trade intent to validate.</param>
|
||||||
|
/// <returns>RiskDecision indicating whether the order is allowed.</returns>
|
||||||
|
public RiskDecision ValidatePortfolioRisk(string strategyId, StrategyIntent intent)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(strategyId)) throw new ArgumentNullException("strategyId");
|
||||||
|
if (intent == null) throw new ArgumentNullException("intent");
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
// Kill switch — blocks everything immediately
|
||||||
|
if (PortfolioKillSwitch)
|
||||||
|
{
|
||||||
|
var ksMetrics = new Dictionary<string, object>();
|
||||||
|
ksMetrics.Add("kill_switch", true);
|
||||||
|
return new RiskDecision(
|
||||||
|
allow: false,
|
||||||
|
rejectReason: "Portfolio kill switch is active — all trading halted",
|
||||||
|
modifiedIntent: null,
|
||||||
|
riskLevel: RiskLevel.Critical,
|
||||||
|
riskMetrics: ksMetrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Portfolio daily loss limit
|
||||||
|
double totalPnL = 0.0;
|
||||||
|
foreach (var kvp in _strategyPnL)
|
||||||
|
totalPnL += kvp.Value;
|
||||||
|
|
||||||
|
if (totalPnL <= -PortfolioDailyLossLimit)
|
||||||
|
{
|
||||||
|
var pnlMetrics = new Dictionary<string, object>();
|
||||||
|
pnlMetrics.Add("portfolio_pnl", totalPnL);
|
||||||
|
pnlMetrics.Add("limit", PortfolioDailyLossLimit);
|
||||||
|
return new RiskDecision(
|
||||||
|
allow: false,
|
||||||
|
rejectReason: String.Format(
|
||||||
|
"Portfolio daily loss limit breached: {0:C} <= -{1:C}",
|
||||||
|
totalPnL, PortfolioDailyLossLimit),
|
||||||
|
modifiedIntent: null,
|
||||||
|
riskLevel: RiskLevel.Critical,
|
||||||
|
riskMetrics: pnlMetrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Total open contract cap
|
||||||
|
int totalContracts = 0;
|
||||||
|
foreach (var kvp in _strategyOpenContracts)
|
||||||
|
totalContracts += kvp.Value;
|
||||||
|
|
||||||
|
if (totalContracts >= MaxTotalOpenContracts)
|
||||||
|
{
|
||||||
|
var contractMetrics = new Dictionary<string, object>();
|
||||||
|
contractMetrics.Add("total_contracts", totalContracts);
|
||||||
|
contractMetrics.Add("limit", MaxTotalOpenContracts);
|
||||||
|
return new RiskDecision(
|
||||||
|
allow: false,
|
||||||
|
rejectReason: String.Format(
|
||||||
|
"Portfolio contract cap reached: {0} >= {1}",
|
||||||
|
totalContracts, MaxTotalOpenContracts),
|
||||||
|
modifiedIntent: null,
|
||||||
|
riskLevel: RiskLevel.High,
|
||||||
|
riskMetrics: contractMetrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
// All portfolio checks passed
|
||||||
|
var okMetrics = new Dictionary<string, object>();
|
||||||
|
okMetrics.Add("portfolio_pnl", totalPnL);
|
||||||
|
okMetrics.Add("total_contracts", totalContracts);
|
||||||
|
return new RiskDecision(
|
||||||
|
allow: true,
|
||||||
|
rejectReason: null,
|
||||||
|
modifiedIntent: null,
|
||||||
|
riskLevel: RiskLevel.Low,
|
||||||
|
riskMetrics: okMetrics);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reports a fill to the portfolio manager. Updates open contract count for the strategy.
|
||||||
|
/// Called from NT8StrategyBase.OnExecutionUpdate() after each fill.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="strategyId">Strategy that received the fill.</param>
|
||||||
|
/// <param name="fill">Fill details.</param>
|
||||||
|
public void ReportFill(string strategyId, OrderFill fill)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(strategyId) || fill == null) return;
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (!_strategyOpenContracts.ContainsKey(strategyId))
|
||||||
|
_strategyOpenContracts[strategyId] = 0;
|
||||||
|
|
||||||
|
_strategyOpenContracts[strategyId] += fill.Quantity;
|
||||||
|
if (_strategyOpenContracts[strategyId] < 0)
|
||||||
|
_strategyOpenContracts[strategyId] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reports a P&L update for a strategy. Called from NT8StrategyBase
|
||||||
|
/// whenever the strategy's realized P&L changes (typically on position close).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="strategyId">Strategy reporting P&L.</param>
|
||||||
|
/// <param name="pnl">Current cumulative day P&L for this strategy.</param>
|
||||||
|
public void ReportPnL(string strategyId, double pnl)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(strategyId)) return;
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_strategyPnL[strategyId] = pnl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resets daily P&L accumulators for all strategies. Does not clear registrations
|
||||||
|
/// or open contract counts. Typically called at the start of a new trading day.
|
||||||
|
/// </summary>
|
||||||
|
public void ResetDaily()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
var keys = new List<string>(_strategyPnL.Keys);
|
||||||
|
foreach (var key in keys)
|
||||||
|
_strategyPnL[key] = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a snapshot of current portfolio state for diagnostics.
|
||||||
|
/// </summary>
|
||||||
|
public string GetStatusSnapshot()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
double totalPnL = 0.0;
|
||||||
|
foreach (var kvp in _strategyPnL)
|
||||||
|
totalPnL += kvp.Value;
|
||||||
|
|
||||||
|
int totalContracts = 0;
|
||||||
|
foreach (var kvp in _strategyOpenContracts)
|
||||||
|
totalContracts += kvp.Value;
|
||||||
|
|
||||||
|
return String.Format(
|
||||||
|
"Portfolio: strategies={0} totalPnL={1:C} totalContracts={2} killSwitch={3}",
|
||||||
|
_registeredStrategies.Count,
|
||||||
|
totalPnL,
|
||||||
|
totalContracts,
|
||||||
|
PortfolioKillSwitch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
117
tests/NT8.Core.Tests/Risk/PortfolioRiskManagerTests.cs
Normal file
117
tests/NT8.Core.Tests/Risk/PortfolioRiskManagerTests.cs
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
using NT8.Core.Common.Models;
|
||||||
|
using NT8.Core.Risk;
|
||||||
|
|
||||||
|
namespace NT8.Core.Tests.Risk
|
||||||
|
{
|
||||||
|
[TestClass]
|
||||||
|
public class PortfolioRiskManagerTests
|
||||||
|
{
|
||||||
|
private PortfolioRiskManager _manager;
|
||||||
|
|
||||||
|
[TestInitialize]
|
||||||
|
public void TestInitialize()
|
||||||
|
{
|
||||||
|
_manager = PortfolioRiskManager.Instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCleanup]
|
||||||
|
public void TestCleanup()
|
||||||
|
{
|
||||||
|
_manager.UnregisterStrategy("strat1");
|
||||||
|
_manager.UnregisterStrategy("strat2");
|
||||||
|
_manager.UnregisterStrategy("strat3");
|
||||||
|
_manager.UnregisterStrategy("strat4");
|
||||||
|
_manager.UnregisterStrategy("strat5");
|
||||||
|
_manager.PortfolioKillSwitch = false;
|
||||||
|
_manager.PortfolioDailyLossLimit = 2000.0;
|
||||||
|
_manager.MaxTotalOpenContracts = 6;
|
||||||
|
_manager.ResetDaily();
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void PortfolioDailyLossLimit_WhenBreached_BlocksNewOrder()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_manager.RegisterStrategy("strat1", TestDataBuilder.CreateTestRiskConfig());
|
||||||
|
_manager.PortfolioDailyLossLimit = 500;
|
||||||
|
_manager.ReportPnL("strat1", -501);
|
||||||
|
var intent = TestDataBuilder.CreateValidIntent();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var decision = _manager.ValidatePortfolioRisk("strat1", intent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsFalse(decision.Allow);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void MaxTotalOpenContracts_WhenAtCap_BlocksNewOrder()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_manager.RegisterStrategy("strat1", TestDataBuilder.CreateTestRiskConfig());
|
||||||
|
_manager.MaxTotalOpenContracts = 2;
|
||||||
|
|
||||||
|
var fill1 = new OrderFill("ord1", "ES", 1, 5000.0, System.DateTime.UtcNow, 0.0, "exec1");
|
||||||
|
var fill2 = new OrderFill("ord2", "ES", 1, 5001.0, System.DateTime.UtcNow, 0.0, "exec2");
|
||||||
|
_manager.ReportFill("strat1", fill1);
|
||||||
|
_manager.ReportFill("strat1", fill2);
|
||||||
|
var intent = TestDataBuilder.CreateValidIntent();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var decision = _manager.ValidatePortfolioRisk("strat1", intent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsFalse(decision.Allow);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void PortfolioKillSwitch_WhenTrue_BlocksAllOrders()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_manager.RegisterStrategy("strat1", TestDataBuilder.CreateTestRiskConfig());
|
||||||
|
_manager.PortfolioKillSwitch = true;
|
||||||
|
var intent = TestDataBuilder.CreateValidIntent();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var decision = _manager.ValidatePortfolioRisk("strat1", intent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsFalse(decision.Allow);
|
||||||
|
Assert.IsTrue(decision.RejectReason.ToLowerInvariant().Contains("kill switch"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ValidatePortfolioRisk_WhenWithinLimits_Passes()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_manager.RegisterStrategy("strat1", TestDataBuilder.CreateTestRiskConfig());
|
||||||
|
var intent = TestDataBuilder.CreateValidIntent();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var decision = _manager.ValidatePortfolioRisk("strat1", intent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsTrue(decision.Allow);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ResetDaily_ClearsPnL_UnblocksTrading()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_manager.RegisterStrategy("strat1", TestDataBuilder.CreateTestRiskConfig());
|
||||||
|
_manager.PortfolioDailyLossLimit = 500;
|
||||||
|
_manager.ReportPnL("strat1", -600);
|
||||||
|
var intent = TestDataBuilder.CreateValidIntent();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var blocked = _manager.ValidatePortfolioRisk("strat1", intent);
|
||||||
|
_manager.ResetDaily();
|
||||||
|
var unblocked = _manager.ValidatePortfolioRisk("strat1", intent);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsFalse(blocked.Allow);
|
||||||
|
Assert.IsTrue(unblocked.Allow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user