Production hardening: kill switch, circuit breaker, trailing stops, log level, holiday calendar
Some checks failed
Build and Test / build (push) Has been cancelled

This commit is contained in:
2026-02-24 15:00:41 -05:00
parent 0e36fe5d23
commit a87152effb
50 changed files with 12849 additions and 752 deletions

View File

@@ -10,4 +10,9 @@
<ProjectReference Include="..\NT8.Core\NT8.Core.csproj" />
</ItemGroup>
</Project>
<ItemGroup>
<Compile Remove="Strategies\**\*.cs" />
<None Include="Strategies\**\*.cs" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,365 @@
using System;
using System.Collections.Generic;
using NT8.Core.OMS;
namespace NT8.Adapters.NinjaTrader
{
/// <summary>
/// Adapter for executing orders through NinjaTrader 8 platform.
/// Bridges SDK order requests to NT8 order submission and handles callbacks.
/// Thread-safe for concurrent NT8 callbacks.
/// </summary>
public class NT8ExecutionAdapter
{
private readonly object _lock = new object();
private readonly Dictionary<string, OrderTrackingInfo> _orderTracking;
private readonly Dictionary<string, string> _nt8ToSdkOrderMap;
/// <summary>
/// Creates a new NT8 execution adapter.
/// </summary>
public NT8ExecutionAdapter()
{
_orderTracking = new Dictionary<string, OrderTrackingInfo>();
_nt8ToSdkOrderMap = new Dictionary<string, string>();
}
/// <summary>
/// Submit an order to NinjaTrader 8.
/// NOTE: This method tracks order state only. Actual NT8 submission is performed by strategy wrapper code.
/// </summary>
/// <param name="request">SDK order request.</param>
/// <param name="sdkOrderId">Unique SDK order ID.</param>
/// <returns>Tracking info for the submitted order.</returns>
/// <exception cref="ArgumentNullException">Thrown when request or sdkOrderId is invalid.</exception>
/// <exception cref="InvalidOperationException">Thrown when the same order ID is submitted twice.</exception>
public OrderTrackingInfo SubmitOrder(OrderRequest request, string sdkOrderId)
{
if (request == null)
{
throw new ArgumentNullException("request");
}
if (string.IsNullOrWhiteSpace(sdkOrderId))
{
throw new ArgumentNullException("sdkOrderId");
}
try
{
lock (_lock)
{
if (_orderTracking.ContainsKey(sdkOrderId))
{
throw new InvalidOperationException(string.Format("Order {0} already exists", sdkOrderId));
}
var trackingInfo = new OrderTrackingInfo();
trackingInfo.SdkOrderId = sdkOrderId;
trackingInfo.Nt8OrderId = null;
trackingInfo.OriginalRequest = request;
trackingInfo.CurrentState = OrderState.Pending;
trackingInfo.FilledQuantity = 0;
trackingInfo.AverageFillPrice = 0.0;
trackingInfo.LastUpdate = DateTime.UtcNow;
trackingInfo.ErrorMessage = null;
_orderTracking.Add(sdkOrderId, trackingInfo);
return trackingInfo;
}
}
catch (Exception)
{
throw;
}
}
/// <summary>
/// Process order update callback from NinjaTrader 8.
/// Called by NT8 strategy wrapper OnOrderUpdate.
/// </summary>
/// <param name="nt8OrderId">NT8 order ID.</param>
/// <param name="sdkOrderId">SDK order ID.</param>
/// <param name="orderState">NT8 order state string.</param>
/// <param name="filled">Filled quantity.</param>
/// <param name="averageFillPrice">Average fill price.</param>
/// <param name="errorCode">Error code if rejected.</param>
/// <param name="errorMessage">Error message if rejected.</param>
public void ProcessOrderUpdate(
string nt8OrderId,
string sdkOrderId,
string orderState,
int filled,
double averageFillPrice,
int errorCode,
string errorMessage)
{
if (string.IsNullOrWhiteSpace(sdkOrderId))
{
return;
}
try
{
lock (_lock)
{
if (!_orderTracking.ContainsKey(sdkOrderId))
{
return;
}
var info = _orderTracking[sdkOrderId];
if (!string.IsNullOrWhiteSpace(nt8OrderId) && info.Nt8OrderId == null)
{
info.Nt8OrderId = nt8OrderId;
_nt8ToSdkOrderMap[nt8OrderId] = sdkOrderId;
}
info.CurrentState = MapNT8OrderState(orderState);
info.FilledQuantity = filled;
info.AverageFillPrice = averageFillPrice;
info.LastUpdate = DateTime.UtcNow;
if (errorCode != 0 && !string.IsNullOrWhiteSpace(errorMessage))
{
info.ErrorMessage = string.Format("[{0}] {1}", errorCode, errorMessage);
info.CurrentState = OrderState.Rejected;
}
}
}
catch (Exception)
{
throw;
}
}
/// <summary>
/// Process execution callback from NinjaTrader 8.
/// Called by NT8 strategy wrapper OnExecutionUpdate.
/// </summary>
/// <param name="nt8OrderId">NT8 order ID.</param>
/// <param name="executionId">Execution identifier.</param>
/// <param name="price">Execution price.</param>
/// <param name="quantity">Execution quantity.</param>
/// <param name="time">Execution time.</param>
public void ProcessExecution(
string nt8OrderId,
string executionId,
double price,
int quantity,
DateTime time)
{
if (string.IsNullOrWhiteSpace(nt8OrderId))
{
return;
}
try
{
lock (_lock)
{
if (!_nt8ToSdkOrderMap.ContainsKey(nt8OrderId))
{
return;
}
var sdkOrderId = _nt8ToSdkOrderMap[nt8OrderId];
if (!_orderTracking.ContainsKey(sdkOrderId))
{
return;
}
var info = _orderTracking[sdkOrderId];
info.LastUpdate = time;
if (info.FilledQuantity >= info.OriginalRequest.Quantity)
{
info.CurrentState = OrderState.Filled;
}
else if (info.FilledQuantity > 0)
{
info.CurrentState = OrderState.PartiallyFilled;
}
}
}
catch (Exception)
{
throw;
}
}
/// <summary>
/// Request to cancel an order.
/// NOTE: Actual cancellation is performed by strategy wrapper code.
/// </summary>
/// <param name="sdkOrderId">SDK order ID to cancel.</param>
/// <returns>True when cancel request is accepted; otherwise false.</returns>
public bool CancelOrder(string sdkOrderId)
{
if (string.IsNullOrWhiteSpace(sdkOrderId))
{
throw new ArgumentNullException("sdkOrderId");
}
try
{
lock (_lock)
{
if (!_orderTracking.ContainsKey(sdkOrderId))
{
return false;
}
var info = _orderTracking[sdkOrderId];
if (info.CurrentState == OrderState.Filled ||
info.CurrentState == OrderState.Cancelled ||
info.CurrentState == OrderState.Rejected)
{
return false;
}
info.LastUpdate = DateTime.UtcNow;
return true;
}
}
catch (Exception)
{
throw;
}
}
/// <summary>
/// Get current status of an order.
/// </summary>
/// <param name="sdkOrderId">SDK order ID.</param>
/// <returns>Order status snapshot; null when not found.</returns>
public OrderStatus GetOrderStatus(string sdkOrderId)
{
if (string.IsNullOrWhiteSpace(sdkOrderId))
{
return null;
}
try
{
lock (_lock)
{
if (!_orderTracking.ContainsKey(sdkOrderId))
{
return null;
}
var info = _orderTracking[sdkOrderId];
var status = new OrderStatus();
status.OrderId = info.SdkOrderId;
status.Symbol = info.OriginalRequest.Symbol;
status.Side = info.OriginalRequest.Side;
status.Quantity = info.OriginalRequest.Quantity;
status.Type = info.OriginalRequest.Type;
status.State = info.CurrentState;
status.FilledQuantity = info.FilledQuantity;
status.AverageFillPrice = info.FilledQuantity > 0 ? (decimal)info.AverageFillPrice : 0m;
status.CreatedTime = info.LastUpdate;
status.FilledTime = info.FilledQuantity > 0 ? (DateTime?)info.LastUpdate : null;
return status;
}
}
catch (Exception)
{
throw;
}
}
/// <summary>
/// Maps NinjaTrader order state string to SDK order state.
/// </summary>
/// <param name="nt8State">NT8 order state string.</param>
/// <returns>Mapped SDK state.</returns>
private OrderState MapNT8OrderState(string nt8State)
{
if (string.IsNullOrWhiteSpace(nt8State))
{
return OrderState.Expired;
}
switch (nt8State.ToUpperInvariant())
{
case "ACCEPTED":
case "WORKING":
return OrderState.Working;
case "FILLED":
return OrderState.Filled;
case "PARTFILLED":
case "PARTIALLYFILLED":
return OrderState.PartiallyFilled;
case "CANCELLED":
case "CANCELED":
return OrderState.Cancelled;
case "REJECTED":
return OrderState.Rejected;
case "PENDINGCANCEL":
return OrderState.Working;
case "PENDINGCHANGE":
case "PENDINGSUBMIT":
return OrderState.Pending;
default:
return OrderState.Expired;
}
}
}
/// <summary>
/// Internal tracking information for orders managed by NT8ExecutionAdapter.
/// </summary>
public class OrderTrackingInfo
{
/// <summary>
/// SDK order identifier.
/// </summary>
public string SdkOrderId { get; set; }
/// <summary>
/// NinjaTrader order identifier.
/// </summary>
public string Nt8OrderId { get; set; }
/// <summary>
/// Original order request.
/// </summary>
public OrderRequest OriginalRequest { get; set; }
/// <summary>
/// Current SDK order state.
/// </summary>
public OrderState CurrentState { get; set; }
/// <summary>
/// Filled quantity.
/// </summary>
public int FilledQuantity { get; set; }
/// <summary>
/// Average fill price.
/// </summary>
public double AverageFillPrice { get; set; }
/// <summary>
/// Last update timestamp.
/// </summary>
public DateTime LastUpdate { get; set; }
/// <summary>
/// Last error message.
/// </summary>
public string ErrorMessage { get; set; }
}
}

