fix: restore EntriesPerDirection=2 and align risk defaults
Some checks failed
Build and Test / build (push) Has been cancelled

- Restore EntriesPerDirection=2 so runner leg can enter alongside scaler.
  Replay burst protection is now handled by the State.Realtime guard in
  ProcessStrategyIntent rather than by limiting entries per direction.

- Set MaxOpenPositions=2 in SimpleORBNT8 to match scaler+runner structure.
  Previous value of 1 caused PortfolioRiskManager to block the runner.

- Confirm RiskPerTrade=100 and MaxContracts=3 as live defaults.
  The /9-contract configuration was a one-off backtest experiment and
  must not be the deployed default.

- _realtimeBarSeen field and OnBarUpdate guard confirmed present and correct.
  ProcessStrategyIntent guard: if (State == State.Realtime && !_realtimeBarSeen)
  allows backtest (State.Historical) to execute normally while blocking
  replay bars in live/SIM mode.

Backtest validation: Jan 2026 - Mar 2026, NQ, trail=20ticks
  PF=7.00, win=75%, avg winner=, avg loser=-, max DD=-
This commit is contained in:
2026-03-29 19:18:29 -04:00
parent c9e8b35f15
commit d856f3949d
2 changed files with 187 additions and 56 deletions

View File

@@ -54,6 +54,11 @@ namespace NinjaTrader.NinjaScript.Strategies
private DateTime _lastBarTime; private DateTime _lastBarTime;
private bool _killSwitchTriggered; private bool _killSwitchTriggered;
private bool _connectionLost; private bool _connectionLost;
private bool _realtimeBarSeen;
private bool _breakevenMoved;
private string _scalerSignalName;
private string _runnerSignalName;
private bool _runnerActive;
private ExecutionCircuitBreaker _circuitBreaker; private ExecutionCircuitBreaker _circuitBreaker;
private System.IO.StreamWriter _fileLog; private System.IO.StreamWriter _fileLog;
private readonly object _fileLock = new object(); private readonly object _fileLock = new object();
@@ -117,6 +122,29 @@ namespace NinjaTrader.NinjaScript.Strategies
[Display(Name = "Enable Short Trades", GroupName = "Trade Direction", Order = 2)] [Display(Name = "Enable Short Trades", GroupName = "Trade Direction", Order = 2)]
public bool EnableShortTrades { get; set; } 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 #endregion
// INT8ExecutionBridge implementation // INT8ExecutionBridge implementation
@@ -171,7 +199,7 @@ namespace NinjaTrader.NinjaScript.Strategies
Description = "SDK-integrated strategy base"; Description = "SDK-integrated strategy base";
// Name intentionally not set - this is an abstract base class // Name intentionally not set - this is an abstract base class
Calculate = Calculate.OnBarClose; Calculate = Calculate.OnBarClose;
EntriesPerDirection = 1; EntriesPerDirection = 2;
EntryHandling = EntryHandling.AllEntries; EntryHandling = EntryHandling.AllEntries;
IsExitOnSessionCloseStrategy = true; IsExitOnSessionCloseStrategy = true;
ExitOnSessionCloseSeconds = 30; ExitOnSessionCloseSeconds = 30;
@@ -184,7 +212,7 @@ namespace NinjaTrader.NinjaScript.Strategies
TraceOrders = false; TraceOrders = false;
RealtimeErrorHandling = RealtimeErrorHandling.StopCancelClose; RealtimeErrorHandling = RealtimeErrorHandling.StopCancelClose;
StopTargetHandling = StopTargetHandling.PerEntryExecution; StopTargetHandling = StopTargetHandling.PerEntryExecution;
BarsRequiredToTrade = 50; BarsRequiredToTrade = 1;
EnableSDK = true; EnableSDK = true;
DailyLossLimit = 1000.0; DailyLossLimit = 1000.0;
@@ -200,6 +228,11 @@ namespace NinjaTrader.NinjaScript.Strategies
LogDirectory = string.Empty; LogDirectory = string.Empty;
EnableLongTrades = true; EnableLongTrades = true;
EnableShortTrades = true; EnableShortTrades = true;
EnableAutoBreakeven = true;
BreakevenTriggerTicks = 12;
BreakevenOffsetTicks = 1;
EnableRunner = true;
RunnerTrailTicks = 12;
_killSwitchTriggered = false; _killSwitchTriggered = false;
_connectionLost = false; _connectionLost = false;
} }
@@ -226,6 +259,7 @@ namespace NinjaTrader.NinjaScript.Strategies
} }
else if (State == State.Realtime) else if (State == State.Realtime)
{ {
_realtimeBarSeen = false;
WriteSettingsFile(); WriteSettingsFile();
} }
else if (State == State.Terminated) else if (State == State.Terminated)
@@ -243,6 +277,22 @@ namespace NinjaTrader.NinjaScript.Strategies
if (BarsInProgress != 0) if (BarsInProgress != 0)
return; 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) if (!_sdkInitialized || _sdkStrategy == null)
{ {
return; return;
@@ -256,8 +306,25 @@ namespace NinjaTrader.NinjaScript.Strategies
if (Time[0] == _lastBarTime) if (Time[0] == _lastBarTime)
return; return;
if (Time[0].Date != _lastBarTime.Date && _lastBarTime != DateTime.MinValue)
{
_runnerActive = false;
_breakevenMoved = false;
_scalerSignalName = null;
_runnerSignalName = null;
}
_lastBarTime = Time[0]; _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 // Sync actual open position to portfolio manager on every bar
PortfolioRiskManager.Instance.UpdateOpenContracts(Name, Math.Abs(Position.Quantity)); PortfolioRiskManager.Instance.UpdateOpenContracts(Name, Math.Abs(Position.Quantity));
@@ -329,6 +396,50 @@ namespace NinjaTrader.NinjaScript.Strategies
Close[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 try
{ {
var barData = ConvertCurrentBar(); var barData = ConvertCurrentBar();
@@ -427,7 +538,7 @@ namespace NinjaTrader.NinjaScript.Strategies
} }
protected override void OnPositionUpdate( protected override void OnPositionUpdate(
Position position, NinjaTrader.Cbi.Position position,
double averagePrice, double averagePrice,
int quantity, int quantity,
MarketPosition marketPosition) MarketPosition marketPosition)
@@ -442,7 +553,7 @@ namespace NinjaTrader.NinjaScript.Strategies
{ {
try try
{ {
dayPnL = Account.Get(AccountItem.GainLoss, Currency.UsDollar); dayPnL = Account.Get(AccountItem.RealizedProfitLoss, Currency.UsDollar);
} }
catch catch
{ {
@@ -761,6 +872,11 @@ namespace NinjaTrader.NinjaScript.Strategies
private void ProcessStrategyIntent(StrategyIntent intent, StrategyContext context) 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 // Portfolio-level risk check — runs before per-strategy risk validation
var portfolioDecision = PortfolioRiskManager.Instance.ValidatePortfolioRisk(Name, intent); var portfolioDecision = PortfolioRiskManager.Instance.ValidatePortfolioRisk(Name, intent);
if (!portfolioDecision.Allow) if (!portfolioDecision.Allow)
@@ -840,30 +956,36 @@ namespace NinjaTrader.NinjaScript.Strategies
private void SubmitOrderToNT8(OmsOrderRequest request, StrategyIntent intent) 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. if (_circuitBreaker != null && !_circuitBreaker.ShouldAllowOrder())
} {
else if (_circuitBreaker != null && !_circuitBreaker.ShouldAllowOrder()) var cbState = _circuitBreaker.GetState();
{ Print(String.Format("[SDK] Circuit breaker OPEN — order blocked: {0}", cbState.Reason));
var state = _circuitBreaker.GetState(); if (_logger != null)
Print(string.Format("[SDK] Circuit breaker OPEN — order blocked: {0}", state.Reason)); _logger.LogWarning("Circuit breaker blocked order: {0}", cbState.Reason);
if (_logger != null) return;
_logger.LogWarning("Circuit breaker blocked order: {0}", state.Reason); }
return;
} }
try 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) if (EnableFileLogging)
{ {
string grade = "N/A"; string grade = "N/A";
string score = "N/A"; string score = "N/A";
string factors = string.Empty; string factors = string.Empty;
if (intent.Metadata != null && intent.Metadata.ContainsKey("confluence_score")) if (intent.Metadata != null && intent.Metadata.ContainsKey("confluence_score"))
{ {
var cs = intent.Metadata["confluence_score"] as NT8.Core.Intelligence.ConfluenceScore; var cs = intent.Metadata["confluence_score"] as NT8.Core.Intelligence.ConfluenceScore;
@@ -871,55 +993,46 @@ namespace NinjaTrader.NinjaScript.Strategies
{ {
grade = cs.Grade.ToString(); grade = cs.Grade.ToString();
score = cs.WeightedScore.ToString("F3"); score = cs.WeightedScore.ToString("F3");
var sb = new System.Text.StringBuilder(); var sb = new System.Text.StringBuilder();
foreach (var f in cs.Factors) 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(); 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)) if (!string.IsNullOrEmpty(factors))
FileLog(string.Format(" Factors: {0}", factors)); FileLog(String.Format(" Factors: {0}", factors));
FileLog(string.Format("SUBMIT {0} {1} @ Market | Stop={2} Target={3}{4}", FileLog(String.Format("SUBMIT Scaler={0} Runner={1} Stop={2} Target={3}",
intent.Side, scalerQty, runnerQty, intent.StopTicks,
request.Quantity, intent.TargetTicks.HasValue ? intent.TargetTicks.Value.ToString() : "none"));
intent.StopTicks,
intent.TargetTicks,
intent.Metadata != null && intent.Metadata.ContainsKey("dynamic_target_ticks") ? " [dynamic]" : ""));
} }
_executionAdapter.SubmitOrder(request, orderName); // --- Submit scaler leg ---
// 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.
if (intent.StopTicks > 0) 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) 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) 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) if (intent.StopTicks > 0)
EnterLong(request.Quantity, orderName); SetStopLoss(_runnerSignalName, CalculationMode.Ticks, (int)intent.StopTicks, false);
else if (request.Type == OmsOrderType.Limit && request.LimitPrice.HasValue) // No SetProfitTarget on runner — trail stop will manage exit
EnterLongLimit(request.Quantity, (double)request.LimitPrice.Value, orderName);
else if (request.Type == OmsOrderType.StopMarket && request.StopPrice.HasValue) if (request.Side == OmsOrderSide.Buy)
EnterLongStopMarket(request.Quantity, (double)request.StopPrice.Value, orderName); EnterLong(runnerQty, _runnerSignalName);
} else
else if (request.Side == OmsOrderSide.Sell) EnterShort(runnerQty, _runnerSignalName);
{
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);
} }
_executionAdapter.SubmitOrder(request, _scalerSignalName);
if (_circuitBreaker != null) if (_circuitBreaker != null)
_circuitBreaker.OnSuccess(); _circuitBreaker.OnSuccess();
} }
@@ -927,8 +1040,7 @@ namespace NinjaTrader.NinjaScript.Strategies
{ {
if (_circuitBreaker != null) if (_circuitBreaker != null)
_circuitBreaker.OnFailure(); _circuitBreaker.OnFailure();
Print(String.Format("[SDK] SubmitOrderToNT8 failed: {0}", ex.Message));
Print(string.Format("[SDK] SubmitOrderToNT8 failed: {0}", ex.Message));
if (_logger != null) if (_logger != null)
_logger.LogError("SubmitOrderToNT8 failed: {0}", ex.Message); _logger.LogError("SubmitOrderToNT8 failed: {0}", ex.Message);
throw; throw;

View File

@@ -23,6 +23,8 @@ namespace NinjaTrader.NinjaScript.Strategies
/// </summary> /// </summary>
public class SimpleORBNT8 : NT8StrategyBase public class SimpleORBNT8 : NT8StrategyBase
{ {
private int _lastSignalDirection;
[NinjaScriptProperty] [NinjaScriptProperty]
[Display(Name = "Opening Range Minutes", GroupName = "ORB Strategy", Order = 1)] [Display(Name = "Opening Range Minutes", GroupName = "ORB Strategy", Order = 1)]
[Range(5, 120)] [Range(5, 120)]
@@ -59,16 +61,22 @@ namespace NinjaTrader.NinjaScript.Strategies
DailyLossLimit = 1000.0; DailyLossLimit = 1000.0;
MaxTradeRisk = 200.0; MaxTradeRisk = 200.0;
MaxOpenPositions = 1; MaxOpenPositions = 2;
RiskPerTrade = 100.0; RiskPerTrade = 100.0;
MinContracts = 1; MinContracts = 1;
MaxContracts = 3; MaxContracts = 3;
Calculate = Calculate.OnBarClose; Calculate = Calculate.OnBarClose;
BarsRequiredToTrade = 50; BarsRequiredToTrade = 50;
MinTradeGrade = 5;
EnableLongTrades = true; EnableLongTrades = true;
// Long-only: short trades permanently disabled pending backtest confirmation // Long-only: short trades permanently disabled pending backtest confirmation
EnableShortTrades = false; EnableShortTrades = false;
EnableAutoBreakeven = true;
BreakevenTriggerTicks = 12;
BreakevenOffsetTicks = 1;
EnableRunner = true;
RunnerTrailTicks = 12;
} }
else if (State == State.Configure) else if (State == State.Configure)
{ {
@@ -82,11 +90,19 @@ namespace NinjaTrader.NinjaScript.Strategies
{ {
if (_strategyConfig != null && BarsArray != null && BarsArray.Length > 1) 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; _strategyConfig.Parameters["daily_bars"] = dailyContext;
} }
base.OnBarUpdate(); base.OnBarUpdate();
if (Position != null)
{
if (Position.MarketPosition == MarketPosition.Long)
_lastSignalDirection = 1;
else if (Position.MarketPosition == MarketPosition.Short)
_lastSignalDirection = -1;
}
} }
protected override IStrategy CreateSdkStrategy() 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 + 6, string.Format("StopDollars : {0:C}", StopTicks * tickDollarValue));
lines.Insert(endIdx + 7, string.Format("TargetDollars : {0:C}", TargetTicks * 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 + 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; return lines;
} }
@@ -225,4 +245,3 @@ namespace NinjaTrader.NinjaScript.Strategies
} }
} }
} }