Order Flow Imbalance: The Cont-Kukanov-Stoikov Event Decomposition
इस ब्लॉग के अधिकांश book-derived संकेत snapshots हैं: इस समय प्रत्येक तरफ कितना volume मौजूद है। Order flow imbalance एक अलग वस्तु है। यह दो snapshots के बीच updates की stream को एक signed quantity में तोड़ता है — जो bid पर cancellation को उसी तरह गिनती है जैसे market sell को।
यह decomposition इस लेख का विषय है। विशेष रूप से: इसे परिभाषित करने वाले Cont-Kukanov-Stoikov (2014) event indicators, Xu-Gould-Howison (2019) का multi-level विस्तार और उसका principal-component reduction, तथा जब feed trade direction label न करे तब आवश्यक Lee-Ready और Bulk Volume Classification नियम।
पहले एक framing note, क्योंकि इससे नीचे दिए हर number को पढ़ने का तरीका तय होता है। CKS paper का प्रसिद्ध 50-65% R-squared एक contemporaneous regression है: किसी interval का price change उसी interval के order flow पर regress किया गया है। यह price moves का decomposition है, उनका forecast नहीं। इसकी तुलना daily equity return model द्वारा समझाए गए variance के 1-2% से करना apples-to-oranges है, और इस ब्लॉग ने DeepLOB और prediction-vs-profit gap तथा ईमानदार negative result में विस्तार से समझाया है कि दोनों को मिलाना tutorials को traps में बदल देता है। Strictly lagged OFI regression एक वैध और बहुत छोटा number है। यह लेख उसे report नहीं करता।
Trades नहीं, Order Flow क्यों

Continuous limit order book में price change ठीक चार mechanisms से होता है: market buy resting asks को consume करता है; market sell resting bids को consume करता है; bid पर cancellation support हटाकर bid को नीचे जाने देती है; ask पर नया limit order resistance बढ़ाता है। केवल पहले दो trades हैं। आखिरी दो किसी भी volume-based measure — VWAP, on-balance volume, signed trade flow — को दिखाई नहीं देते, और अधिकांश venues पर order-to-trade ratio 10:1 से अधिक है, इसलिए अदृश्य हिस्सा ही बड़ा हिस्सा है। OFI की उपयोगिता का पूरा दावा यही है कि वह इन चारों mechanisms को एक ही scale पर price करता है। (Book को data structure और standard snapshot feature vector के रूप में समझने के लिए DeepLOB देखें।)
Cont-Kukanov-Stoikov OFI Model

मूल model Cont, Kukanov और Stoikov के 2014 paper "The Price Impact of Order Book Events" (Journal of Financial Econometrics) से आता है।
Order Flow Imbalance की परिभाषा
Consider the best bid price , best ask price , and their sizes and . Between consecutive observations at and :
जहां buy-side और sell-side event contributions हैं:
सरल भाषा में: best bid price बढ़े तो नया buy interest आया — उसका पूरा size positive गिनें। अगर वह घटे, तो buy interest गायब हुआ — पुराना size घटाएं। Price न बदले तो केवल size change गिनें। Ask side इसका mirror image है।
इसकी खूबसूरती यह है कि ये तीन indicator branches top of book के हर संभावित transition को cover करती हैं, इसलिए हर update ठीक एक signed number में map होता है। Trade classification नहीं, side labels नहीं, message-level feed की जरूरत नहीं — दो consecutive snapshots पर्याप्त हैं।
Intervals पर Aggregation
For an interval containing book updates:
Linear Price Impact Model
जहां mid-price का change है, price impact coefficient है और residual noise है। US equities में 10-second से 1-minute intervals पर CKS प्रति stock 50-65% contemporaneous report करते हैं। फिर से: same-interval, forward-looking नहीं।
Cross-Sectional Scaling
CKS ने यह भी दिखाया कि impact coefficient depth के साथ inversely scale करता है:
जहां best bid और ask पर average resting volume है। वही flow thin book में price को अधिक दूर धकेलता है। यह model का वह हिस्सा है जो level को refit किए बिना venues के बीच transfer हो सकता है, क्योंकि यह number के बजाय relationship predict करता है — और अलग depth वाले crypto pairs पर सीधे test किया जा सकता है।
Multi-Level Order Flow Imbalance (MLOFI)