View File

@@ -0,0 +1,59 @@
// File: MinimalTestStrategy.cs
using System;
using NinjaTrader.Cbi;
using NinjaTrader.Data;
using NinjaTrader.NinjaScript;
using NinjaTrader.NinjaScript.Strategies;
namespace NinjaTrader.NinjaScript.Strategies
{
/// <summary>
/// Minimal test strategy to validate NT8 integration and compilation.
/// </summary>
public class MinimalTestStrategy : Strategy
{
private int _barCount;
protected override void OnStateChange()
{
if (State == State.SetDefaults)
{
Name = "Minimal Test";
Description = "Simple test strategy - logs bars only";
Calculate = Calculate.OnBarClose;
BarsRequiredToTrade = 1;
}
else if (State == State.DataLoaded)
{
_barCount = 0;
Print("[MinimalTest] Strategy initialized");
}
else if (State == State.Terminated)
{
Print(string.Format("[MinimalTest] Strategy terminated. Processed {0} bars", _barCount));
}
}
protected override void OnBarUpdate()
{
if (CurrentBar < BarsRequiredToTrade)
return;
_barCount++;
if (_barCount % 10 == 0)
{
Print(string.Format(
"[MinimalTest] Bar {0}: {1} O={2:F2} H={3:F2} L={4:F2} C={5:F2} V={6}",
CurrentBar,
Time[0].ToString("HH:mm:ss"),
Open[0],
High[0],
Low[0],
Close[0],
Volume[0]));
}
}
}
}

