NinjaScript OnMarketData and OnMarketDepth: Level 2 Guide
Category: Strategy Guides
Use NinjaScript OnMarketData and OnMarketDepth to classify trades, build a Level 2 order book, and handle Tick Replay limits in NinjaTrader 8.
Bars are a summary. Every candle on your chart compresses hundreds or thousands of individual trades and order book changes into four numbers. For most strategies that compression is fine. For order flow logic, it throws away exactly the information you need: who traded at the bid, who lifted the offer, and how much size is resting at each price.
NinjaScript exposes that raw stream through two event methods: OnMarketData() for Level 1 (trades, best bid, best ask) and OnMarketDepth() for Level 2 (the order book). This guide explains both, shows how to build your own book, and covers the historical-data limits that catch most developers. For the trading concepts behind depth, read our Level 2 market depth guide first.
Level 1 vs Level 2: what each method receives
| OnMarketData() | OnMarketDepth() | |
|---|---|---|
| Data | Level 1: last trade, best bid, best ask, daily stats | Level 2: resting size at multiple price levels |
| Argument | MarketDataEventArgs | MarketDepthEventArgs |
| Typical use | Delta, trade classification, bid/ask volume, spread tracking | Custom book, liquidity imbalance, iceberg detection, absorption |
| Historical calls | Only with Tick Replay enabled | Never in historical processing |
Both methods fire far more often than OnBarUpdate(). During active periods in NQ or ES, depth events can arrive hundreds of times per second. Write them as if performance matters, because it does.
OnMarketData(): the Level 1 stream
protected override void OnMarketData(MarketDataEventArgs e)
{
if (e.MarketDataType == MarketDataType.Last)
{
// a trade printed
double price = e.Price;
long size = e.Volume;
double bidNow = e.Bid; // best bid at time of trade
double askNow = e.Ask; // best ask at time of trade
}
else if (e.MarketDataType == MarketDataType.Bid)
{
// best bid changed (price or size)
}
else if (e.MarketDataType == MarketDataType.Ask)
{
// best ask changed
}
}
MarketDataType also includes values such as DailyHigh, DailyLow, DailyVolume, Opening, Settlement, and OpenInterest. Filter for what you need and return early on everything else.
Classifying trades: bid volume vs ask volume
The core building block of order flow is deciding whether each trade was buyer-initiated or seller-initiated. A simple rule using the bid and ask attached to a Last event:
private long askVol, bidVol;
protected override void OnMarketData(MarketDataEventArgs e)
{
if (e.MarketDataType != MarketDataType.Last)
return;
if (e.Price >= e.Ask)
askVol += e.Volume; // aggressive buyer lifted the offer
else if (e.Price <= e.Bid)
bidVol += e.Volume; // aggressive seller hit the bid
// trades between bid and ask are left unclassified
}
Reset the counters on each new bar (check IsFirstTickOfBar in OnBarUpdate()) and you have per-bar delta: askVol - bidVol. This is the same raw material behind the footprint charts discussed in our footprint chart guide.
Tick Replay: OnMarketData on historical data
By default, OnMarketData() does not run on historical bars. Your delta indicator starts at zero when the chart loads and only fills in from the live session. Enable Tick Replay on the data series and NinjaTrader replays historical trades through OnMarketData(), so historical bars show delta too.
Know the limits:
- Only Last events are replayed, each with the bid and ask that were current at that trade. Historical Bid and Ask change events are not.
- Tick data required: your data provider must supply historical tick data with bid/ask stamps. Our market data feed guide covers what different providers offer.
- Load time: replaying weeks of NQ ticks is slow. Load only as many days as the logic needs.
- Consistency: all indicators on the chart share one Tick Replay setting per series. Turning it on affects load time for everything on that chart.
OnMarketDepth(): the Level 2 stream
Every change to the visible order book arrives as a MarketDepthEventArgs:
| Property | Meaning |
|---|---|
MarketDataType | Ask or Bid side |
Operation | Add, Update, or Remove |
Position | Row index in the book, 0 = best price |
Price | Price of the row |
Volume | Resting size at that row |
IsReset | True after a disconnect: discard and rebuild the book |
The operations describe edits to a list, not a snapshot. Add inserts a new row at Position and pushes deeper rows down. Remove deletes the row at Position. Update changes price or size at Position in place. NinjaTrader delivers these in sequence, so applying them in order reproduces the book.
Building your own Level 2 book
private class Row { public double Price; public long Volume; }
private readonly List<Row> asks = new List<Row>();
private readonly List<Row> bids = new List<Row>();
private readonly object bookLock = new object();
protected override void OnMarketDepth(MarketDepthEventArgs e)
{
lock (bookLock)
{
if (e.IsReset) { asks.Clear(); bids.Clear(); return; }
List<Row> side = e.MarketDataType == MarketDataType.Ask ? asks : bids;
switch (e.Operation)
{
case Operation.Add:
if (e.Position <= side.Count)
side.Insert(e.Position, new Row { Price = e.Price, Volume = e.Volume });
break;
case Operation.Update:
if (e.Position < side.Count)
{
side[e.Position].Price = e.Price;
side[e.Position].Volume = e.Volume;
}
break;
case Operation.Remove:
if (e.Position < side.Count)
side.RemoveAt(e.Position);
break;
}
}
}
Three details matter. The bounds checks prevent exceptions if an event arrives out of the expected range after a reconnect. The lock protects the lists if you read them from OnBarUpdate() or a render method. And the reset branch clears stale data instead of mixing old and new books.
Using the book: a liquidity imbalance example
With a book in memory, you can measure things bars cannot show. A common metric is resting-size imbalance across the top levels:
private double TopImbalance(int levels)
{
lock (bookLock)
{
long b = 0, a = 0;
for (int i = 0; i < levels && i < bids.Count; i++) b += bids[i].Volume;
for (int i = 0; i < levels && i < asks.Count; i++) a += asks[i].Volume;
return (a + b) == 0 ? 0 : (double)(b - a) / (a + b);
}
}
The result ranges from -1 (all resting size on the offer) to +1 (all on the bid). Treat it as context, not a signal by itself. Resting orders can be pulled in milliseconds, and large displayed size is sometimes there precisely to be seen. Pair book data with actual traded volume from OnMarketData(): size that holds while being hit (absorption) means more than size that simply sits.
The historical testing problem
OnMarketDepth() is never called during historical processing, and the Strategy Analyzer has no Level 2 history to replay. A strategy whose entries depend on book data cannot be evaluated with a standard historical run. The options:
- Market Replay: NinjaTrader's Playback connection can replay recorded market data, including depth, for supported instruments and dates. This is the closest thing to a historical test for depth logic.
- Record your own: log book snapshots to a file in real time and analyze them offline.
- Forward test in simulation: run the strategy on a sim account for several weeks before risking capital.
Plan for this from the start. Depth-based logic takes longer to validate than bar-based logic, and that is a real cost.
Performance rules for high-frequency events
- Return early. Filter on
MarketDataTypeon the first line. - No Print() in production. Output window writes at hundreds of events per second slow everything.
- No allocations in the hot path where avoidable. Reuse objects instead of creating new ones per event.
- Keep heavy math out. Update counters in the event; compute derived metrics once per bar or on a timer.
- Do not submit orders on every event. Decide on a signal, then act once with clear state flags.
Data feed and connection requirements
Depth data must come from your connection. CME futures typically provide 10 levels per side through most retail feeds. Some connections deliver fewer, or none for certain instruments. Before building a depth strategy:
- Open a SuperDOM or depth ladder on the instrument and confirm multiple levels update in real time.
- Check whether your feed aggregates or throttles updates. Throttled feeds can hide fast changes.
- Test behavior during a disconnect and reconnect, including the
IsResetbranch.
Where these methods fit in a strategy
Most production strategies use event data as a filter rather than the core signal. A bar-based setup defines the trade; per-bar delta or book imbalance confirms or vetoes it. That structure keeps the strategy testable on bars with Tick Replay, while depth data adds a real-time layer you forward test separately. If you are building indicators first, our custom NinjaTrader indicators guide covers plots, properties, and rendering.
Common mistakes
- Expecting
OnMarketDepth()to run in the Strategy Analyzer. - Building delta without Tick Replay and wondering why history is flat.
- Treating
Updateas "set price X to size Y" instead of an edit at a row position. - Ignoring
IsResetand trading on a corrupt book after a reconnect. - Printing every event during live trading.
- Reading the book from another method without synchronization.
Combining OnMarketData with OnMarketDepth
The two streams are most useful together. Depth tells you what traders intend to do; trades tell you what they actually did. A few combined measures developers build:
- Absorption: heavy bid-side trade volume at a price while the resting bid size at that price stays large or refills. Sellers are hitting, but someone keeps absorbing.
- Pulling and stacking: resting size that disappears just before price reaches it (pulling), or grows as price approaches (stacking).
- Sweep detection: a single burst of trades that clears several book levels within milliseconds, often marking urgent aggressive orders.
The two methods do not fire in a fixed order relative to each other or to OnBarUpdate(). Store state from each, then evaluate the combined condition in one place, typically once per bar or on a short timer.
A testing workflow checklist
- Confirm your connection delivers depth for the instrument.
- Build the trade-classification layer and verify it with Tick Replay on a few days of history.
- Add the book and verify it against the SuperDOM by eye during a live session.
- Record or replay depth via Market Replay to check behavior across busy and quiet periods.
- Forward test in simulation, then go live at minimum size.
The bottom line
OnMarketData() gives you every trade and quote change; OnMarketDepth() gives you the order book. Together they let NinjaScript see what bars hide. Respect their limits: depth has no historical replay in the analyzer, Tick Replay only covers trades, and both methods demand efficient code.
Want order flow context on your trades without writing your own book? Explore NocNoe plans for automated algos, a futures trade journal, and AI coaching that reviews every execution.
NinjaTrader® is a registered trademark of NinjaTrader Group, LLC. No NinjaTrader company has any affiliation with the owner, developer, or provider of the products or services described herein, or any interest, ownership or otherwise, in any such product or service, or endorses, recommends or approves any such product or service.
Risk Disclosure: Futures and forex trading contains substantial risk and is not for every investor. An investor could potentially lose all or more than the initial investment. Risk capital is money that can be lost without jeopardizing ones' financial security or life style. Only risk capital should be used for trading and only those with sufficient risk capital should consider trading. Past performance is not necessarily indicative of future results.