// 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 bool _connectionLost; private bool _realtimeBarSeen; private bool _breakevenMoved; private string _scalerSignalName; private string _runnerSignalName; private bool _runnerActive; 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 = "Min Trade Grade (1=F,2=D,3=C,4=B,5=A,6=A+)", GroupName = "Confluence", Order = 1)] [Range(0, 6)] public int MinTradeGrade { 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; } [NinjaScriptProperty] [Display(Name = "Enable Auto Breakeven", GroupName = "Exit Management", Order = 1)] public bool EnableAutoBreakeven { get; set; } [NinjaScriptProperty] [Display(Name = "Breakeven Trigger Ticks", GroupName = "Exit Management", Order = 2)] [Range(1, 100)] public int BreakevenTriggerTicks { get; set; } [NinjaScriptProperty] [Display(Name = "Breakeven Offset Ticks", GroupName = "Exit Management", Order = 3)] [Range(0, 20)] public int BreakevenOffsetTicks { get; set; } [NinjaScriptProperty] [Display(Name = "Enable Runner", GroupName = "Exit Management", Order = 4)] public bool EnableRunner { get; set; } [NinjaScriptProperty] [Display(Name = "Runner Trail Ticks", GroupName = "Exit Management", Order = 5)] [Range(4, 100)] public int RunnerTrailTicks { 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"); } /// /// Returns true if the concrete strategy has ForceSessionReset enabled. /// Override in subclass to expose the NinjaScript parameter value. /// Default returns false so base class never forces a reset unless overridden. /// protected virtual bool GetForceSessionReset() { return false; } /// /// 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 = 2; EntryHandling = EntryHandling.AllEntries; IsExitOnSessionCloseStrategy = true; ExitOnSessionCloseSeconds = 30; IsFillLimitOnTouch = false; MaximumBarsLookBack = MaximumBarsLookBack.TwoHundredFiftySix; OrderFillResolution = OrderFillResolution.Standard; Slippage = 0; StartBehavior = StartBehavior.AdoptAccountPosition; TimeInForce = TimeInForce.Gtc; TraceOrders = false; RealtimeErrorHandling = RealtimeErrorHandling.StopCancelClose; StopTargetHandling = StopTargetHandling.PerEntryExecution; BarsRequiredToTrade = 1; EnableSDK = true; DailyLossLimit = 1000.0; MaxTradeRisk = 200.0; MaxOpenPositions = 3; RiskPerTrade = 100.0; MinContracts = 1; MaxContracts = 3; EnableKillSwitch = false; EnableVerboseLogging = false; MinTradeGrade = 4; EnableFileLogging = true; LogDirectory = string.Empty; EnableLongTrades = true; EnableShortTrades = true; EnableAutoBreakeven = true; BreakevenTriggerTicks = 20; BreakevenOffsetTicks = 1; EnableRunner = true; RunnerTrailTicks = 20; _killSwitchTriggered = false; _connectionLost = false; } else if (State == State.DataLoaded) { if (EnableSDK) { try { // DIAGNOSTIC: Print actual runtime property values to confirm // what NT8 loaded vs what SetDefaults specified. Print(string.Format("[SDK-DIAG] SetDefaults check: BE={0} Trail={1} MaxC={2} SB={3} EPD={4}", BreakevenTriggerTicks, RunnerTrailTicks, MaxContracts, StartBehavior, EntriesPerDirection)); InitFileLog(); InitializeSdkComponents(); _sdkInitialized = true; Print(string.Format("[SDK] {0} initialized successfully", Name)); WriteSettingsFile(); WriteSessionHeader(); } 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) { _realtimeBarSeen = false; // If ForceSessionReset is enabled, push a reset signal into the SDK strategy // so _tradeTaken is cleared before any live bar is processed. // This recovers from replay-burst scenarios where historical bars set _tradeTaken. if (_sdkStrategy != null && GetForceSessionReset()) { var resetParams = new Dictionary(); resetParams.Add("force_session_reset", true); _sdkStrategy.SetParameters(resetParams); Print(string.Format("[SDK] ForceSessionReset: _tradeTaken cleared on live start at {0}", DateTime.Now.ToString("HH:mm:ss"))); } WriteSettingsFile(); } else if (State == State.Terminated) { PortfolioRiskManager.Instance.UnregisterStrategy(Name); WriteSessionFooter(); } } protected override void OnBarUpdate() { // Only process primary bar series — ignore secondary data series updates. // Secondary series (e.g. daily bars for confluence) trigger OnBarUpdate separately // and must never generate strategy signals. if (BarsInProgress != 0) return; // Require 7 completed daily bars before allowing any signal. // NarrowRangeFactorCalculator needs 7 daily bars for NR7 scoring. // Without this guard, NR7 silently returns its floor score (0.3) // which may suppress trades via the MinTradeGrade confluence gate. if (BarsArray != null && BarsArray.Length > 1 && CurrentBars != null && CurrentBars.Length > 1 && CurrentBars[1] < 7) { // Always print warm-up status — visible in Strategy Analyzer output // to confirm how many daily bars are available on a given backtest range. if (CurrentBar % 20 == 0) Print(String.Format("[SDK] Daily warm-up: {0}/7 bars — waiting for NR7 history. Extend backtest start date or add pre-load days.", CurrentBars[1] + 1)); return; } if (!_sdkInitialized || _sdkStrategy == null) { return; } if (CurrentBar < BarsRequiredToTrade) { return; } if (Time[0] == _lastBarTime) return; if (Time[0].Date != _lastBarTime.Date && _lastBarTime != DateTime.MinValue) { _runnerActive = false; _breakevenMoved = false; _scalerSignalName = null; _runnerSignalName = null; } _lastBarTime = Time[0]; // Mark first bar seen after going realtime. Until this fires, we're // processing catch-up replay bars and must not submit orders. if (State == State.Realtime && !_realtimeBarSeen) { _realtimeBarSeen = true; Print(string.Format("[SDK] First realtime bar seen: {0}", Time[0])); return; } // Sync actual open position to portfolio manager on every bar PortfolioRiskManager.Instance.UpdateOpenContracts(Name, Math.Abs(Position.Quantity)); // 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; } // 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; } // Hard RTH guard using NT8 bar time converted from CT to ET. // Belt-and-suspenders against SDK session timezone issues. DateTime ntBarTimeEt; try { var centralZone = TimeZoneInfo.FindSystemTimeZoneById("Central Standard Time"); var easternZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time"); DateTime utcTime = TimeZoneInfo.ConvertTimeToUtc( DateTime.SpecifyKind(Time[0], DateTimeKind.Unspecified), centralZone); ntBarTimeEt = TimeZoneInfo.ConvertTimeFromUtc(utcTime, easternZone); } catch { ntBarTimeEt = Time[0]; } bool isRthBar = ntBarTimeEt.TimeOfDay >= new TimeSpan(9, 30, 0) && ntBarTimeEt.TimeOfDay < new TimeSpan(16, 0, 0); if (!isRthBar) { if (EnableVerboseLogging && CurrentBar % 500 == 0) Print(string.Format("[SDK] Skipping ETH bar {0} at {1:HH:mm} ET", CurrentBar, ntBarTimeEt)); 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])); } // --- Breakeven and runner trailing monitor --- if (_runnerActive && !string.IsNullOrEmpty(_runnerSignalName) && Position.Quantity != 0) { double entryPrice = Position.AveragePrice; double currentClose = Close[0]; bool isLong = Position.MarketPosition == MarketPosition.Long; double profitTicks = isLong ? (currentClose - entryPrice) / TickSize : (entryPrice - currentClose) / TickSize; // Move runner stop to breakeven + offset once trigger is reached if (EnableAutoBreakeven && !_breakevenMoved && profitTicks >= BreakevenTriggerTicks) { double bePrice = isLong ? entryPrice + (BreakevenOffsetTicks * TickSize) : entryPrice - (BreakevenOffsetTicks * TickSize); SetStopLoss(_runnerSignalName, CalculationMode.Price, bePrice, false); _breakevenMoved = true; Print(String.Format("[SDK] Runner breakeven set at {0:F2} (profit={1:F0} ticks)", bePrice, profitTicks)); if (EnableFileLogging) FileLog(String.Format("BREAKEVEN runner stop -> {0:F2} profit={1:F0}ticks", bePrice, profitTicks)); } // Activate trailing stop on runner once breakeven is secured if (_breakevenMoved && RunnerTrailTicks > 0) { SetTrailStop(_runnerSignalName, CalculationMode.Ticks, RunnerTrailTicks, false); } } // Clear runner state when flat if (Position.Quantity == 0 && _runnerActive) { _runnerActive = false; _breakevenMoved = false; if (EnableFileLogging && !string.IsNullOrEmpty(_runnerSignalName)) FileLog("RUNNER closed — position flat"); } 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)); 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); } protected override void OnPositionUpdate( NinjaTrader.Cbi.Position position, double averagePrice, int quantity, MarketPosition marketPosition) { if (!_sdkInitialized || _riskManager == null) return; try { double dayPnL = 0.0; if (Account != null) { try { dayPnL = Account.Get(AccountItem.RealizedProfitLoss, Currency.UsDollar); } catch { dayPnL = 0.0; } } _riskManager.OnPnLUpdate(dayPnL, dayPnL); PortfolioRiskManager.Instance.ReportPnL(Name, dayPnL); if (EnableVerboseLogging) Print(string.Format("[SDK] P&L update: DayPnL={0:C} Position={1} Qty={2}", dayPnL, marketPosition, quantity)); FileLog(string.Format("PNL_UPDATE DayPnL={0:C} Position={1} Qty={2}", dayPnL, marketPosition, quantity)); } catch (Exception ex) { Print(string.Format("[SDK] OnPositionUpdate error: {0}", ex.Message)); } } /// /// Handles broker connection status changes. Halts new orders on disconnect, /// logs reconnect, and resets the connection flag when restored. /// NinjaScript signature: single ConnectionStatusEventArgs parameter. /// protected override void OnConnectionStatusUpdate( ConnectionStatusEventArgs connectionStatusUpdate) { if (connectionStatusUpdate == null) return; if (connectionStatusUpdate.Status == ConnectionStatus.Connected) { if (_connectionLost) { _connectionLost = false; Print(string.Format("[NT8-SDK] Connection RESTORED at {0} — trading resumed.", DateTime.Now.ToString("HH:mm:ss"))); FileLog(string.Format("CONNECTION RESTORED at {0}", DateTime.Now.ToString("HH:mm:ss"))); } } else if (connectionStatusUpdate.Status == ConnectionStatus.Disconnected || connectionStatusUpdate.Status == ConnectionStatus.ConnectionLost) { if (!_connectionLost) { _connectionLost = true; Print(string.Format("[NT8-SDK] Connection LOST at {0} — halting new orders. Status={1}", DateTime.Now.ToString("HH:mm:ss"), connectionStatusUpdate.Status)); FileLog(string.Format("CONNECTION LOST at {0} Status={1}", DateTime.Now.ToString("HH:mm:ss"), connectionStatusUpdate.Status)); } } } 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("Mode : {0}", State == State.Historical ? "BACKTEST" : "LIVE/SIM")); 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(string.Format("ConnectionLost : {0}", _connectionLost)); 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, DailyLossLimit); _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(); PortfolioRiskManager.Instance.RegisterStrategy(Name, _riskConfig); Print(string.Format("[NT8-SDK] Registered with PortfolioRiskManager: {0}", PortfolioRiskManager.Instance.GetStatusSnapshot())); _ordersSubmittedToday = 0; _lastBarTime = DateTime.MinValue; _lastAccountInfo = null; _lastPosition = null; _currentSession = null; } private BarData ConvertCurrentBar() { DateTime barTimeEt; try { var centralZone = TimeZoneInfo.FindSystemTimeZoneById("Central Standard Time"); var easternZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time"); DateTime utcTime = TimeZoneInfo.ConvertTimeToUtc( DateTime.SpecifyKind(Time[0], DateTimeKind.Unspecified), centralZone); barTimeEt = TimeZoneInfo.ConvertTimeFromUtc(utcTime, easternZone); } catch { barTimeEt = Time[0]; } return NT8DataConverter.ConvertBar( Instrument.MasterInstrument.Name, barTimeEt, Open[0], High[0], Low[0], Close[0], (long)Volume[0], (int)BarsPeriod.Value); } private StrategyContext BuildStrategyContext() { DateTime etTime; try { var centralZone = TimeZoneInfo.FindSystemTimeZoneById("Central Standard Time"); var easternZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time"); DateTime utcTime = TimeZoneInfo.ConvertTimeToUtc( DateTime.SpecifyKind(Time[0], DateTimeKind.Unspecified), centralZone); etTime = TimeZoneInfo.ConvertTimeFromUtc(utcTime, 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 centralZone = TimeZoneInfo.FindSystemTimeZoneById("Central Standard Time"); var easternZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time"); DateTime utcTime = TimeZoneInfo.ConvertTimeToUtc( DateTime.SpecifyKind(Time[0], DateTimeKind.Unspecified), centralZone); etTime = TimeZoneInfo.ConvertTimeFromUtc(utcTime, easternZone); } catch { etTime = Time[0]; } // Futures trade nearly 24 hours. Bars at/after 17:00 ET belong to the next // calendar day's RTH trading session. DateTime tradingDate; if (etTime.TimeOfDay >= new TimeSpan(17, 0, 0)) tradingDate = etTime.Date.AddDays(1); else tradingDate = etTime.Date; if (EnableVerboseLogging && (CurrentBar == BarsRequiredToTrade || CurrentBar % 500 == 0)) { Print(string.Format("[SDK-TZ] Bar {0}: NT8 Time[0]={1:yyyy-MM-dd HH:mm:ss} | etTime={2:yyyy-MM-dd HH:mm:ss} | isRth={3}", CurrentBar, Time[0], etTime, etTime.TimeOfDay >= TimeSpan.FromHours(9.5) && etTime.TimeOfDay < TimeSpan.FromHours(16.0))); } var sessionStart = tradingDate.AddHours(9).AddMinutes(30); var sessionEnd = tradingDate.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) { // In live/SIM: block if we haven't seen a genuine realtime bar yet (replay guard). // In Strategy Analyzer (State.Historical): always allow — backtest must execute normally. if (State == State.Realtime && !_realtimeBarSeen) return; // 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 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) { if (State != State.Historical) { if (_circuitBreaker != null && !_circuitBreaker.ShouldAllowOrder()) { var cbState = _circuitBreaker.GetState(); Print(String.Format("[SDK] Circuit breaker OPEN — order blocked: {0}", cbState.Reason)); if (_logger != null) _logger.LogWarning("Circuit breaker blocked order: {0}", cbState.Reason); return; } } try { bool useRunner = EnableRunner && request.Quantity >= 2; int scalerQty = useRunner ? request.Quantity - 1 : request.Quantity; int runnerQty = useRunner ? 1 : 0; string baseId = Guid.NewGuid().ToString("N").Substring(0, 12); _scalerSignalName = String.Format("SDK_{0}_S_{1}", intent.Symbol, baseId); _runnerSignalName = useRunner ? String.Format("SDK_{0}_R_{1}", intent.Symbol, baseId) : null; _breakevenMoved = false; _runnerActive = useRunner; 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 Scaler={0} Runner={1} Stop={2} Target={3}", scalerQty, runnerQty, intent.StopTicks, intent.TargetTicks.HasValue ? intent.TargetTicks.Value.ToString() : "none")); } // --- Submit scaler leg --- if (intent.StopTicks > 0) SetStopLoss(_scalerSignalName, CalculationMode.Ticks, (int)intent.StopTicks, false); if (intent.TargetTicks.HasValue && intent.TargetTicks.Value > 0) SetProfitTarget(_scalerSignalName, CalculationMode.Ticks, (int)intent.TargetTicks.Value); if (request.Side == OmsOrderSide.Buy) EnterLong(scalerQty, _scalerSignalName); else EnterShort(scalerQty, _scalerSignalName); // --- Submit runner leg (no fixed target — exits via trailing stop) --- if (useRunner) { if (intent.StopTicks > 0) SetStopLoss(_runnerSignalName, CalculationMode.Ticks, (int)intent.StopTicks, false); // No SetProfitTarget on runner — trail stop will manage exit if (request.Side == OmsOrderSide.Buy) EnterLong(runnerQty, _runnerSignalName); else EnterShort(runnerQty, _runnerSignalName); } _executionAdapter.SubmitOrder(request, _scalerSignalName); 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); } /// /// Returns all strategy parameter lines for the settings export file. /// Override in subclasses to append strategy-specific parameters. /// Call base.GetStrategySettingsLines() first then add to the list. /// protected virtual List GetStrategySettingsLines() { var lines = new List(); lines.Add("=== STRATEGY SETTINGS EXPORT ==="); lines.Add(string.Format("ExportTime : {0:yyyy-MM-dd HH:mm:ss}", DateTime.Now)); lines.Add(string.Format("StrategyName : {0}", Name)); lines.Add(string.Format("Description : {0}", Description)); lines.Add(string.Format("Account : {0}", Account != null ? Account.Name : "N/A")); lines.Add(string.Format("Instrument : {0}", Instrument != null ? Instrument.FullName : "N/A")); lines.Add(string.Format("BarsPeriod : {0} {1}", BarsPeriod != null ? BarsPeriod.Value.ToString() : "N/A", BarsPeriod != null ? BarsPeriod.BarsPeriodType.ToString() : string.Empty)); lines.Add(string.Format("BarsRequiredToTrade: {0}", BarsRequiredToTrade)); lines.Add(string.Format("Calculate : {0}", Calculate)); lines.Add("--- Risk ---"); lines.Add(string.Format("DailyLossLimit : {0:C}", DailyLossLimit)); lines.Add(string.Format("MaxTradeRisk : {0:C}", MaxTradeRisk)); lines.Add(string.Format("MaxOpenPositions : {0}", MaxOpenPositions)); lines.Add(string.Format("RiskPerTrade : {0:C}", RiskPerTrade)); lines.Add("--- Sizing ---"); lines.Add(string.Format("MinContracts : {0}", MinContracts)); lines.Add(string.Format("MaxContracts : {0}", MaxContracts)); lines.Add("--- Direction ---"); lines.Add(string.Format("EnableLongTrades : {0}", EnableLongTrades)); lines.Add(string.Format("EnableShortTrades : {0}", EnableShortTrades)); lines.Add("--- Controls ---"); lines.Add(string.Format("EnableKillSwitch : {0}", EnableKillSwitch)); lines.Add(string.Format("EnableVerboseLogging: {0}", EnableVerboseLogging)); lines.Add(string.Format("MinTradeGrade : {0}", MinTradeGrade)); lines.Add(string.Format("EnableFileLogging : {0}", EnableFileLogging)); lines.Add(string.Format("LogDirectory : {0}", string.IsNullOrEmpty(LogDirectory) ? "(default)" : LogDirectory)); lines.Add("--- Portfolio ---"); lines.Add(string.Format("PortfolioStatus : {0}", PortfolioRiskManager.Instance.GetStatusSnapshot())); lines.Add("=== END SETTINGS ==="); return lines; } /// /// Writes a settings export file to the same directory as the session log. /// File is named settings_STRATEGYNAME_YYYYMMDD_HHmmss.txt. /// Only writes when EnableVerboseLogging is true. /// private void WriteSettingsFile() { if (!EnableVerboseLogging) 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 safeName = Name.Replace(" ", "_").Replace("/", "_").Replace("\\", "_"); string path = System.IO.Path.Combine(dir, string.Format("settings_{0}_{1}.txt", safeName, DateTime.Now.ToString("yyyyMMdd_HHmmss"))); var lines = GetStrategySettingsLines(); System.IO.File.WriteAllLines(path, lines.ToArray()); Print(string.Format("[NT8-SDK] Settings exported: {0}", path)); FileLog(string.Format("SETTINGS FILE: {0}", path)); } catch (Exception ex) { Print(string.Format("[NT8-SDK] WARNING: Could not write settings file: {0}", ex.Message)); } } } }