fix: restore EntriesPerDirection=2 and align risk defaults
Some checks failed
Build and Test / build (push) Has been cancelled
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:
@@ -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;
|
||||
|
||||
@@ -23,6 +23,8 @@ namespace NinjaTrader.NinjaScript.Strategies
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user