NinjaScript 8 – Calculating Historical Bid/Ask Delta Per Bar Without Order Flow+

I’m looking for guidance on the most reliable NinjaScript approach to calculate bid/ask volume delta for individual candles, including historical data, without relying on Order Flow+ Volumetric Bars.

My objective is to develop a lightweight indicator that calculates:

  • Total volume traded at the bid (aggressive selling).

  • Total volume traded at the ask (aggressive buying).

  • Bar Delta = Ask Volume − Bid Volume.

  • Absolute delta and average absolute delta across a configurable number of bars.

  • Ideally, bid/ask volume at individual price levels within each candle.

I’m primarily using 40-range NQ/MNQ charts and would eventually like to calculate these statistics across specific historical time windows (e.g., the NY opening session) over 10–20 trading days.

I would appreciate clarification on the following:

  1. Historical data access: Can OnMarketData() with Tick Replay reliably provide historical Last trades together with their corresponding bid/ask prices for delta classification? Are there limitations depending on the data provider?

  2. Alternative methods: Would adding a secondary 1-tick series via AddDataSeries() be a viable alternative, or would that only provide trade prices and volumes without historical bid/ask quotes?

  3. Per-bar aggregation: What is the recommended way to aggregate classified trades into the exact boundaries of a primary 40-range bar series, including during rapid bar formation?

  4. Historical loading and performance: Is there a supported way for an indicator to process 10–20 days of historical tick data while the primary chart loads only one day, or would a separate data-processing indicator/chart be necessary?

  5. Alternative APIs: Are there any other supported NinjaScript methods for retrieving historical bid/ask traded volume or volume-at-price data without an Order Flow+ licence?

Ultimately, I’d like to calculate rolling session statistics such as average absolute delta and the 95th percentile of absolute delta, without repeatedly processing large amounts of historical Tick Replay data on my main trading chart.

Any recommendations regarding the most efficient and accurate architecture would be appreciated, particularly the limitations of historical bid/ask classification.

Thanks!

Short version: Tick Replay with OnMarketData is the documented route, and what it gives you is the bid and ask price stamped on each trade, not bid/ask volume. The built-in Buy Sell Volume indicator does exactly this classification, so its source (NinjaScript Editor, Indicators folder) is a working template. Taking your points in turn:

1. Tick Replay and OnMarketData. Yes, within two limits. Tick Replay replays Last events only, and each one carries e.Bid and e.Ask as they stood when the trade printed (Developing for Tick Replay). Classify the way Buy Sell Volume does: e.Price >= e.Ask is a buy, e.Price <= e.Bid a sell, a trade in between is ignored. The limits, both from that page: you only get as many days as your provider lets you download tick data for, and where no bid/ask was stored with a tick NinjaTrader fills in Bid = Last and Ask = Bid + 1 tick. Under that substitution every trade classifies as a sell, so a delta that never goes positive means your history carries no stamps. The same page points to the help guide’s data-by-provider table for which feeds support Tick Replay. To switch it on: Tools, Options, Market Data, Show Tick Replay, then tick Tick Replay on the chart’s primary Data Series.

2. A 1-tick secondary series. AddDataSeries(BarsPeriodType.Tick, 1) gives you trade price and volume in OnBarUpdate, and the Bars object also has GetBid(index) and GetAsk(index), which the docs say return expected values “when 1 tick bid / ask stamped data is used and available from your provider” (GetBid). So BarsArray[1].GetBid(CurrentBars[1]) and the matching GetAsk are a route without Tick Replay; print a few against the chart before you rely on them. Not the same thing: a series added with MarketDataType.Bid or Ask holds every quote change, not the quote at the trade, and it cannot be combined with Tick Replay (Using Historical Bid/Ask Series).

3. Exact bar boundaries. With Tick Replay on the 40-range primary series, the Tick Replay page guarantees the stored sequence, with OnMarketData called after every OnBarUpdate that builds the current bar, so when you classify a trade it is already in the bar it belongs to. Use Calculate.OnEachTick (the Buy Sell Volume page says it requires it): add to running totals in OnMarketData, and in OnBarUpdate, when CurrentBar changes, write the totals to the bar just closed and reset. Key everything on the bar index, never on time: in a fast open several 40-range bars can share a timestamp. On the 1-tick secondary route you match ticks to primary bars by time, so a tick that shares its time with a bar’s close is ambiguous; if you go that way and need exact boundaries, build the 40-range bars yourself from the ticks.

