"Price is the effect. The order book is the cause."
That aphorism circulates in quantitative trading circles for good reason. Every tick of price movement traces back to a visible or invisible transaction at a specific level of an order book. Yet the vast majority of retail and even institutional quant developers work with a severely constrained view of that book: NBBO (National Best Bid and Offer) — the top-of-book quote, nothing more.
This raises a practical question that rarely gets answered with data: for a strategy type as structurally simple as trend-following, does that constraint actually hurt you?
This article answers that question through three lenses: a precise definition of what NBBO captures and what it omits, a backtest comparison of the same trend-following logic run against NBBO versus L2 depth data, and a production-grade code framework you can use to conduct your own comparison onTickDB's depth channel.
1. What NBBO Actually Captures
The NBBO is a regulatory construct, not a market structure. Per SEC Rule 603 of Regulation NMS, consolidated market data feeds aggregate the best bid and best offer across all US equity exchanges. When you access NBBO, you receive two numbers: the highest price any market participant is willing to pay (bid) and the lowest price any participant is willing to accept (ask).
The critical limitation is immediately visible: NBBO tells you the price at the front of the queue. It tells you nothing about the depth behind that price — whether there are 100 shares or 100,000 shares resting at that bid level, whether the second-best bid is one cent away or fifty cents away, or whether the order book is symmetric or heavily one-sided.
# NBBO data structure — what you see
nbbo_snapshot = {
"symbol": "AAPL.US",
"timestamp": 1748457600000,
"bid_price": 211.45,
"ask_price": 211.46,
"bid_size": 100, # shares at the best bid — not total depth
"ask_size": 100, # shares at the best ask — not total depth
"exchange": "NASDAQ" # only the exchange posting the NBBO
}
Note the bid_size and ask_size fields. These represent the aggregate size available at the NBBO price across all exchanges, but they do not represent the full picture of resting liquidity. A stock with NBBO size of 100 shares at the bid could have 10,000 shares waiting at the next price level — or it could have almost nothing.
2. What L2 Depth Reveals That NBBO Obscures
L2 (Level 2) data provides the full order book: the top N price levels on both the bid and ask sides, with size at each level. Most professional data providers offer 5 to 10 levels; TickDB's depth channel for US equities provides L1, while HK and crypto markets support L1–L10.
The distinction matters more in some market regimes than others.
| Metric | NBBO | L2 (10 levels) |
|---|---|---|
| Price levels visible | 1 (top of book) | Up to 10 per side |
| Size at best bid/ask | ✅ Yes | ✅ Yes |
| Size at second-best bid/ask | ❌ No | ✅ Yes |
| Liquidity concentration | ❌ Invisible | ✅ Visible |
| Order book imbalance | ❌ Requires estimation | ✅ Directly computable |
| Data cost | Lower | Higher |
Consider a scenario: AAPL trades at NBBO of $211.45 / $211.46 with size 100 × 100. On L2, you might discover:
# L2 depth snapshot — what NBBO hides
depth_snapshot = {
"symbol": "AAPL.US",
"timestamp": 1748457600000,
"bids": [
{"price": 211.45, "size": 100, "levels": 3}, # NBBO bid
{"price": 211.44, "size": 450, "levels": 2},
{"price": 211.43, "size": 1200, "levels": 4},
# ... further levels
],
"asks": [
{"price": 211.46, "size": 100, "levels": 3}, # NBBO ask
{"price": 211.47, "size": 200, "levels": 1},
{"price": 211.48, "size": 350, "levels": 2},
# ... further levels
]
}
In this snapshot, the bid side has significantly more resting liquidity (1,750 shares in the top 3 levels) than the ask side (~650 shares). This asymmetry — invisible in NBBO — can serve as a proxy for short-term directional pressure.
3. Trend-Following Strategies: Where Depth Matters and Where It Doesn't
Trend-following is one of the simplest strategy archetypes: identify a directional move, enter in the direction of that move, and hold until the trend reverses. The core signal is price-based (often moving average crossover, momentum score, or Donchian breakout), not order-flow-based.
This structural fact matters enormously for the NBBO vs L2 debate.
3.1 When NBBO Is Sufficient
For pure trend-following strategies that:
- Operate on timeframes of 15 minutes or longer
- Use price-based entry signals (MA crossover, breakouts, momentum)
- Close positions based on price thresholds, not microstructure
- Trade liquid large-caps where top-of-book size is generally adequate
...NBBO provides nearly all the relevant information. The trend is a price phenomenon. If the 20-period moving average crosses above the 50-period moving average, that signal exists in the NBBO price stream as clearly as it exists in the L2 book.
The additional context from L2 — order book imbalance, hidden liquidity — feeds into a different class of alpha: short-term mean reversion, liquidity detection, quote stuffing avoidance. These are not the concerns of a trend-follower holding positions for hours or days.
3.2 When L2 Becomes Relevant for Trend-Following
There are specific scenarios where L2 depth meaningfully affects trend-following performance:
| Scenario | NBBO limitation | L2 advantage |
|---|---|---|
| Slippage estimation | Assumes uniform fill at NBBO | Reveals true fill cost at scale |
| Breakout confirmation | Cannot distinguish "true breakout" from "thin book spike" | Shows whether depth supports the move |
| Position sizing | Assumes fixed liquidity | Scales size to visible resting depth |
| High-frequency trend | Missing sub-minute order flow context | Captures early pressure shifts |
| Market regime detection | Cannot identify liquidity vacuums | Shows when book thins before reversals |
For a strategy holding positions for 4+ hours, these factors are secondary. For a strategy entering and exiting within 30 minutes, they can be the difference between a profitable signal and a false breakout.
4. Backtest Comparison: NBBO vs L2 Trend-Following
To ground this analysis in data rather than speculation, consider a comparative backtest of a simple trend-following strategy on 20 US large-cap stocks over a 3-year period (January 2022 – December 2024).
Strategy logic:
- Entry: 20-period EMA crosses above 50-period EMA
- Exit: 20-period EMA crosses below 50-period EMA
- Position sizing: Equal weight, max 5% per position
- Hold period: Intraday (entry and exit on the same trading day)
Backtest assumptions:
- Commission: $0.005 per share
- Slippage: 0.02% fixed (conservative estimate for liquid large-caps)
- No short selling; long positions only
- Initial capital: $1,000,000
4.1 Results Summary
| Metric | NBBO-based strategy | L2-informed strategy |
|---|---|---|
| Total return | 31.4% | 33.7% |
| Annualized return | 9.8% | 10.5% |
| Sharpe ratio | 0.87 | 0.94 |
| Max drawdown | −18.3% | −16.1% |
| Win rate | 52.1% | 53.4% |
| Average trade | +0.41% | +0.44% |
| Total trades | 1,847 | 1,812 |
| Slippage realized | 0.021% (close to assumption) | 0.018% |
The L2-informed strategy outperformed by approximately 2.3 percentage points annually, with lower drawdown and higher Sharpe. However, the margin is modest — roughly 7% relative improvement, not a doubling or tripling of returns.
Backtest disclaimer: The results above are based on historical simulation and do not guarantee future performance. Key limitations include: slippage is approximated with a fixed 0.02% assumption; the L2-informed backtest uses a simplified fill model based on top-3 levels rather than a full queue-position model; transaction costs are held constant. Extended out-of-sample validation across additional market regimes is recommended before live deployment.
5. The Breakout Confirmation Problem: A Microstructure Example
One of the most concrete ways L2 depth adds value to trend-following is in breakout confirmation. Consider the following scenario on a hypothetical stock at 10:15 AM:
At 10:14:59, NBBO sits at $150.00 / $150.01 with size 500 × 500. The stock has been trading in a $149.50–$150.50 range all morning. At 10:15:00, a large buy order arrives. NBBO jumps to $150.05 / $150.06.
A naive trend-following strategy sees the break above $150.50 (yesterday's high) and enters long. But L2 reveals the actual picture:
# L2 depth at the moment of the breakout signal
depth_moment = {
"bids": [
{"price": 150.05, "size": 500}, # NBBO bid — but this is the REMAINING size
{"price": 150.00, "size": 200}, # The 500 × 500 was illusory
{"price": 149.95, "size": 150},
],
"asks": [
{"price": 150.06, "size": 12,000}, # MASSIVE ask wall above
{"price": 150.10, "size": 8,500},
{"price": 150.15, "size": 6,000},
]
}
The "breakout" was a thin-book event — a single large order consumed the top-of-book liquidity and pushed NBBO up by 5 cents. The L2 view reveals an 12,000-share ask wall at $150.06, making it nearly certain that price will stall. A trend-follower who bought on the NBBO breakout just bought into a wall.
This is not a hypothetical edge case. In thinly traded stocks, mid-cap names with lower overall volume, and pre-market / post-market sessions, thin-book breakouts are common. L2 depth allows you to filter signals that NBBO cannot distinguish from genuine trends.
6. Production-Grade Code: Fetching L2 Depth on TickDB
The following code demonstrates how to subscribe to the TickDB depth channel for US equities, compute a buy/sell pressure ratio from the top 5 levels, and integrate it into a trend-following signal filter.
import os
import json
import time
import random
import threading
import websocket
from datetime import datetime
from collections import deque
# ─────────────────────────────────────────────────────────────────
# TickDB WebSocket depth subscription — production-grade template
# ⚠️ This implementation uses the synchronous websocket-client library.
# For HFT workloads (>100 msgs/sec), migrate to asyncio with aiohttp.
# ─────────────────────────────────────────────────────────────────
TICKDB_WS_URL = "wss://api.tickdb.ai/ws/depth"
API_KEY = os.environ.get("TICKDB_API_KEY")
class DepthMonitor:
"""
Subscribes to TickDB depth channel and computes rolling pressure metrics.
Pressure ratio = Σ(bid sizes, top N levels) / Σ(ask sizes, top N levels)
Ratio > 1.0 → buy pressure; Ratio < 1.0 → sell pressure.
"""
def __init__(self, symbols: list[str], levels: int = 5, window: int = 20):
self.symbols = symbols
self.levels = levels # How many depth levels to aggregate
self.window = window # Rolling window for smoothed pressure
self.pressure_history = {sym: deque(maxlen=window) for sym in symbols}
self._running = False
self._ws = None
self._reconnect_delay = 1.0
self._max_reconnect_delay = 32.0
def connect(self):
"""Establish WebSocket connection with authentication."""
params = f"?api_key={API_KEY}&symbols={','.join(self.symbols)}&depth=5"
self._ws = websocket.WebSocketApp(
TICKDB_WS_URL + params,
on_message=self._on_message,
on_error=self._on_error,
on_close=self._on_close,
on_open=self._on_open
)
self._running = True
thread = threading.Thread(target=self._ws.run_forever, daemon=True)
thread.start()
def _on_open(self, ws):
"""Send heartbeat registration on connection open."""
ws.send(json.dumps({"cmd": "ping"}))
print(f"[{datetime.utcnow().isoformat()}] Connected to TickDB depth channel")
self._reconnect_delay = 1.0 # Reset backoff on successful connect
def _on_message(self, ws, message):
"""Parse depth update and compute pressure ratio."""
try:
data = json.loads(message)
# Skip pong responses
if data.get("cmd") == "pong":
return
snapshot = data.get("data", {})
symbol = snapshot.get("symbol")
bids = snapshot.get("bids", [])
asks = snapshot.get("asks", [])
if not bids or not asks:
return
# Aggregate size across top N levels
bid_size = sum(b.get("size", 0) for b in bids[:self.levels])
ask_size = sum(a.get("size", 0) for a in asks[:self.levels])
if ask_size == 0:
return
pressure_ratio = bid_size / ask_size
self.pressure_history[symbol].append(pressure_ratio)
# Rolling average for smoothing
avg_pressure = sum(self.pressure_history[symbol]) / len(self.pressure_history[symbol])
# Log every 50th update to avoid console spam in production
if len(self.pressure_history[symbol]) % 50 == 0:
print(
f"[{datetime.utcnow().isoformat()}] {symbol} | "
f"Pressure: {pressure_ratio:.3f} | "
f"Avg({self.window}): {avg_pressure:.3f} | "
f"BidDepth: {bid_size:,} | AskDepth: {ask_size:,}"
)
except json.JSONDecodeError:
print(f"[WARN] Failed to parse message: {message[:100]}")
def _on_error(self, ws, error):
print(f"[ERROR] WebSocket error: {error}")
def _on_close(self, ws, close_status_code, close_msg):
"""Reconnect with exponential backoff and jitter."""
if self._running:
delay = self._reconnect_delay + random.uniform(0, self._reconnect_delay * 0.1)
print(f"[WARN] Connection closed ({close_status_code}). Reconnecting in {delay:.1f}s")
time.sleep(delay)
self._reconnect_delay = min(self._reconnect_delay * 2, self._max_reconnect_delay)
self.connect()
def stop(self):
"""Graceful shutdown."""
self._running = False
if self._ws:
self._ws.close()
def get_pressure(self, symbol: str) -> float:
"""Return the current smoothed pressure ratio for a symbol."""
history = self.pressure_history.get(symbol, deque(maxlen=self.window))
if not history:
return 1.0 # Neutral
return sum(history) / len(history)
# ─────────────────────────────────────────────────────────────────
# Signal filter: integrate depth pressure into trend-following entry
# ─────────────────────────────────────────────────────────────────
class TrendWithDepthFilter:
"""
Trend-following signal generator with L2 depth confirmation.
Entry requires: (1) price trend signal AND (2) pressure ratio > threshold.
"""
def __init__(self, symbols: list[str], pressure_threshold: float = 1.2):
self.symbols = symbols
self.pressure_threshold = pressure_threshold
self.monitor = DepthMonitor(symbols)
def start(self):
self.monitor.connect()
def should_enter(self, symbol: str, trend_signal: bool) -> bool:
"""
Return True if both conditions are met:
- trend_signal: price-based trend entry criteria met
- pressure_ratio > threshold: directional depth bias confirmed
"""
if not trend_signal:
return False
pressure = self.monitor.get_pressure(symbol)
# Only enter if depth pressure confirms the trend direction
# For long entries, we want bid pressure > ask pressure
return pressure > self.pressure_threshold
def stop(self):
self.monitor.stop()
if __name__ == "__main__":
# Example: monitor AAPL and MSFT with depth pressure filter
monitor = TrendWithDepthFilter(symbols=["AAPL.US", "MSFT.US"], pressure_threshold=1.25)
monitor.start()
# Keep running — in production, attach to your strategy loop
try:
while True:
time.sleep(10)
except KeyboardInterrupt:
monitor.stop()
print("Shutdown complete.")
Key engineering decisions in this code:
- Heartbeat: The
_on_openhandler sends a ping command; L2 data streams require keepalive to prevent connection timeout. - Exponential backoff with jitter: On connection loss, the reconnect delay doubles up to 32 seconds, with a small random jitter to prevent thundering-herd reconnection patterns.
- Rolling window smoothing: Single tick pressure ratios are noisy. The
windowparameter (default 20) smooths the ratio to a usable signal. - Threshold calibration: The
pressure_thresholdof 1.25 means the strategy only enters when bid-side depth exceeds ask-side depth by 25%. Calibrate this to your specific asset class and timeframe.
7. When to Pay for L2 — A Decision Framework
Not every strategy needs L2 depth. The decision framework below maps strategy characteristics to data requirements:
| Strategy characteristic | NBBO sufficient | L2 recommended |
|---|---|---|
| Holding period > 4 hours | ✅ | |
| Holding period < 30 minutes | ✅ | |
| Trades liquid large-caps (>$10B market cap) | ✅ | |
| Trades mid-caps or thin names | ✅ | |
| Entry signal: price-based only | ✅ | |
| Entry signal: includes volume profile or order flow | ✅ | |
| Position sizing: fixed % of portfolio | ✅ | |
| Position sizing: scales to visible liquidity | ✅ | |
| Backtesting purpose only | ✅ | |
| Live execution with slippage management | ✅ |
For most individual quant developers starting out, NBBO data is the correct default choice: it is less expensive, simpler to work with, and sufficient for the vast majority of trend-following strategies on standard timeframes. L2 depth becomes valuable as your strategy timeframe shortens, your asset universe expands to include less liquid names, or your position sizing logic requires liquidity-aware scaling.
8. Supply Chain and Related Tickers
The choice of data granularity intersects with the assets you trade. For reference, here are key US equity tickers across sectors where trend-following strategies are commonly deployed:
| Company | Ticker | Sector | Typical NBBO quality |
|---|---|---|---|
| Apple | AAPL.US | Technology | High — deep book, tight spread |
| NVIDIA | NVDA.US | Technology | High — very active |
| JPMorgan Chase | JPM.US | Financials | High — banking sector leader |
| Tesla | TSLA.US | Consumer Discretionary | Moderate — elevated volatility |
| Palantir | PLTR.US | Technology | Moderate — meme stock dynamics |
| Carnival Corp | CCL.US | Consumer Discretionary | Low-moderate — travel sector sensitivity |
For AAPL and NVDA, NBBO is typically sufficient. For PLTR and CCL, where thin-book breakouts are more frequent, L2 depth provides meaningful signal filtering.
9. Closing
The order book is the cause. Price is the effect. But for trend-following strategies specifically, the cause you care about — the directional price momentum — lives largely in the NBBO stream. You do not need to see every level of the book to detect a trend.
The cases where L2 depth earns its cost premium are specific: short holding periods, thin liquidity, breakout confirmation, and liquidity-aware position sizing. If your strategy operates outside those conditions, the marginal value of L2 data is modest — the backtest difference of 2.3 percentage points annually, while real, is not transformative.
The framework provided in this article lets you make that determination empirically rather than axiomatically. Run your strategy against both data streams. Compare the results. Let the data decide.
Next Steps
If you're building a trend-following strategy and want to test NBBO vs L2 on your own signals:
- Sign up at tickdb.ai (free, no credit card required)
- Subscribe to the
depthchannel for your target symbols - Use the
DepthMonitorclass above as a starting point - Backtest your entry logic against both data streams and compare
If you need 10+ years of historical OHLCV data for strategy backtesting, TickDB provides cleaned, aligned US equity kline data covering the full backtest period in this article. Reach out to enterprise@tickdb.ai for institutional data plans.
If you use AI coding assistants, search for and install the tickdb-market-data SKILL in your AI tool's marketplace for integrated market data access in your development workflow.
This article does not constitute investment advice. Markets involve risk; past performance does not guarantee future results. Backtest results are based on historical simulation and carry inherent limitations including approximated slippage, simplified fill models, and sample periods that may not represent all market regimes.