View File

@@ -0,0 +1,545 @@
// File: NT8StrategyBase.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using NinjaTrader.Cbi;
using NinjaTrader.Data;
using NinjaTrader.Gui;
using NinjaTrader.Gui.Chart;
using NinjaTrader.Gui.Tools;
using NinjaTrader.NinjaScript;
using NinjaTrader.NinjaScript.Indicators;
using NinjaTrader.NinjaScript.Strategies;
using Microsoft.Extensions.Logging.Abstractions;
using NT8.Adapters.NinjaTrader;
using NT8.Core.Common.Interfaces;
using NT8.Core.Common.Models;
using NT8.Core.Execution;
using NT8.Core.Logging;
using NT8.Core.Risk;
using NT8.Core.Sizing;
using SdkPosition = NT8.Core.Common.Models.Position;
using SdkOrderSide = NT8.Core.Common.Models.OrderSide;
using SdkOrderType = NT8.Core.Common.Models.OrderType;
using OmsOrderRequest = NT8.Core.OMS.OrderRequest;
using OmsOrderSide = NT8.Core.OMS.OrderSide;
using OmsOrderType = NT8.Core.OMS.OrderType;
using OmsOrderState = NT8.Core.OMS.OrderState;
using OmsOrderStatus = NT8.Core.OMS.OrderStatus;
namespace NinjaTrader.NinjaScript.Strategies
{
/// <summary>
/// Base class for strategies that integrate NT8 SDK components.
/// </summary>
public abstract class NT8StrategyBase : Strategy
{
private readonly object _lock = new object();
protected IStrategy _sdkStrategy;
protected IRiskManager _riskManager;
protected IPositionSizer _positionSizer;
protected NT8ExecutionAdapter _executionAdapter;
protected ILogger _logger;
protected StrategyConfig _strategyConfig;
protected RiskConfig _riskConfig;
protected SizingConfig _sizingConfig;
private bool _sdkInitialized;
private AccountInfo _lastAccountInfo;
private SdkPosition _lastPosition;
private MarketSession _currentSession;
private int _ordersSubmittedToday;
private DateTime _lastBarTime;
private bool _killSwitchTriggered;
private ExecutionCircuitBreaker _circuitBreaker;
#region User-Configurable Properties
[NinjaScriptProperty]
[Display(Name = "Enable SDK", GroupName = "SDK", Order = 1)]
public bool EnableSDK { get; set; }
[NinjaScriptProperty]
[Display(Name = "Daily Loss Limit", GroupName = "Risk", Order = 1)]
public double DailyLossLimit { get; set; }
[NinjaScriptProperty]
[Display(Name = "Max Trade Risk", GroupName = "Risk", Order = 2)]
public double MaxTradeRisk { get; set; }
[NinjaScriptProperty]
[Display(Name = "Max Positions", GroupName = "Risk", Order = 3)]
public int MaxOpenPositions { get; set; }
[NinjaScriptProperty]
[Display(Name = "Risk Per Trade", GroupName = "Sizing", Order = 1)]
public double RiskPerTrade { get; set; }
[NinjaScriptProperty]
[Display(Name = "Min Contracts", GroupName = "Sizing", Order = 2)]
public int MinContracts { get; set; }
[NinjaScriptProperty]
[Display(Name = "Max Contracts", GroupName = "Sizing", Order = 3)]
public int MaxContracts { get; set; }
[NinjaScriptProperty]
[Display(Name = "Kill Switch (Flatten + Stop)", GroupName = "Emergency Controls", Order = 1)]
public bool EnableKillSwitch { get; set; }
[NinjaScriptProperty]
[Display(Name = "Verbose Logging", GroupName = "Debug", Order = 1)]
public bool EnableVerboseLogging { get; set; }
#endregion
/// <summary>
/// Create the SDK strategy instance.
/// </summary>
protected abstract IStrategy CreateSdkStrategy();
/// <summary>
/// Configure strategy-specific values after initialization.
/// </summary>
protected abstract void ConfigureStrategyParameters();
protected override void OnStateChange()
{
if (State == State.SetDefaults)
{
Description = "SDK-integrated strategy base";
// Name intentionally not set - this is an abstract base class
Calculate = Calculate.OnBarClose;
EntriesPerDirection = 1;
EntryHandling = EntryHandling.AllEntries;
IsExitOnSessionCloseStrategy = true;
ExitOnSessionCloseSeconds = 30;
IsFillLimitOnTouch = false;
MaximumBarsLookBack = MaximumBarsLookBack.TwoHundredFiftySix;
OrderFillResolution = OrderFillResolution.Standard;
Slippage = 0;
StartBehavior = StartBehavior.WaitUntilFlat;
TimeInForce = TimeInForce.Gtc;
TraceOrders = false;
RealtimeErrorHandling = RealtimeErrorHandling.StopCancelClose;
StopTargetHandling = StopTargetHandling.PerEntryExecution;
BarsRequiredToTrade = 20;
EnableSDK = true;
DailyLossLimit = 1000.0;
MaxTradeRisk = 200.0;
MaxOpenPositions = 3;
RiskPerTrade = 100.0;
MinContracts = 1;
MaxContracts = 10;
EnableKillSwitch = false;
EnableVerboseLogging = false;
_killSwitchTriggered = false;
}
else if (State == State.DataLoaded)
{
if (EnableSDK)
{
try
{
InitializeSdkComponents();
_sdkInitialized = true;
Print(string.Format("[SDK] {0} initialized successfully", Name));
}
catch (Exception ex)
{
Print(string.Format("[SDK ERROR] Initialization failed: {0}", ex.Message));
Log(string.Format("[SDK ERROR] {0}", ex.ToString()), LogLevel.Error);
_sdkInitialized = false;
}
}
}
}
protected override void OnBarUpdate()
{
// Kill switch check — must be first
if (EnableKillSwitch)
{
if (!_killSwitchTriggered)
{
_killSwitchTriggered = true;
Print(string.Format("[SDK] KILL SWITCH ACTIVATED at {0} — flattening all positions.", Time[0]));
try
{
ExitLong("KillSwitch");
ExitShort("KillSwitch");
}
catch (Exception ex)
{
Print(string.Format("[SDK] Kill switch flatten error: {0}", ex.Message));
}
}
return;
}
if (!_sdkInitialized || _sdkStrategy == null)
{
if (CurrentBar == 0)
Print(string.Format("[SDK] Not initialized: sdkInit={0}, strategy={1}", _sdkInitialized, _sdkStrategy != null));
return;
}
if (CurrentBar < BarsRequiredToTrade)
{
if (CurrentBar == 0)
Print(string.Format("[SDK] Waiting for bars: current={0}, required={1}", CurrentBar, BarsRequiredToTrade));
return;
}
if (Time[0] == _lastBarTime)
return;
_lastBarTime = Time[0];
// Log first processable bar and every 100th bar.
if (CurrentBar == BarsRequiredToTrade || CurrentBar % 100 == 0)
{
Print(string.Format("[SDK] Processing bar {0}: {1} O={2:F2} H={3:F2} L={4:F2} C={5:F2}",
CurrentBar,
Time[0].ToString("yyyy-MM-dd HH:mm"),
Open[0],
High[0],
Low[0],
Close[0]));
}
try
{
var barData = ConvertCurrentBar();
var context = BuildStrategyContext();
StrategyIntent intent;
lock (_lock)
{
intent = _sdkStrategy.OnBar(barData, context);
}
if (intent != null)
{
Print(string.Format("[SDK] Intent generated: {0} {1} @ {2}", intent.Side, intent.Symbol, intent.EntryType));
ProcessStrategyIntent(intent, context);
}
}
catch (Exception ex)
{
if (_logger != null)
_logger.LogError("OnBarUpdate failed: {0}", ex.Message);
Print(string.Format("[SDK ERROR] OnBarUpdate: {0}", ex.Message));
Log(string.Format("[SDK ERROR] {0}", ex.ToString()), LogLevel.Error);
}
}
protected override void OnOrderUpdate(
Order order,
double limitPrice,
double stopPrice,
int quantity,
int filled,
double averageFillPrice,
NinjaTrader.Cbi.OrderState orderState,
DateTime time,
ErrorCode errorCode,
string nativeError)
{
if (!_sdkInitialized || _executionAdapter == null || order == null)
return;
if (string.IsNullOrEmpty(order.Name) || !order.Name.StartsWith("SDK_"))
return;
// Record NT8 rejections in circuit breaker
if (orderState == NinjaTrader.Cbi.OrderState.Rejected && _circuitBreaker != null)
{
var reason = string.Format("{0} {1}", errorCode, nativeError ?? string.Empty);
_circuitBreaker.RecordOrderRejection(reason);
Print(string.Format("[SDK] Order rejected by NT8: {0}", reason));
}
_executionAdapter.ProcessOrderUpdate(
order.OrderId,
order.Name,
orderState.ToString(),
filled,
averageFillPrice,
(int)errorCode,
nativeError);
}
protected override void OnExecutionUpdate(
Execution execution,
string executionId,
double price,
int quantity,
MarketPosition marketPosition,
string orderId,
DateTime time)
{
if (!_sdkInitialized || _executionAdapter == null || execution == null || execution.Order == null)
return;
if (string.IsNullOrEmpty(execution.Order.Name) || !execution.Order.Name.StartsWith("SDK_"))
return;
_executionAdapter.ProcessExecution(orderId, executionId, price, quantity, time);
}
private void InitializeSdkComponents()
{
_logger = new BasicLogger(Name);
Print(string.Format("[SDK] Initializing with: DailyLoss={0:C}, TradeRisk={1:C}, MaxPos={2}",
DailyLossLimit,
MaxTradeRisk,
MaxOpenPositions));
_riskConfig = new RiskConfig(DailyLossLimit, MaxTradeRisk, MaxOpenPositions, true);
_sizingConfig = new SizingConfig(
SizingMethod.FixedDollarRisk,
MinContracts,
MaxContracts,
RiskPerTrade,
new Dictionary<string, object>());
_strategyConfig = new StrategyConfig(
Name,
Instrument.MasterInstrument.Name,
new Dictionary<string, object>(),
_riskConfig,
_sizingConfig);
_riskManager = new BasicRiskManager(_logger);
_positionSizer = new BasicPositionSizer(_logger);
_circuitBreaker = new ExecutionCircuitBreaker(
NullLogger<ExecutionCircuitBreaker>.Instance,
failureThreshold: 3,
timeout: TimeSpan.FromSeconds(30));
_executionAdapter = new NT8ExecutionAdapter();
_sdkStrategy = CreateSdkStrategy();
if (_sdkStrategy == null)
throw new InvalidOperationException("CreateSdkStrategy returned null");
_sdkStrategy.Initialize(_strategyConfig, null, _logger);
ConfigureStrategyParameters();
_ordersSubmittedToday = 0;
_lastBarTime = DateTime.MinValue;
_lastAccountInfo = null;
_lastPosition = null;
_currentSession = null;
}
private BarData ConvertCurrentBar()
{
return NT8DataConverter.ConvertBar(
Instrument.MasterInstrument.Name,
Time[0],
Open[0],
High[0],
Low[0],
Close[0],
(long)Volume[0],
(int)BarsPeriod.Value);
}
private StrategyContext BuildStrategyContext()
{
var customData = new Dictionary<string, object>();
customData.Add("CurrentBar", CurrentBar);
customData.Add("BarsRequiredToTrade", BarsRequiredToTrade);
customData.Add("OrdersToday", _ordersSubmittedToday);
return NT8DataConverter.ConvertContext(
Instrument.MasterInstrument.Name,
Time[0],
BuildPositionInfo(),
BuildAccountInfo(),
BuildSessionInfo(),
customData);
}
private AccountInfo BuildAccountInfo()
{
var accountInfo = NT8DataConverter.ConvertAccount(100000.0, 250000.0, 0.0, 0.0, DateTime.UtcNow);
_lastAccountInfo = accountInfo;
return accountInfo;
}
private SdkPosition BuildPositionInfo()
{
var p = NT8DataConverter.ConvertPosition(
Instrument.MasterInstrument.Name,
Position.Quantity,
Position.AveragePrice,
0.0,
0.0,
DateTime.UtcNow);
_lastPosition = p;
return p;
}
private MarketSession BuildSessionInfo()
{
if (_currentSession != null && _currentSession.SessionStart.Date == Time[0].Date)
return _currentSession;
var sessionStart = Time[0].Date.AddHours(9).AddMinutes(30);
var sessionEnd = Time[0].Date.AddHours(16);
var isRth = Time[0].Hour >= 9 && Time[0].Hour < 16;
var sessionName = isRth ? "RTH" : "ETH";
_currentSession = NT8DataConverter.ConvertSession(sessionStart, sessionEnd, isRth, sessionName);
return _currentSession;
}
private void ProcessStrategyIntent(StrategyIntent intent, StrategyContext context)
{
if (EnableVerboseLogging)
Print(string.Format("[SDK] Validating intent: {0} {1}", intent.Side, intent.Symbol));
var riskDecision = _riskManager.ValidateOrder(intent, context, _riskConfig);
if (!riskDecision.Allow)
{
if (EnableVerboseLogging)
Print(string.Format("[SDK] Risk REJECTED: {0}", riskDecision.RejectReason));
if (_logger != null)
_logger.LogWarning("Intent rejected by risk manager: {0}", riskDecision.RejectReason);
return;
}
if (EnableVerboseLogging)
Print(string.Format("[SDK] Risk approved"));
var sizingResult = _positionSizer.CalculateSize(intent, context, _sizingConfig);
if (EnableVerboseLogging)
{
Print(string.Format("[SDK] Position size: {0} contracts (min={1}, max={2})",
sizingResult.Contracts,
MinContracts,
MaxContracts));
}
if (sizingResult.Contracts < MinContracts)
{
if (EnableVerboseLogging)
Print(string.Format("[SDK] Size too small: {0} < {1}", sizingResult.Contracts, MinContracts));
return;
}
var request = new OmsOrderRequest();
request.Symbol = intent.Symbol;
request.Side = MapOrderSide(intent.Side);
request.Type = MapOrderType(intent.EntryType);
request.Quantity = sizingResult.Contracts;
request.LimitPrice = intent.LimitPrice.HasValue ? (decimal?)intent.LimitPrice.Value : null;
request.StopPrice = null;
if (EnableVerboseLogging)
{
Print(string.Format("[SDK] Submitting order: {0} {1} {2} @ {3}",
request.Side,
request.Quantity,
request.Symbol,
request.Type));
}
SubmitOrderToNT8(request, intent);
_ordersSubmittedToday++;
}
private void SubmitOrderToNT8(OmsOrderRequest request, StrategyIntent intent)
{
// Circuit breaker gate
if (_circuitBreaker != null && !_circuitBreaker.ShouldAllowOrder())
{
var state = _circuitBreaker.GetState();
Print(string.Format("[SDK] Circuit breaker OPEN — order blocked: {0}", state.Reason));
if (_logger != null)
_logger.LogWarning("Circuit breaker blocked order: {0}", state.Reason);
return;
}
try
{
var orderName = string.Format("SDK_{0}_{1}", intent.Symbol, DateTime.Now.Ticks);
_executionAdapter.SubmitOrder(request, orderName);
if (request.Side == OmsOrderSide.Buy)
{
if (request.Type == OmsOrderType.Market)
EnterLong(request.Quantity, orderName);
else if (request.Type == OmsOrderType.Limit && request.LimitPrice.HasValue)
EnterLongLimit(request.Quantity, (double)request.LimitPrice.Value, orderName);
else if (request.Type == OmsOrderType.StopMarket && request.StopPrice.HasValue)
EnterLongStopMarket(request.Quantity, (double)request.StopPrice.Value, orderName);
}
else if (request.Side == OmsOrderSide.Sell)
{
if (request.Type == OmsOrderType.Market)
EnterShort(request.Quantity, orderName);
else if (request.Type == OmsOrderType.Limit && request.LimitPrice.HasValue)
EnterShortLimit(request.Quantity, (double)request.LimitPrice.Value, orderName);
else if (request.Type == OmsOrderType.StopMarket && request.StopPrice.HasValue)
EnterShortStopMarket(request.Quantity, (double)request.StopPrice.Value, orderName);
}
if (intent.StopTicks > 0)
SetStopLoss(orderName, CalculationMode.Ticks, (int)intent.StopTicks, false);
if (intent.TargetTicks.HasValue && intent.TargetTicks.Value > 0)
SetProfitTarget(orderName, CalculationMode.Ticks, (int)intent.TargetTicks.Value);
if (_circuitBreaker != null)
_circuitBreaker.OnSuccess();
}
catch (Exception ex)
{
if (_circuitBreaker != null)
_circuitBreaker.OnFailure();
Print(string.Format("[SDK] SubmitOrderToNT8 failed: {0}", ex.Message));
if (_logger != null)
_logger.LogError("SubmitOrderToNT8 failed: {0}", ex.Message);
throw;
}
}
private static OmsOrderSide MapOrderSide(SdkOrderSide side)
{
if (side == SdkOrderSide.Buy)
return OmsOrderSide.Buy;
return OmsOrderSide.Sell;
}
private static OmsOrderType MapOrderType(SdkOrderType type)
{
if (type == SdkOrderType.Market)
return OmsOrderType.Market;
if (type == SdkOrderType.Limit)
return OmsOrderType.Limit;
if (type == SdkOrderType.StopLimit)
return OmsOrderType.StopLimit;
return OmsOrderType.StopMarket;
}
protected OmsOrderStatus GetSdkOrderStatus(string orderName)
{
if (_executionAdapter == null)
return null;
return _executionAdapter.GetOrderStatus(orderName);
}
}
}