मूल model केवल top of book का उपयोग करता है। Xu, Gould और Howison (2019) ने इसे levels तक बढ़ाया।
परिभाषा
जहां , -वें bid/ask pair पर वही three-branch formula लागू करता है।
Multi-Level Price Impact
Nasdaq equities पर published finding यह है कि हर अतिरिक्त level out-of-sample बढ़ाता है — one level से five levels तक लगभग 10-15 percentage points, और ten levels पर भी marginal gains मौजूद हैं। Coefficients monotonically घटते हैं, : top of book dominant है, लेकिन deeper levels में non-trivial incremental signal रहता है।
क्या यह crypto book पर भी टिकता है, इस लेख का सबसे रोचक खुला प्रश्न है। Crypto books अधिक पतली होती हैं और depth पर बहुत अधिक churn होता है; यह पूरी तरह संभव है कि Nasdaq order book से बाहर levels 2-5 कुछ भी न जोड़ें।
Principal Component Reduction
Adjacent-level OFIs अत्यधिक correlated हैं (published equity results में 0.8 से ऊपर), इसलिए principal component decomposition स्वाभाविक है। Reported first component total variance का 89% से अधिक capture करता है और single aggregate signal की तरह काम करता है:
जहां OFI covariance matrix का leading eigenvector है। Practical लाभ यह है कि collinear regression एक well-conditioned scalar में collapse हो जाती है — आपके data पर variance share कम आए, तब भी यह करना उपयोगी है।
Trades का Classification: Buy बनाम Sell Initiated

OFI को स्वयं trade classification की आवश्यकता नहीं है। लेकिन यदि आप इसकी तुलना trade-based measures से करना चाहते हैं, या आपके पास केवल aggregated bars हैं, तो direction infer करनी होगी।
Quote Rule
Compare the trade price to the prevailing midpoint :
Midpoint से ऊपर का trade संभवतः offer उठाने वाला buyer था; नीचे का trade bid hit करने वाला seller था।
Lee-Ready algorithm (1991) quote rule है, जिसमें midpoint पर ठीक होने वाले trades के लिए tick-rule fallback है, क्योंकि वहां quote rule indeterminate होता है। Tick rule — last price change का sign, और zero tick पर previous side को आगे ले जाना — Beyond time bars में derive और implement किया गया है, जहां working _tick_sign() उपलब्ध है। Reported Lee-Ready classification accuracy market और period के अनुसार 72-85% है।
Bulk Volume Classification (BVC)
जब individual trades assign नहीं किए जा सकते — aggregated bars और अधिकांश public candle APIs में — Easley, Lopez de Prado और O'Hara का Bulk Volume Classification normalized price change से bar volume का buy fraction estimate करता है:
जहां standard normal CDF है और recent price changes से estimate किया जाता है। Tick-level classification से कम accurate है, लेकिन OHLCV पर काम करता है।
OFI क्या नहीं है

तीन neighbouring quantities को OFI के साथ confuse किया जाता है। इनमें से हर एक को इस ब्लॉग में कहीं और ठीक से cover किया गया है; यहां distinctions महत्वपूर्ण हैं।
Static order book imbalance (OBI) snapshot version है — resting bid volume बनाम resting ask volume, बिना event tracking के। Formula और multi-level form के लिए DeepLOB के traditional LOB features देखें। यही OFI का मुख्य contrast है: OBI book की state बताता है, OFI बताता है कि वह state बनी कैसे।
Trade imbalance (TI) और इसके volume-weighted variants signed trade flow हैं, जिन्हें spread modeling with machine learning में rolling features के रूप में define और implement किया गया है। इस संबंध पर एक published result यहां आधारभूत है: जब OFI और TI को mid-price changes पर regressors के रूप में साथ दिया जाता है, TI statistically insignificant हो जाता है। उसका content subsume हो जाता है। यही trades के बजाय book events track करने का empirical case है — जो events लगभग trades बने, उनमें वही information है जो वास्तव में बने trades में है।
Volume-weighted mid fair-value estimator है, imbalance measure नहीं, और इसे DeepLOB में cover किया गया है — साथ में यह naming caveat भी कि यह Stoikov का micro-price नहीं है। Stoikov micro-price martingale-adjusted estimator है, जिसे ठीक इसलिए बनाया गया क्योंकि naive weighted mid biased है।
Python Implementation

