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()
DataLevel 1: last trade, best bid, best ask, daily statsLevel 2: resting size at multiple price levels
ArgumentMarketDataEventArgsMarketDepthEventArgs
Typical useDelta, trade classification, bid/ask volume, spread trackingCustom book, liquidity imbalance, iceberg detection, absorption
Historical callsOnly with Tick Replay enabledNever 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:

OnMarketDepth(): the Level 2 stream

Every change to the visible order book arrives as a MarketDepthEventArgs:

PropertyMeaning
MarketDataTypeAsk or Bid side
OperationAdd, Update, or Remove
PositionRow index in the book, 0 = best price
PricePrice of the row
VolumeResting size at that row
IsResetTrue 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:

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

  1. Return early. Filter on MarketDataType on the first line.
  2. No Print() in production. Output window writes at hundreds of events per second slow everything.
  3. No allocations in the hot path where avoidable. Reuse objects instead of creating new ones per event.
  4. Keep heavy math out. Update counters in the event; compute derived metrics once per bar or on a timer.
  5. 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:

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

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:

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

  1. Confirm your connection delivers depth for the instrument.
  2. Build the trade-classification layer and verify it with Tick Replay on a few days of history.
  3. Add the book and verify it against the SuperDOM by eye during a live session.
  4. Record or replay depth via Market Replay to check behavior across busy and quiet periods.
  5. 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.