Orderflow Engine — Math and Implementation
1. Introduction to Orderflow Analysis
Orderflow is the study of the net flow of aggressive orders in the market. Unlike classic technical analysis (which only looks at OHLCV), orderflow splits volume into: - Buy volume (market BUY orders consuming liquidity on the ask side) - Sell volume (market SELL orders consuming liquidity on the bid side)
This distinction reveals who is aggressive and in which direction — information that candlesticks and classic indicators do not capture.
2. Delta and Cumulative Volume Delta (CVD)
2.1 Per-Candle Delta
A candle's Delta is the difference between aggressive buy and sell volume within the interval:
where:
- buy_volume_i = quantity_i if is_buyer_maker = False (BUY aggression)
- sell_volume_i = quantity_i if is_buyer_maker = True (SELL aggression)
Interpretation: - Delta > 0 → more volume bought aggressively → bullish pressure - Delta < 0 → more volume sold aggressively → bearish pressure - Delta = 0 → perfect balance (rare)
Percentage delta (normalized by total volume):
Delta% > 60% → strong buy dominance; Delta% < -60% → strong sell dominance.
2.2 Cumulative Volume Delta (CVD)
The CVD accumulates Delta over time, producing a curve that shows the net orderflow trend:
CVD is computed on multiple timeframes (1m, 5m, 15m) via sliding time windows:
# Pseudo-code (implementation in orderflow.py)
class Accumulator:
def __init__(self, window_sec: int):
self.window_sec = window_sec
self.buffer = deque() # (timestamp_ms, delta)
self.cvd = 0.0
def update(self, timestamp_ms: int, buy_vol: float, sell_vol: float):
delta = buy_vol - sell_vol
self.buffer.append((timestamp_ms, delta))
self.cvd += delta
# Drop events outside the window
cutoff = timestamp_ms - self.window_sec * 1000
while self.buffer and self.buffer[0][0] < cutoff:
_, old_delta = self.buffer.popleft()
self.cvd -= old_delta
Implemented timeframes:
- cvd_1m : CVD over the last minute (very short-term momentum signal)
- cvd_5m : CVD over the last 5 minutes (trend confirmation)
- cvd_15m: CVD over the last 15 minutes (structural context)
CVD/Price divergence — an important signal: - Price rises, CVD falls → the rally is not supported by flow → possible reversal - Price falls, CVD rises → the decline is not supported by flow → possible bounce
3. Volume Weighted Average Price (VWAP) and VWAP Z-Score
3.1 Intraday VWAP
The VWAP is the volume-weighted average price of the current session:
where i is every single trade. Implemented with running sums:
The VWAP is the institutional equilibrium price: large players (funds, prop desks) often use the VWAP as the benchmark for their orders (VWAP execution algorithms). Price >> VWAP → the market bought expensive relative to the average.
3.2 Micro-VWAP (5-minute rolling)
The Micro-VWAP is a VWAP over a recent moving window, more sensitive to short-term variations:
micro_vwap_window = deque(maxlen=300) # last 300 trades (~5 minutes on BTCUSDT)
for trade in micro_vwap_window:
pq_sum += trade.price * trade.quantity
vol_sum += trade.quantity
micro_vwap = pq_sum / vol_sum
3.3 VWAP Z-Score
The VWAP Z-Score measures how far the current price is from the VWAP in standard-deviation terms:
Typical values for BTCUSDT futures: - |vwap_z| < 1.0 → price near the VWAP (neutral zone) - 1.0 < |vwap_z| < 2.0 → moderate extension - |vwap_z| > 2.0 → significant extension → potential mean reversion
4. Aggression Ratio and Absorption
4.1 Aggression Ratio
computed over the last N seconds (default 300s = 5 minutes).
- AR > 0.6 → aggressive buy dominance → sustained bullish pressure
- AR < 0.4 → aggressive sell dominance → sustained bearish pressure
- AR ~0.5 → balance → market waiting
Difference vs OBI: - OBI measures intention (limit orders in the book) - AR measures action (orders executed aggressively)
They often diverge: high OBI + low AR → a liquidity wall on the bid is absorbing sells. An accumulation scenario.
4.2 Absorption (Reversal Signal)
Absorption happens when one side of the book absorbs large amounts of aggressive orders without the price moving proportionally. It signals that sized players (often institutional) are buying/selling against the retail flow.
Detection algorithm:
def _detect_absorption(self, candle) -> bool:
"""
Buy absorption: strongly negative delta (heavy selling) but close >= open
(the price did not fall)
-> buyers absorbed all the selling without giving ground
"""
delta_pct = candle.delta / candle.volume if candle.volume > 0 else 0
price_change = (candle.close - candle.open) / candle.open
# Case 1: heavy sell flow but price holds (bull absorption)
if delta_pct < -0.3 and price_change >= -0.001: # -30% delta, max -0.1% price
return True
# Case 2: heavy buy flow but price does not rise (bear absorption — top)
if delta_pct > 0.3 and price_change <= 0.001:
return True
return False
Absorption is one of the most reliable microstructure signals because it reveals an "invisible wall" in the book that does not appear in the L2 data but shows up in the flow.
5. Kyle's Lambda — Price Impact
5.1 Theory
Kyle's Lambda (from Albert Kyle, 1985, "Continuous Auctions and Insider Trading", Econometrica) measures the price impact per unit of net volume:
where:
- Delta_P = price change
- Q_net = net volume (buy - sell) in the period
- lambda = price impact coefficient (Kyle's Lambda)
- epsilon = random component
High lambda → illiquid market → every additional unit of volume moves the price a lot. Low lambda → liquid market → large volume needed to move the price.
5.2 OLS Estimation on a Rolling Window
We estimate Lambda via OLS (Ordinary Least Squares) over a window of N candles:
which is the regression coefficient of Delta_P on Q_net (regression through the origin).
Implementation (src/engine/orderflow.py):
def _compute_kyle_lambda(self, symbol: str) -> float:
"""
Estimates Kyle's Lambda on a rolling window of 1m candles.
Lambda = Cov(dP, Q_net) / Var(Q_net)
"""
history = self._kyle_buffer.get(symbol, deque(maxlen=100))
if len(history) < 20:
return 0.0
dp = np.array([h["price_change"] for h in history]) # Delta P
qnet = np.array([h["net_volume"] for h in history]) # Q_net = buy_vol - sell_vol
var_q = np.var(qnet)
if var_q < 1e-10: # degenerate: no volume variation
return 0.0
cov_dpq = np.cov(dp, qnet)[0, 1]
return cov_dpq / var_q # OLS estimate
Buffer updated on every finalized candle:
self._kyle_buffer[symbol].append({
"price_change": close - open, # Delta P in USD
"net_volume": buy_vol - sell_vol # Q_net in BTC
})
5.3 Operational Interpretation
| Kyle's Lambda | Interpretation | Action |
|---|---|---|
| < 0.001 | Excellent liquidity, low impact | Normal position size |
| 0.001 - 0.005 | Normal liquidity | Normal position size |
| 0.005 - 0.01 | Reduced liquidity | Reduce size |
| > 0.01 | Illiquid market | Avoid market orders, use limits |
Negative lambda: rare, indicates price mean-reverting relative to flow (typical of very liquid markets with aggressive market makers).
Inverse formula — expected slippage for an order of quantity Q:
Example: Lambda = 0.003, 5 BTC order → expected slippage = 0.003 * 5 = 0.015 USD/BTC = $0.015 on a $64500 price ≈ 0.023 bps (negligible for BTCUSDT perpetual).
6. Volume Z-Score
The Volume Z-Score measures how anomalous a candle's volume is relative to recent history:
computed over a rolling window of N candles (default 50).
Uses:
- volume_z > 2 → volume spike → potentially genuine breakout
- volume_z < -1 → low volume → unreliable signals (reduced liquidity)
7. MarketSnapshot — Unified Output
The OrderflowEngine produces a MarketSnapshot on request, aggregating
every computed metric:
@dataclass
class MarketSnapshot:
symbol: str
timestamp_ms: int
price: float # last price
# Multi-timeframe CVD
cvd_1m: float # CVD last minute
cvd_5m: float # CVD last 5 minutes
cvd_15m: float # CVD last 15 minutes
# Order book
book_imbalance: float # OBI top 10 [0,1]
# Flow metrics
aggression_ratio: float # AR 5 minutes [0,1]
volume_zscore: float # volume Z-score (rolling 50 candles)
vwap_z: float # VWAP Z-score
micro_vwap: float # 5-minute Micro-VWAP
# Open Interest
oi_change_pct: float # OI% change vs previous snapshot
# Microstructure
kyle_lambda: float # price impact coefficient
is_absorption: bool # absorption signal
is_liq_vacuum: bool # liquidity vacuum in the book
is_volume_spike: bool # volume_zscore > 2.5
# Aggregated liquidations, 10 minutes
liq_buy_volume_10m: float # short-liquidation volume (short squeeze)
liq_sell_volume_10m: float # long-liquidation volume (long dump)
The snapshot is the interface contract between the data layer and the strategies. Each strategy reads only the fields it needs.
8. Computational Complexity
| Operation | Complexity | Notes |
|---|---|---|
update_from_trade() |
O(1) | Append on deque only |
update_from_book() |
O(1) | Snapshot copy |
_finalize_candle() |
O(K) | K = candle history size (50) for Z-score |
_compute_kyle_lambda() |
O(N) | N = buffer size (100), run per candle |
get_snapshot() |
O(1) | Returns last cached snapshot |
get_candle_history() |
O(N) | N = number of requested candles |
flush_snapshot() |
O(K) | Forces finalization of the incomplete candle |
The bottleneck is not the math (all O(N) with small N) but JSON parsing from the WebSocket. For extreme volumes (>10k msg/s) msgpack or protocol buffers could be used.