CKS decomposition का direct implementation, जिसे levels तक generalize किया गया है।
Core OFI Calculation
import numpy as np
import pandas as pd
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class OrderBookSnapshot:
timestamp: float
bid_prices: np.ndarray # best bid at index 0, descending
ask_prices: np.ndarray # best ask at index 0, ascending
bid_sizes: np.ndarray
ask_sizes: np.ndarray
@dataclass
class OFICalculator:
"""
Computes Order Flow Imbalance from consecutive order book snapshots.
Supports multi-level OFI (MLOFI) up to `n_levels` deep.
"""
n_levels: int = 5
prev_snapshot: Optional[OrderBookSnapshot] = field(default=None, init=False)
def compute_level_ofi(
self,
prev_price: float, curr_price: float,
prev_size: float, curr_size: float,
side: str
) -> float:
"""Compute single-level OFI contribution for bid or ask side."""
if side == "bid":
if curr_price > prev_price:
return curr_size # new level appeared above
elif curr_price < prev_price:
return -prev_size # old level disappeared
else:
return curr_size - prev_size # same level, size changed
else: # ask side
if curr_price < prev_price:
return curr_size # new level appeared below
elif curr_price > prev_price:
return -prev_size # old level disappeared
else:
return curr_size - prev_size # same level, size changed
def update(self, snapshot: OrderBookSnapshot) -> Optional[np.ndarray]:
"""
Process new snapshot, return MLOFI vector of shape (n_levels,).
Returns None on first call (no previous snapshot to compare).
"""
if self.prev_snapshot is None:
self.prev_snapshot = snapshot
return None
prev = self.prev_snapshot
n = min(self.n_levels, len(snapshot.bid_prices), len(prev.bid_prices))
ofi = np.zeros(n)
for level in range(n):
e_buy = self.compute_level_ofi(
prev.bid_prices[level], snapshot.bid_prices[level],
prev.bid_sizes[level], snapshot.bid_sizes[level],
side="bid"
)
e_sell = self.compute_level_ofi(
prev.ask_prices[level], snapshot.ask_prices[level],
prev.ask_sizes[level], snapshot.ask_sizes[level],
side="ask"
)
ofi[level] = e_buy - e_sell
self.prev_snapshot = snapshot
return ofi
Windows में Aggregation
@dataclass
class OFIAggregator:
"""
Aggregates raw OFI updates into fixed time windows.
Emits the regression inputs (aggregated MLOFI) and target (delta mid).
"""
window_seconds: float = 10.0
n_levels: int = 5
calculator: OFICalculator = field(init=False)
buffer: list = field(default_factory=list, init=False)
window_start: float = 0.0
def __post_init__(self):
self.calculator = OFICalculator(n_levels=self.n_levels)
def on_snapshot(self, snapshot: OrderBookSnapshot) -> Optional[dict]:
"""
Feed a new order book snapshot.
Returns aggregated window dict when a window completes, else None.
"""
ofi_vec = self.calculator.update(snapshot)
if ofi_vec is None:
self.window_start = snapshot.timestamp
return None
if not self.buffer:
self.window_start = snapshot.timestamp
self.buffer.append({
"timestamp": snapshot.timestamp,
"ofi": ofi_vec.copy(),
"mid": (snapshot.bid_prices[0] + snapshot.ask_prices[0]) / 2,
})
elapsed = snapshot.timestamp - self.window_start
if elapsed >= self.window_seconds:
return self._flush()
return None
def _flush(self) -> dict:
"""Aggregate buffered OFI updates into a single window record."""
ofi_matrix = np.array([b["ofi"] for b in self.buffer])
agg_ofi = ofi_matrix.sum(axis=0) # shape: (n_levels,)
result = {
"window_start": self.window_start,
"window_end": self.buffer[-1]["timestamp"],
"n_updates": len(self.buffer),
"mid_open": self.buffer[0]["mid"],
"mid_close": self.buffer[-1]["mid"],
"delta_mid": self.buffer[-1]["mid"] - self.buffer[0]["mid"],
"ofi_level1": agg_ofi[0],
"ofi_total": agg_ofi.sum(),
"mlofi": agg_ofi,
}
self.buffer.clear()
return result
ध्यान दें कि aggregator क्या emit करता है: same window पर mlofi और delta_mid। यही contemporaneous regression है। Predictive regression के लिए window के mlofi को window के delta_mid के साथ pair करें और काफी खराब fit की अपेक्षा रखें।
Trade Classification (Lee-Ready)
def classify_trades_lee_ready(
trades: pd.DataFrame,
quotes: pd.DataFrame
) -> pd.DataFrame:
"""
Classify trades as buy (+1) or sell (-1) using Lee-Ready:
quote rule first, tick rule as fallback at the midpoint.
Parameters
----------
trades : DataFrame with columns ['timestamp', 'price', 'size']
quotes : DataFrame with columns ['timestamp', 'bid', 'ask']
Returns
-------
trades with added 'side' column
"""
trades = trades.sort_values("timestamp").copy()
quotes = quotes.sort_values("timestamp")
trades = pd.merge_asof(
trades, quotes,
on="timestamp",
direction="backward"
)
trades["mid"] = (trades["bid"] + trades["ask"]) / 2
trades["side"] = np.where(
trades["price"] > trades["mid"], 1,
np.where(trades["price"] < trades["mid"], -1, 0)
)
trades["price_diff"] = trades["price"].diff()
tick_sign = np.sign(trades["price_diff"])
tick_sign = tick_sign.replace(0, np.nan).ffill().fillna(1)
midpoint_mask = trades["side"] == 0
trades.loc[midpoint_mask, "side"] = tick_sign[midpoint_mask].astype(int)
return trades
Signal को Normalize करना
class OFISignal:
"""
Rolling z-score normalization of aggregated OFI.
Deliberately does NOT convert OFI into a predicted return: that
requires a fitted beta, and beta is venue-, pair- and regime-specific.
Fit it on your own data before wiring this into anything.
"""
def __init__(self, window_seconds: float = 10.0, n_levels: int = 5,
lookback: int = 100):
self.aggregator = OFIAggregator(
window_seconds=window_seconds, n_levels=n_levels,
)
self.lookback = lookback
self.ofi_history: list[float] = []
def process(self, snapshot: OrderBookSnapshot) -> Optional[dict]:
agg = self.aggregator.on_snapshot(snapshot)
if agg is None:
return None
ofi = agg["ofi_level1"]
self.ofi_history.append(ofi)
if len(self.ofi_history) > self.lookback:
self.ofi_history.pop(0)
if len(self.ofi_history) >= 20:
arr = np.array(self.ofi_history)
mu, sigma = arr.mean(), arr.std()
zscore = (ofi - mu) / max(sigma, 1e-10)
else:
zscore = 0.0
return {**agg, "ofi_zscore": zscore}
OFI कहां जुड़ता है