4. 10 to 20 days without loading the trading chart. Tick Replay pushes every tick of every loaded day through your code, so keep it off the chart you trade from. Two ways:

  • A second chart with 20 days loaded and Tick Replay on, running a stats-only indicator that writes each session’s per-bar deltas, or the finished statistics, to a file. The indicator on your trading chart reads the file and does no tick work.
  • A BarsRequest from the indicator for whole trading days of 1-tick data, independent of the chart’s own range (BarsRequest), reading the stamps with GetBid and GetAsk as in point 2. It is not synchronised with the chart’s bars, so it suits session statistics better than plotting per bar; request once, then unsubscribe and dispose.

Either way the statistics are the easy part: collect the absolute delta of each bar that closes inside your window (09:30 to 10:00 New York, say), then take the mean and the 95th percentile from the sorted list.

5. Other APIs without Order Flow+. The documented ones are those above: Buy Sell Volume and Buy Sell Pressure (per-bar buys and sells, Tick Replay for history), Bars.GetBid and GetAsk on 1-tick data, and BarsRequest for ticks outside the chart’s range. I know of no documented call for bid/ask volume at each price outside Volumetric bars, but it falls out of the same loop: keep a dictionary per bar keyed by price, with an at-bid and an at-ask total.

One limit on accuracy: the classification is only as good as the quote your provider stamped on each trade, and trades inside the spread are dropped, so expect small differences from another tool’s delta on the same session.

If it helps, I can post a compact indicator for the Tick Replay route: per-bar buys, sells and delta, plus the average and 95th percentile of absolute delta for a session window.

Thanks very useful - think I have a roadmap - I have data access trhough Apex CME subscription at the moment so not sure what that will mean on accuracy. I wouldnt mind an example to get started so I can see how is all wrapped around . Also am tryong to understand if I would get any more advanatges by using the orderflow+ volumetric bars apart from coding simplification.

Glad it helps. Your three points:

Your Apex data. Check which provider your Apex connection uses (Connections, Configure). If it is Rithmic, the help guide’s data-by-provider table marks Rithmic YES for historical tick data, Tick Replay and bid/ask stamped tick data, so the classification gets real quotes rather than the substitution. Two things only your own history can tell you: how far back the tick data actually starts (load the days you want and look at the first bar), and whether the stamps are really there, which the indicator below checks for you.

What Volumetric bars add besides simpler code. From the Volumetric Bars and AddVolumetric pages:

  • Bid, ask and delta at every price, ready-made: GetBidVolumeForPrice, GetAskVolumeForPrice, GetDeltaForPrice, plus MaxSeenDelta and MinSeenDelta within the bar and a CumulativeDelta that resets at the session break.
  • The numbers live in the bar type, and you read them in OnBarUpdate with no OnMarketData pass of your own.
  • AddVolumetric adds them as a secondary series, which suits a multi-series script; the Tick Replay page says Tick Replay is not compatible with most of those.
  • A size filter (R17 and later) and an UpDownTick delta type beside BidAsk.
  • The footprint display itself.

What I would not expect is a more accurate delta: the BidAsk type classifies trades at the ask or at the bid, the same rule as here, and it can only work from the history your provider sends. So it comes down to the licence against your time and how much of the per-price detail you need.

The example. In the NinjaScript Editor (Control Center, New, NinjaScript Editor), right-click the Indicators folder, choose New Indicator, name it BarDeltaStats and finish the wizard; select everything in the new file, paste this over it and press F5. Put it on your 40-range chart with Tick Replay ticked on the Data Series and as many days loaded as you want in the statistics. It plots each bar’s delta and the average absolute delta over the last N bars, and once history has loaded it prints to the Output window the mean and the 95th percentile of absolute delta for the bars stamped inside your window, across every day loaded. Times are in your NinjaTrader time zone (Tools, Options, General), so 93000 to 100000 is the New York open only if that zone is Eastern.

#region Using declarations
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Windows.Media;
using System.Xml.Serialization;
using NinjaTrader.Cbi;
using NinjaTrader.Data;
using NinjaTrader.Gui;
using NinjaTrader.NinjaScript;
#endregion

namespace NinjaTrader.NinjaScript.Indicators
{
	// Per-bar delta from Tick Replay, classified like Buy Sell Volume: at the ask or above a buy, at the bid or below a sell.
	public class BarDeltaStats : Indicator
	{
		private double buys, sells;                  // the bar being built
		private int activeBar = -1;
		private double totalBuys, totalSells;        // everything loaded, for the missing-stamps check
		private readonly Queue<double> recentAbs = new Queue<double>();
		private double recentAbsSum;
		private readonly List<double> windowAbs = new List<double>();   // closed bars stamped inside the window

