// 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 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, INT8ExecutionBridge { 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; private System.IO.StreamWriter _fileLog; private readonly object _fileLock = new object(); #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; } [NinjaScriptProperty] [Display(Name = "Enable File Logging", GroupName = "Diagnostics", Order = 10)] public bool EnableFileLogging { get; set; } [NinjaScriptProperty] [Display(Name = "Log Directory", GroupName = "Diagnostics", Order = 11)] public string LogDirectory { get; set; } [NinjaScriptProperty] [Display(Name = "Enable Long Trades", GroupName = "Trade Direction", Order = 1)] public bool EnableLongTrades { get; set; } [NinjaScriptProperty] [Display(Name = "Enable Short Trades", GroupName = "Trade Direction", Order = 2)] public bool EnableShortTrades { get; set; } #endregion // INT8ExecutionBridge implementation public void EnterLongManaged(int quantity, string signalName, int stopTicks, int targetTicks, double tickSize) { if (stopTicks > 0) SetStopLoss(signalName, CalculationMode.Ticks, stopTicks, false); if (targetTicks > 0) SetProfitTarget(signalName, CalculationMode.Ticks, targetTicks); EnterLong(quantity, signalName); } public void EnterShortManaged(int quantity, string signalName, int stopTicks, int targetTicks, double tickSize) { if (stopTicks > 0) SetStopLoss(signalName, CalculationMode.Ticks, stopTicks, false); if (targetTicks > 0) SetProfitTarget(signalName, CalculationMode.Ticks, targetTicks); EnterShort(quantity, signalName); } public void ExitLongManaged(string signalName) { ExitLong(signalName); } public void ExitShortManaged(string signalName) { ExitShort(signalName); } public void FlattenAll() { ExitLong("EmergencyFlatten"); ExitShort("EmergencyFlatten"); } /// /// 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 = 50; EnableSDK = true; DailyLossLimit = 1000.0; MaxTradeRisk = 200.0; MaxOpenPositions = 3; RiskPerTrade = 100.0; MinContracts = 1; MaxContracts = 10; EnableKillSwitch = false; EnableVerboseLogging = false; EnableFileLogging = true; LogDirectory = string.Empty; EnableLongTrades = true; EnableShortTrades = true; _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()), NinjaTrader.Cbi.LogLevel.Error); _sdkInitialized = false; } } } else if (State == State.Realtime) { InitFileLog(); WriteSessionHeader(); } else if (State == State.Terminated) { WriteSessionFooter(); } } protected override void OnBarUpdate() { if (!_sdkInitialized || _sdkStrategy == null) { return; } if (CurrentBar < BarsRequiredToTrade) { return; } if (Time[0] == _lastBarTime) return; _lastBarTime = Time[0]; // Kill switch — checked AFTER bar guards so ExitLong/ExitShort are valid 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; } // 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()), NinjaTrader.Cbi.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; FileLog(string.Format("FILL {0} {1} @ {2:F2} | OrderId={3}", execution.MarketPosition, execution.Quantity, execution.Price, execution.OrderId)); _executionAdapter.ProcessExecution(orderId, executionId, price, quantity, time); } private void InitFileLog() { if (!EnableFileLogging) return; try { string dir = string.IsNullOrEmpty(LogDirectory) ? System.IO.Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "NinjaTrader 8", "log", "nt8-sdk") : LogDirectory; System.IO.Directory.CreateDirectory(dir); string path = System.IO.Path.Combine( dir, string.Format("session_{0}.log", DateTime.Now.ToString("yyyyMMdd_HHmmss"))); _fileLog = new System.IO.StreamWriter(path, false); _fileLog.AutoFlush = true; Print(string.Format("[NT8-SDK] File log started: {0}", path)); } catch (Exception ex) { Print(string.Format("[NT8-SDK] Failed to open file log: {0}", ex.Message)); } } private void FileLog(string message) { if (_fileLog == null) return; lock (_fileLock) { try { _fileLog.WriteLine(string.Format("[{0:HH:mm:ss.fff}] {1}", DateTime.Now, message)); } catch { } } } private void WriteSessionHeader() { FileLog("=== SESSION START " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " ==="); FileLog(string.Format("Strategy : {0}", Name)); FileLog(string.Format("Account : {0}", Account != null ? Account.Name : "N/A")); FileLog(string.Format("Symbol : {0}", Instrument != null ? Instrument.FullName : "N/A")); FileLog(string.Format("Risk : DailyLimit=${0} MaxTradeRisk=${1} RiskPerTrade=${2}", DailyLossLimit, MaxTradeRisk, RiskPerTrade)); FileLog(string.Format("Sizing : MinContracts={0} MaxContracts={1}", MinContracts, MaxContracts)); FileLog(string.Format("VerboseLog : {0} FileLog: {1}", EnableVerboseLogging, EnableFileLogging)); FileLog("---"); } private void WriteSessionFooter() { FileLog("---"); FileLog("=== SESSION END " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " ==="); if (_fileLog != null) { lock (_fileLock) { try { _fileLog.Close(); } catch { } _fileLog = null; } } } 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( _logger, 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() { DateTime etTime; try { var easternZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time"); etTime = TimeZoneInfo.ConvertTime(Time[0], easternZone); } catch { etTime = Time[0]; } var customData = new Dictionary(); customData.Add("CurrentBar", CurrentBar); customData.Add("BarsRequiredToTrade", BarsRequiredToTrade); customData.Add("OrdersToday", _ordersSubmittedToday); return NT8DataConverter.ConvertContext( Instrument.MasterInstrument.Name, etTime, BuildPositionInfo(), BuildAccountInfo(), BuildSessionInfo(), customData); } private AccountInfo BuildAccountInfo() { double cashValue = 100000.0; double buyingPower = 250000.0; try { if (Account != null) { cashValue = Account.Get(AccountItem.CashValue, Currency.UsDollar); buyingPower = Account.Get(AccountItem.BuyingPower, Currency.UsDollar); } } catch (Exception ex) { Print(string.Format("[NT8-SDK] WARNING: Could not read live account balance, using defaults: {0}", ex.Message)); } var accountInfo = NT8DataConverter.ConvertAccount(cashValue, buyingPower, 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() { DateTime etTime; try { var easternZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time"); etTime = TimeZoneInfo.ConvertTime(Time[0], easternZone); } catch { etTime = Time[0]; } var sessionStart = etTime.Date.AddHours(9).AddMinutes(30); var sessionEnd = etTime.Date.AddHours(16); var isRth = etTime.TimeOfDay >= TimeSpan.FromHours(9.5) && etTime.TimeOfDay < TimeSpan.FromHours(16.0); _currentSession = NT8DataConverter.ConvertSession(sessionStart, sessionEnd, isRth, isRth ? "RTH" : "ETH"); return _currentSession; } private void ProcessStrategyIntent(StrategyIntent intent, StrategyContext context) { // Direction filter — checked before risk to avoid unnecessary processing if (intent.Side == SdkOrderSide.Buy && !EnableLongTrades) { if (EnableVerboseLogging) Print(string.Format("[SDK] Long trade filtered by direction setting: {0}", intent.Symbol)); return; } if (intent.Side == SdkOrderSide.Sell && !EnableShortTrades) { if (EnableVerboseLogging) Print(string.Format("[SDK] Short trade filtered by direction setting: {0}", intent.Symbol)); return; } 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 (State == State.Historical) { // Skip circuit breaker during backtest — wall-clock timeout is meaningless on historical data. } else 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, Guid.NewGuid().ToString("N").Substring(0, 12)); if (EnableFileLogging) { string grade = "N/A"; string score = "N/A"; string factors = string.Empty; if (intent.Metadata != null && intent.Metadata.ContainsKey("confluence_score")) { var cs = intent.Metadata["confluence_score"] as NT8.Core.Intelligence.ConfluenceScore; if (cs != null) { grade = cs.Grade.ToString(); score = cs.WeightedScore.ToString("F3"); var sb = new System.Text.StringBuilder(); foreach (var f in cs.Factors) sb.Append(string.Format("{0}={1:F2} ", f.Type, f.Score)); factors = sb.ToString().TrimEnd(); } } FileLog(string.Format("SIGNAL {0} | Grade={1} | Score={2}", intent.Side, grade, score)); if (!string.IsNullOrEmpty(factors)) FileLog(string.Format(" Factors: {0}", factors)); FileLog(string.Format("SUBMIT {0} {1} @ Market | Stop={2} Target={3}", intent.Side, request.Quantity, intent.StopTicks, intent.TargetTicks)); } _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); } } }