Market making. OFI fair value में predictive skew term के रूप में प्रवेश करता है, inventory skew के ऊपर जिसे Avellaneda-Stoikov market maker में derive और code किया गया है: । दोनों terms अलग questions का उत्तर देते हैं — flow term बताता है price किधर जा रही है, inventory term बताता है कि आप क्या hold कर सकते हैं — और published article दूसरे term को cover करता है, जिसमें यह भी शामिल है कि positive inventory दोनों quotes को नीचे क्यों धकेलता है। बड़ा adverse-selection trigger भी हो सकता है, defensive responses (widen, shrink, pull one side) के लिए, जो digital fingerprints of trader identification और anomaly detection में catalogued हैं।
Execution. OFI tactics layer के fill-probability estimate का input है, अलग urgency controller नहीं। Post-versus-cross decision स्पष्ट break-even arithmetic है — जहां — और child order execution tactics में रहता है। वही article यह भी कहता है कि tactics layer को market के अपने view से urgency फिर से derive नहीं करनी चाहिए, क्योंकि इससे असहमत दो controllers बनते हैं।
Practical Considerations

Signal half-life और rolling recalibration। यह OFI-specific हिस्सा है। Liquid instruments में OFI का predictive content milliseconds से seconds के timescale पर घटता है, इसलिए aggregation window कोई free parameter नहीं — यह horizon पर एक bet है। और constant नहीं है: यह intraday, depth के साथ और scheduled events के आसपास बदलता है, इसलिए production fit इसे backtest से निकली fixed value पर pin करने के बजाय rolling windows पर फिर estimate करता है।
बाकी सब कुछ कहीं और cover किया गया है। Latency budgets और co-location / FPGA / kernel-bypass ladder: DeepLOB का production section। U-shaped intraday liquidity pattern और drifting normalization statistics: spread modeling। Per-venue recalibration और यह कारण कि Nasdaq पर fitted model crypto pair पर बिना बदलाव transfer नहीं होगा: फिर से DeepLOB, और fragmentation पक्ष smart order routing में।
Manipulation. इस model का एक परिणाम खास है और साफ कहने योग्य है: CKS decomposition हर event को केवल size से weight करता है, intent या persistence की कोई समझ नहीं होती। इसलिए spoofer का size place और cancel करना construction के अनुसार full weight के साथ सीधे signal में घुस जाता है। Detection heuristics — cancel rates, order lifetimes, price के पास आने पर wall behaviour — queue position और order book wall analysis में हैं; cross-venue phantom liquidity smart order routing में है।
मुख्य निष्कर्ष