		protected override void OnStateChange()
		{
			if (State == State.SetDefaults)
			{
				Name = "BarDeltaStats";
				Calculate = Calculate.OnEachTick;    // the bar boundary logic below needs OnBarUpdate on every tick
				IsSuspendedWhileInactive = false;    // a background tab must not pause OnBarUpdate while trades keep arriving
				IsOverlay = false;
				AvgPeriod = 20;
				WindowStart = 93000;                 // HHmmss in your NinjaTrader time zone
				WindowEnd = 100000;
				AddPlot(new Stroke(Brushes.DodgerBlue, 3), PlotStyle.Bar, "Delta");
				AddPlot(Brushes.Orange, "AvgAbsDelta");
				AddLine(Brushes.Gray, 0, "Zero");
			}
			else if (State == State.DataLoaded)
			{
				if (!Bars.IsTickReplay)
					Print(Name + ": Tick Replay is off on this chart, so historical bars get no delta");
				if (Calculate != Calculate.OnEachTick)
					Print(Name + ": set Calculate to On each tick, or trades land in the wrong bar");
			}
			else if (State == State.Realtime)
				PrintStats();
		}

		protected override void OnBarUpdate()
		{
			if (CurrentBar == activeBar)
				return;

			// First tick of a new bar. OnMarketData for this tick runs after this call,
			// so the running totals still hold exactly the bar that just closed.
			if (activeBar >= 0)
				CloseBar(CurrentBar - activeBar);    // bars ago of the closed bar, normally 1

			buys = 0;
			sells = 0;
			activeBar = CurrentBar;
		}

		protected override void OnMarketData(MarketDataEventArgs e)
		{
			if (e.MarketDataType != MarketDataType.Last || activeBar < 0)
				return;

			if (e.Price >= e.Ask)
			{
				buys += e.Volume;
				totalBuys += e.Volume;
			}
			else if (e.Price <= e.Bid)
			{
				sells += e.Volume;
				totalSells += e.Volume;
			}
			// a trade between the bid and the ask counts on neither side

			Delta[0] = buys - sells;                 // the bar still forming
		}

		private void CloseBar(int ago)
		{
			double delta = buys - sells;
			double abs = Math.Abs(delta);
			Delta[ago] = delta;

			recentAbs.Enqueue(abs);
			recentAbsSum += abs;
			if (recentAbs.Count > AvgPeriod)
				recentAbsSum -= recentAbs.Dequeue();
			AvgAbsDelta[ago] = recentAbsSum / recentAbs.Count;

			int t = ToTime(Time[ago]);
			if (t >= WindowStart && t < WindowEnd)
				windowAbs.Add(abs);
		}

		private void PrintStats()
		{
			if (totalSells > 0 && totalBuys == 0)
				Print(Name + ": no volume at the ask in all the history loaded, so the ticks probably carry no bid/ask stamps");

			if (windowAbs.Count == 0)
			{
				Print(Name + ": no closed bars inside the window in the history loaded");
				return;
			}

			List<double> sorted = windowAbs.OrderBy(x => x).ToList();
			int rank = (int)Math.Ceiling(0.95 * sorted.Count);   // nearest-rank 95th percentile
			Print(string.Format("{0}: {1} bars from {2} to {3}, mean |delta| {4:0.0}, 95th percentile {5:0.0}",
				Name, sorted.Count, WindowStart, WindowEnd, sorted.Average(), sorted[rank - 1]));
		}

		#region Properties
		[Browsable(false)]
		[XmlIgnore]
		public Series<double> Delta
		{
			get { return Values[0]; }
		}

		[Browsable(false)]
		[XmlIgnore]
		public Series<double> AvgAbsDelta
		{
			get { return Values[1]; }
		}

		[NinjaScriptProperty]
		[Range(1, int.MaxValue)]
		[Display(Name = "Average period (bars)", Order = 1, GroupName = "Parameters")]
		public int AvgPeriod { get; set; }

		[NinjaScriptProperty]
		[Range(0, 235959)]
		[Display(Name = "Window start (HHmmss)", Order = 2, GroupName = "Parameters")]
		public int WindowStart { get; set; }

		[NinjaScriptProperty]
		[Range(0, 235959)]
		[Display(Name = "Window end (HHmmss)", Order = 3, GroupName = "Parameters")]
		public int WindowEnd { get; set; }
		#endregion
	}
}

How it hangs together: for every tick NinjaTrader calls OnBarUpdate first and OnMarketData second, so when OnBarUpdate sees a new bar index, every trade of the bar before has already been counted and its totals are final. That is the whole boundary logic, and it is why Calculate has to stay On each tick: on bar close, OnBarUpdate stops marking where one bar ends and the next begins. If the Output says there is no volume at the ask in the whole history, your ticks came without bid/ask stamps and NinjaTrader’s substitute quotes turned every trade into a sell. Two next steps when you get there: bid and ask volume per price is a dictionary keyed by price, filled in the same OnMarketData lines and cleared with the totals; and for the trading chart, write the statistics to a file instead of printing them, so that chart reads one line and never replays ticks.

