diff --git a/src/NT8.Adapters/Strategies/NT8StrategyBase.cs b/src/NT8.Adapters/Strategies/NT8StrategyBase.cs index 68f60b5..e45f132 100644 --- a/src/NT8.Adapters/Strategies/NT8StrategyBase.cs +++ b/src/NT8.Adapters/Strategies/NT8StrategyBase.cs @@ -54,6 +54,11 @@ namespace NinjaTrader.NinjaScript.Strategies 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(); @@ -117,6 +122,29 @@ namespace NinjaTrader.NinjaScript.Strategies [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 @@ -171,7 +199,7 @@ namespace NinjaTrader.NinjaScript.Strategies Description = "SDK-integrated strategy base"; // Name intentionally not set - this is an abstract base class Calculate = Calculate.OnBarClose; - EntriesPerDirection = 1; + EntriesPerDirection = 2; EntryHandling = EntryHandling.AllEntries; IsExitOnSessionCloseStrategy = true; ExitOnSessionCloseSeconds = 30; @@ -184,7 +212,7 @@ namespace NinjaTrader.NinjaScript.Strategies TraceOrders = false; RealtimeErrorHandling = RealtimeErrorHandling.StopCancelClose; StopTargetHandling = StopTargetHandling.PerEntryExecution; - BarsRequiredToTrade = 50; + BarsRequiredToTrade = 1; EnableSDK = true; DailyLossLimit = 1000.0; @@ -200,6 +228,11 @@ namespace NinjaTrader.NinjaScript.Strategies LogDirectory = string.Empty; EnableLongTrades = true; EnableShortTrades = true; + EnableAutoBreakeven = true; + BreakevenTriggerTicks = 12; + BreakevenOffsetTicks = 1; + EnableRunner = true; + RunnerTrailTicks = 12; _killSwitchTriggered = false; _connectionLost = false; } @@ -226,6 +259,7 @@ namespace NinjaTrader.NinjaScript.Strategies } else if (State == State.Realtime) { + _realtimeBarSeen = false; WriteSettingsFile(); } else if (State == State.Terminated) @@ -243,6 +277,22 @@ namespace NinjaTrader.NinjaScript.Strategies 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; @@ -256,8 +306,25 @@ namespace NinjaTrader.NinjaScript.Strategies 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)); @@ -329,6 +396,50 @@ namespace NinjaTrader.NinjaScript.Strategies 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(); @@ -427,7 +538,7 @@ namespace NinjaTrader.NinjaScript.Strategies } protected override void OnPositionUpdate( - Position position, + NinjaTrader.Cbi.Position position, double averagePrice, int quantity, MarketPosition marketPosition) @@ -442,7 +553,7 @@ namespace NinjaTrader.NinjaScript.Strategies { try { - dayPnL = Account.Get(AccountItem.GainLoss, Currency.UsDollar); + dayPnL = Account.Get(AccountItem.RealizedProfitLoss, Currency.UsDollar); } catch { @@ -761,6 +872,11 @@ namespace NinjaTrader.NinjaScript.Strategies 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) @@ -840,30 +956,36 @@ namespace NinjaTrader.NinjaScript.Strategies private void SubmitOrderToNT8(OmsOrderRequest request, StrategyIntent intent) { - // Circuit breaker gate - if (State == State.Historical) + 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; + 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 { - var orderName = string.Format("SDK_{0}_{1}", intent.Symbol, Guid.NewGuid().ToString("N").Substring(0, 12)); + 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; @@ -871,55 +993,46 @@ namespace NinjaTrader.NinjaScript.Strategies { 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)); + 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)); + 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}{4}", - intent.Side, - request.Quantity, - intent.StopTicks, - intent.TargetTicks, - intent.Metadata != null && intent.Metadata.ContainsKey("dynamic_target_ticks") ? " [dynamic]" : "")); + 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")); } - _executionAdapter.SubmitOrder(request, orderName); - - // Register stop and target BEFORE submitting the entry order. - // NT8 requires stop/target to be pre-registered to the signal name - // so they are applied correctly in both backtest and live/SIM modes. + // --- Submit scaler leg --- if (intent.StopTicks > 0) - SetStopLoss(orderName, CalculationMode.Ticks, (int)intent.StopTicks, false); - + SetStopLoss(_scalerSignalName, CalculationMode.Ticks, (int)intent.StopTicks, false); if (intent.TargetTicks.HasValue && intent.TargetTicks.Value > 0) - SetProfitTarget(orderName, CalculationMode.Ticks, (int)intent.TargetTicks.Value); + 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 (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(_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(); } @@ -927,8 +1040,7 @@ namespace NinjaTrader.NinjaScript.Strategies { if (_circuitBreaker != null) _circuitBreaker.OnFailure(); - - Print(string.Format("[SDK] SubmitOrderToNT8 failed: {0}", ex.Message)); + Print(String.Format("[SDK] SubmitOrderToNT8 failed: {0}", ex.Message)); if (_logger != null) _logger.LogError("SubmitOrderToNT8 failed: {0}", ex.Message); throw; diff --git a/src/NT8.Adapters/Strategies/SimpleORBNT8.cs b/src/NT8.Adapters/Strategies/SimpleORBNT8.cs index b0b6560..0326961 100644 --- a/src/NT8.Adapters/Strategies/SimpleORBNT8.cs +++ b/src/NT8.Adapters/Strategies/SimpleORBNT8.cs @@ -23,6 +23,8 @@ namespace NinjaTrader.NinjaScript.Strategies /// public class SimpleORBNT8 : NT8StrategyBase { + private int _lastSignalDirection; + [NinjaScriptProperty] [Display(Name = "Opening Range Minutes", GroupName = "ORB Strategy", Order = 1)] [Range(5, 120)] @@ -59,16 +61,22 @@ namespace NinjaTrader.NinjaScript.Strategies DailyLossLimit = 1000.0; MaxTradeRisk = 200.0; - MaxOpenPositions = 1; + MaxOpenPositions = 2; RiskPerTrade = 100.0; MinContracts = 1; MaxContracts = 3; Calculate = Calculate.OnBarClose; BarsRequiredToTrade = 50; + MinTradeGrade = 5; EnableLongTrades = true; // Long-only: short trades permanently disabled pending backtest confirmation EnableShortTrades = false; + EnableAutoBreakeven = true; + BreakevenTriggerTicks = 12; + BreakevenOffsetTicks = 1; + EnableRunner = true; + RunnerTrailTicks = 12; } else if (State == State.Configure) { @@ -82,11 +90,19 @@ namespace NinjaTrader.NinjaScript.Strategies { if (_strategyConfig != null && BarsArray != null && BarsArray.Length > 1) { - DailyBarContext dailyContext = BuildDailyBarContext(0, 0.0, (double)Volume[0]); + DailyBarContext dailyContext = BuildDailyBarContext(_lastSignalDirection, 0.0, (double)Volume[0]); _strategyConfig.Parameters["daily_bars"] = dailyContext; } base.OnBarUpdate(); + + if (Position != null) + { + if (Position.MarketPosition == MarketPosition.Long) + _lastSignalDirection = 1; + else if (Position.MarketPosition == MarketPosition.Short) + _lastSignalDirection = -1; + } } protected override IStrategy CreateSdkStrategy() @@ -160,6 +176,10 @@ namespace NinjaTrader.NinjaScript.Strategies lines.Insert(endIdx + 6, string.Format("StopDollars : {0:C}", StopTicks * tickDollarValue)); lines.Insert(endIdx + 7, string.Format("TargetDollars : {0:C}", TargetTicks * tickDollarValue)); lines.Insert(endIdx + 8, string.Format("RR_Ratio : {0:F2}:1", (double)TargetTicks / StopTicks)); + lines.Insert(endIdx + 9, String.Format("AutoBreakeven : {0} @ {1}ticks + {2}tick offset", + EnableAutoBreakeven, BreakevenTriggerTicks, BreakevenOffsetTicks)); + lines.Insert(endIdx + 10, String.Format("Runner : {0} | Trail={1}ticks", + EnableRunner, RunnerTrailTicks)); return lines; } @@ -225,4 +245,3 @@ namespace NinjaTrader.NinjaScript.Strategies } } } -