-
OFI event decomposition है, snapshot नहीं। Three-branch indicator formula हर top-of-book transition — price up, price down, size change — को दो consecutive snapshots और कुछ नहीं से एक signed number में map करता है।
-
Headline R-squared contemporaneous है। CKS का 50-65% figure same-interval price moves का decomposition है; यह forecast नहीं है और इसे forward-looking return models से compare नहीं करना चाहिए।
-
Multi-level OFI levels जोड़ता है और PCA उन्हें collapse करता है। Published incremental- और 89%-variance results Nasdaq equities से हैं। Crypto book पर depth अभी भी मदद करती है या नहीं, यहां test नहीं किया गया है।
-
OFI trade imbalance को subsume करता है। साथ enter करने पर TI insignificant हो जाता है — OFI जिन cancellations को देखता है और TI नहीं देख सकता, वही अंतर बनाती हैं।
-
Model intent से blind है। Size के आधार पर equal weighting ही इसे spoofable बनाती है।
-
इस लेख में कुछ भी measured नहीं है। हर quoted number equities literature से है। इस signal को capital तक पहुंचाने से पहले अपने book data पर fit, अलग से report की गई lagged specification, और fees तथा spread के बाद edge बचती है या नहीं इसका check आवश्यक है।
आगे का अध्ययन

- Cont, R., Kukanov, A., & Stoikov, S. (2014). "The Price Impact of Order Book Events." Journal of Financial Econometrics, 12(1), 47-88.
- Xu, K., Gould, M., & Howison, S. (2019). "Multi-Level Order-Flow Imbalance in a Limit Order Book." arXiv:1907.06230.
- Kolm, P., Turiel, J., & Westray, N. (2023). "Deep Order Flow Imbalance: Extracting Alpha at Multiple Horizons from the Limit Order Book." Mathematical Finance, 33(4).
- Lee, C., & Ready, M. (1991). "Inferring Trade Direction from Intraday Data." Journal of Finance, 46(2), 733-746.
- Easley, D., Lopez de Prado, M., & O'Hara, M. (2012). "Flow Toxicity and Liquidity in a High-Frequency World." Review of Financial Studies, 25(5), 1457-1493.
Authors
Trading-systems engineer
Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.