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.