// 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 { /// /// Base class for strategies that integrate NT8 SDK components. /// 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 /// /// Create the SDK strategy instance. /// protected abstract IStrategy CreateSdkStrategy(); /// /// Configure strategy-specific values after initialization. /// 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()); _strategyConfig = new StrategyConfig( Name, Instrument.MasterInstrument.Name, new Dictionary(), _riskConfig, _sizingConfig); _riskManager = new BasicRiskManager(_logger); _positionSizer = new BasicPositionSizer(_logger); _circuitBreaker = new ExecutionCircuitBreaker( NullLogger.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(); 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); } } }