When you add a higher-timeframe series with AddDataSeries and read it on your primary series, it is easy to consume a higher-timeframe bar before it has actually closed — NinjaScript's equivalent of the TradingView request.security repaint. This guide shows correct BarsInProgress routing and how to reference secondary series without leaking future data. It is educational material about code correctness and implies nothing about profitability.
AddDataSeries attaches an extra bar series — a higher timeframe, another instrument, or both — to a strategy. Each series has its own index, addressed by BarsArray[i] and its own barsAgo accessors like Closes[i][0]. The trap: a higher-timeframe bar spans many primary bars, and for most of that span the higher-timeframe bar is still forming. If you read its current value from your fast series, you are reading a partial or, worse, a completed value that historically did not exist yet at that primary-bar timestamp.
This is the NinjaScript version of the classic request.security repaint from Pine. On a chart it looks stable because you only ever see closed bars. In a bar-by-bar backtest, an unsynced read hands your fast series the finished higher-timeframe close before the higher-timeframe bar has closed — pure future data.
// index 0 = primary (say 1-minute), index 1 = added 60-minute
protected override void OnStateChange()
{
if (State == State.Configure)
AddDataSeries(BarsPeriodType.Minute, 60);
}The series is now attached. Whether you use it correctly depends entirely on when you read it. See the general [repainting](/learn/repainting) writeup for how this class of bug behaves across platforms.
OnBarUpdate is called for every series the strategy carries, not just the primary. BarsInProgress tells you which series triggered the current call: 0 for the primary, 1 for the first AddDataSeries, and so on. If you ignore it, your primary logic runs on higher-timeframe ticks and vice versa, and your indexing silently refers to the wrong series.
Route explicitly:
protected override void OnBarUpdate()
{
if (BarsInProgress == 0)
{
// primary-series logic here
}
else if (BarsInProgress == 1)
{
// 60-minute-series logic here; a bar on index 1 just closed
}
}When BarsInProgress == 1 fires, a 60-minute bar has genuinely completed — that is the safe moment to capture its values. When BarsInProgress == 0 fires, you are on a primary bar and any higher-timeframe value you read must be the most recently closed higher-timeframe bar, never the one in progress. Mixing these up is the whole bug. BarsInProgress is your synchronization primitive; treat every unguarded OnBarUpdate body as suspect.
The reliable pattern: reference the secondary series with Closes[1][0] and trust that, on a primary-series call, [0] on the secondary is the last closed higher-timeframe bar. NinjaTrader synchronizes the secondary index so that Closes[1][0] from within BarsInProgress == 0 does not include the still-forming higher-timeframe bar — provided you do not force it otherwise.
// CORRECT: primary logic reads the last CLOSED 60-min close
protected override void OnBarUpdate()
{
if (BarsInProgress != 0) return;
if (CurrentBars[0] < 1 || CurrentBars[1] < 1) return;
double htfClose = Closes[1][0]; // last closed 60-min bar
if (Close[0] > htfClose)
EnterLong();
}CurrentBars[i] guards each series independently — you must confirm both have enough bars before indexing, or early primary bars will read into an empty higher-timeframe series. A common explicit-safety variant captures the value when the higher-timeframe bar closes and stores it:
private double lastHtfClose = double.NaN;
protected override void OnBarUpdate()
{
if (BarsInProgress == 1)
lastHtfClose = Closes[1][0]; // confirmed on close
else if (BarsInProgress == 0)
{
if (double.IsNaN(lastHtfClose)) return;
if (Close[0] > lastHtfClose)
EnterLong();
}
}This makes the causality explicit: the primary series can only ever act on a higher-timeframe value that was already sealed.
Four recur constantly.
Forgetting the BarsInProgress gate, so primary logic executes on every secondary tick and fires orders at the wrong times.
Indexing the wrong series accessor — using Close[0] (primary) when you meant Closes[1][0] (secondary), or the reverse. The names are one character apart and the compiler will not save you.
Assuming Closes[1][0] is the forming higher-timeframe bar and building logic that only makes sense with that assumption — then getting a fill in live trading at a different value than the backtest showed. If the numbers agree historically but diverge live, an unsynced higher-timeframe read is the first place to look. That divergence is the subject of [Why Your NinjaTrader Backtest Doesn't Match Live](/ninjatrader-backtest-vs-live).
Placing orders from the secondary BarsInProgress. Entry and exit calls route to the series named by their index parameter; submitting from BarsInProgress == 1 without specifying the primary index can fill against the wrong series. Keep order submission in the primary block unless you deliberately intend otherwise.
// WRONG: reading a value that only "exists" once the 60-min bar closes, // but using it on primary bars mid-formation without syncing if (Closes[1][0] > Opens[1][0]) EnterLong(); // no BarsInProgress gate
Start with structure. Every OnBarUpdate must branch on BarsInProgress before touching any series, and every secondary index must be guarded by CurrentBars[i]. If either is missing, fix that before reading a single result.
Then test the values directly. Print Time[0], Times[1][0], and Closes[1][0] on primary bars and confirm the higher-timeframe timestamp is always earlier than or equal to the primary timestamp — never later. A Times[1][0] in the future relative to Time[0] is a caught leak.
if (BarsInProgress == 0 && CurrentBars[1] >= 0)
Print($"{Time[0]} htfBarTime={Times[1][0]} htfClose={Closes[1][0]}");Finally, run the strategy with and without Tick Replay, and compare against live/sim. A multi-series strategy that is correctly synced should not swing wildly between those modes for the same reason the single-series version should not. The free [/ninjascript validator](/ninjascript) statically flags unsynced secondary-series reads and missing BarsInProgress gates, which is faster than tracing every accessor by hand. As with everything on this site, this is educational material about writing correct code — it makes no claim that any synchronized strategy will be profitable.
Educational only — not financial advice. Trading involves substantial risk of loss.