1 Like

One additional point I would test before relying on historical delta statistics is whether the historical trade classification is actually consistent across the entire sample.

The implementation discussed above is a useful starting point, but I would separate three questions:

Data availability: Does the connection provide historical Last trades with meaningful bid/ask context for the dates being tested?

Classification quality: What percentage of traded volume is classified at the bid, at the ask, or remains unclassified? A historical feed can produce plausible-looking delta values without necessarily reproducing the original trade-side information accurately.

Reconstruction consistency: If you calculate delta during a live session, save the results, and reload that same session later, do the completed bars produce identical values?

For your NQ/MNQ range-bar use case, I would pay particular attention to the last test. Rapidly forming range bars can expose boundary and synchronization differences that are less obvious on time-based charts.

There is also an important distinction between calculating total bar delta and constructing a footprint or bid/ask volume-at-price distribution. The latter needs reliable classification at each individual price level, not just an aggregated delta value for the candle.

I would validate two or three completed sessions first, including the New York open, before extending the calculation to 20 days.

That gives you a much better basis for deciding whether a custom implementation meets your requirements or whether the built-in Volumetric Bars provide enough additional value to justify using them.

Thanks for the earlier guidance. I implemented a prototype using Tick Replay and OnMarketData on native MNQ 40-range bars, and have now compared live results with the same candles after Reload All Historical Data.

My feed is Apex/Rithmic. The objective is a dashboard calculating average absolute bar delta and percentiles over the previous 5/10/20 sessions within configurable New York windows. My trading chart loads only one (1) day because I have indicators making longer Tick Replay loads expensive.

The implementation counts Last events only, using the bid/ask supplied with each event:

  • Price >= Ask: buy.

  • Price <= Bid: sell.

  • Inside-spread trades: unclassified.

  • Locked/crossed or nonfinite quotes: tracked separately as unusable.

Bars are assigned by primary bar index, not timestamp. Calculate is OnEachTick. Completed-bar trade volume is reconciled against primary chart volume. Estimated classifications are not included.

1. Small differences between live and reloaded delta

I did a test during a NY-opening-period comparison covering 81 completed candles around 09:38–09:44:

  • Delta matched exactly on 39 candles.

  • Mean absolute difference was 2.35 delta units; maximum difference was 24.

  • Average absolute delta changed from 166.54 to 164.52.

Some trades unclassified live became classified after reloading. W

Are small differences like these expected between live Rithmic data and historical reloads? In particular:

  • Do e.Bid/e.Ask on live Last events and Tick Replay historical Last events have identical quote-association semantics?

  • Could historical storage precision, trade aggregation or bid/ask association explain differences despite identical OHLCV?

  • How does NinjaTrader’s own delta classification handle locked/crossed quotes and inside-spread trades?

  • What additional logging would help distinguish implementation errors from differences in the underlying data?

An earlier test also showed a 17-contract shortfall in one historical/live transition candle. Later tests did not reproduce it. Is there a recommended transition-handling pattern to investigate this?

2. Would Order Flow+ Volumetric bars improve consistency?

If I used BidAsk Volumetric bars with a 40-range base, no size filter and otherwise matching settings:

  • Would they use the same underlying historical trade-time bid/ask data as this Tick Replay implementation?

  • Would their live-versus-reloaded results be expected to match more closely?

  • Are there differences in classification or bar-boundary handling that could explain improved consistency?

I am trying to distinguish simpler access to precomputed values from genuinely different data or calculation behaviour. I do not assume that purchasing Order Flow+ would eliminate these discrepancies.

3. Historical backfill without loading a separate chart

The current solution uses a separate plain 40-range Tick Replay chart to build a persistent per-session cache. My one-day trading chart reads that cache.

What supported architecture would let an indicator or AddOn retrieve and update the previous 20 sessions without increasing the trading chart’s history or keeping a separate visible chart open?

Would BarsRequest be suitable? The critical requirement is reproducing the exact native 40-range bars, including several bars sharing timestamps, rather than ambiguously matching independent ticks to range bars by time.

Can a supported background request provide native range bars together with enough trade-time bid/ask information to calculate their exact deltas? Would Order Flow+ provide another supported route?

I’m curious. is this directly from your deep understanding or is it based on AI replies, if so please ask your AI to summarize lol, too much spam.

I had some specific questions I couldn’t actually solve entirely with AI also because am more interested in people experience with the topic … so yes sorry if this feels spamming but rather have exaustive questions than an unuseful summary !

You can see the code/logic in the my post here