View File

@@ -0,0 +1,112 @@
// File: SimpleORBNT8.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using NinjaTrader.Cbi;
using NinjaTrader.Data;
using NinjaTrader.Gui;
using NinjaTrader.Gui.Chart;
using NinjaTrader.Gui.Tools;
using NinjaTrader.NinjaScript;
using NinjaTrader.NinjaScript.Indicators;
using NinjaTrader.NinjaScript.Strategies;
using NT8.Core.Common.Interfaces;
using NT8.Strategies.Examples;
using SdkSimpleORB = NT8.Strategies.Examples.SimpleORBStrategy;
namespace NinjaTrader.NinjaScript.Strategies
{
/// <summary>
/// Simple Opening Range Breakout strategy integrated with NT8 SDK.
/// </summary>
public class SimpleORBNT8 : NT8StrategyBase
{
[NinjaScriptProperty]
[Display(Name = "Opening Range Minutes", GroupName = "ORB Strategy", Order = 1)]
[Range(5, 120)]
public int OpeningRangeMinutes { get; set; }
[NinjaScriptProperty]
[Display(Name = "Std Dev Multiplier", GroupName = "ORB Strategy", Order = 2)]
[Range(0.5, 3.0)]
public double StdDevMultiplier { get; set; }
[NinjaScriptProperty]
[Display(Name = "Stop Loss Ticks", GroupName = "ORB Risk", Order = 1)]
[Range(1, 50)]
public int StopTicks { get; set; }
[NinjaScriptProperty]
[Display(Name = "Profit Target Ticks", GroupName = "ORB Risk", Order = 2)]
[Range(1, 100)]
public int TargetTicks { get; set; }
protected override void OnStateChange()
{
if (State == State.SetDefaults)
{
Name = "Simple ORB NT8";
Description = "Opening Range Breakout with NT8 SDK integration";
OpeningRangeMinutes = 30;
StdDevMultiplier = 1.0;
StopTicks = 8;
TargetTicks = 16;
DailyLossLimit = 1000.0;
MaxTradeRisk = 200.0;
MaxOpenPositions = 1;
RiskPerTrade = 100.0;
MinContracts = 1;
MaxContracts = 3;
Calculate = Calculate.OnBarClose;
BarsRequiredToTrade = 50;
}
base.OnStateChange();
}
protected override IStrategy CreateSdkStrategy()
{
return new SdkSimpleORB(OpeningRangeMinutes, StdDevMultiplier);
}
protected override void ConfigureStrategyParameters()
{
_strategyConfig.RiskSettings.DailyLossLimit = DailyLossLimit;
_strategyConfig.RiskSettings.MaxTradeRisk = MaxTradeRisk;
_strategyConfig.RiskSettings.MaxOpenPositions = MaxOpenPositions;
// Guard: Instrument may be null during strategy list loading
if (Instrument != null && Instrument.MasterInstrument != null)
{
var pointValue = Instrument.MasterInstrument.PointValue;
var tickSize = Instrument.MasterInstrument.TickSize;
var dollarRisk = StopTicks * tickSize * pointValue;
if (dollarRisk > _strategyConfig.RiskSettings.MaxTradeRisk)
_strategyConfig.RiskSettings.MaxTradeRisk = dollarRisk;
}
_strategyConfig.SizingSettings.RiskPerTrade = RiskPerTrade;
_strategyConfig.SizingSettings.MinContracts = MinContracts;
_strategyConfig.SizingSettings.MaxContracts = MaxContracts;
_strategyConfig.Parameters["StopTicks"] = StopTicks;
_strategyConfig.Parameters["TargetTicks"] = TargetTicks;
_strategyConfig.Parameters["OpeningRangeMinutes"] = OpeningRangeMinutes;
if (_logger != null)
{
_logger.LogInformation(
"Simple ORB configured: OR={0}min, Stop={1}ticks, Target={2}ticks",
OpeningRangeMinutes,
StopTicks,
TargetTicks);
}
}
}
}