Order Flow Imbalance: The Cont-Kukanov-Stoikov Event Decomposition
La plupart des signaux dérivés des livres sur ce blog sont des instantanés : combien de volume reste actuellement de chaque côté. Le déséquilibre des flux de commandes est un objet différent. Il s'agit d'une décomposition du flux de mises à jour entre deux instantanés en une seule quantité signée – une quantité qui compte une annulation sur l'offre de la même manière qu'elle compte une vente sur le marché.
Cette décomposition fait l’objet de cet article. Plus précisément : les indicateurs d'événement Cont-Kukanov-Stoikov (2014) qui le définissent, l'extension multi-niveaux Xu-Gould-Howison (2019) et sa réduction des composants principaux, ainsi que les règles Lee-Ready et Bulk Volume Classification dont vous avez besoin lorsque votre flux n'indique pas la direction commerciale.
Une note de cadrage dès le départ, car elle détermine la façon dont vous devez lire chaque chiffre ci-dessous. Le fameux R au carré de 50 à 65 % du document CKS est une régression contemporaine : la variation des prix sur un intervalle a régressé sur le flux de commandes sur le même intervalle. Il s’agit d’une décomposition des mouvements de prix, et non d’une prévision de ceux-ci. En le comparant aux 1 à 2 % de variance qu'un modèle de rendement quotidien des actions explique, c'est des pommes avec des oranges, et ce blog a longuement soutenu - dans [DeepLOB et l'écart prédiction-bénéfice] (/en/blog/post/deeplob-deep-learning-order-book) et dans [le résultat négatif honnête] (/en/blog/post/honest-negative-no-robust-edge) - que la confusion des deux est la façon dont les tutoriels deviennent des pièges. Une régression OFI strictement décalée est un nombre légitime et beaucoup plus petit. Cet article n’en rapporte pas.
Pourquoi le flux des commandes, pas les transactions

Un changement de prix dans un carnet d’ordres limités continu se produit par exactement quatre mécanismes : un achat sur le marché consomme des demandes au repos ; une vente sur le marché consomme les offres restantes ; une annulation de l'offre supprime le support et laisse tomber l'offre ; un nouvel ordre limité à la demande ajoute de la résistance. Seuls les deux premiers sont des échanges. Les deux derniers sont invisibles pour toute mesure basée sur le volume – VWAP, volume en solde, flux d’échanges signés – et sur la plupart des sites, le ratio commandes/échanges dépasse 10 : 1, donc la partie invisible est la partie la plus importante. L'utilité de l'OFI réside dans le fait qu'il évalue les quatre mécanismes sur la même échelle. (Pour le livre en tant que structure de données et le vecteur de fonctionnalités d'instantané standard , voir DeepLOB.)
Le modèle OFI Cont-Kukanov-Stoikov

Le modèle fondamental provient de l'article de 2014 de Cont, Kukanov et Stoikov « The Price Impact of Order Book Events » (Journal of Financial Econometrics).
Définition du déséquilibre du flux de commandes
Considérez le meilleur prix acheteur , le meilleur prix vendeur et leurs tailles et . Entre les observations consécutives à et :
où les contributions aux événements côté acheteur et côté vente sont :
En langage clair : si le meilleur prix acheteur augmente, un nouvel intérêt d’achat apparaît – comptez sa taille totale comme positive. S'il baisse, les intérêts d'achat ont disparu – soustrayez l'ancienne taille. Si le prix reste inchangé, comptez uniquement le changement de taille. Le côté demandé est l’image miroir.
L'élégance réside dans le fait que ces trois branches d'indicateur couvrent toutes les transitions possibles du haut du livre, de sorte que chaque mise à jour correspond exactement à un numéro signé. Aucune classification commerciale, aucune étiquette latérale, aucun flux au niveau du message requis : deux instantanés consécutifs suffisent.
Agrégation sur intervalles
Pour un intervalle contenant mises à jour de livres :
Le modèle linéaire d'impact sur les prix
où est la variation du prix moyen, est le coefficient d'impact sur le prix et est le bruit résiduel. Sur les actions américaines sur des intervalles de 10 secondes à 1 minute, CKS rapporte un contemporain de 50 à 65 % par action. Encore une fois : au même intervalle, pas prospectif.
Mise à l'échelle transversale
CKS a également montré que les coefficients d’impact évoluent inversement avec la profondeur :
où est le volume de repos moyen au meilleur cours acheteur et vendeur. Le même flux pousse les prix plus loin dans un livre mince. Il s’agit de la seule partie du modèle qui est transférée d’un site à l’autre sans réajuster le niveau, car elle prédit une relation plutôt qu’un nombre – et elle est directement testable sur des paires cryptographiques de profondeur différente.
Déséquilibre des flux de commandes à plusieurs niveaux (MLOFI)

Le modèle original utilise uniquement le haut du livre. Xu, Gould et Howison (2019) l'ont étendu aux niveaux .
Définition
où applique la formule identique à trois branches à la -ème paire offre/vente.
Impact sur les prix à plusieurs niveaux
Le résultat publié, sur les actions du Nasdaq, est que chaque niveau supplémentaire ajoute hors échantillon – environ 10 à 15 points de pourcentage passant d'un niveau à cinq, avec des gains marginaux toujours présents à dix. Les coefficients décroissent de manière monotone, : le haut du livre domine, mais les niveaux plus profonds véhiculent un signal incrémental non trivial.
La question ouverte la plus intéressante de cet article est de savoir si cela survivra dans un livre de cryptographie. Les livres de cryptographie sont plus minces et beaucoup plus profonds, et il est tout à fait plausible que les niveaux 2 à 5 n'ajoutent rien une fois que vous quittez un carnet d'ordres du Nasdaq.
Réduction de la composante principale
Les AIF de niveau adjacent sont fortement corrélés (au-dessus de 0,8 dans les résultats actions publiés), une décomposition en composantes principales est donc naturelle. La première composante rapportée capture plus de 89 % de la variance totale et sert de signal global unique :
avec le vecteur propre principal de la matrice de covariance OFI. L’attrait pratique est qu’il réduit une régression colinéaire en un seul scalaire bien conditionné – cela vaut la peine même si la part de variance sur vos données est inférieure.
Classification des transactions : achat ou vente initiés

L'OFI lui-même n'a besoin d'aucune classification commerciale. Mais si vous souhaitez le comparer à des mesures basées sur le commerce, ou si vous disposez uniquement de barres agrégées, vous devez en déduire une direction.
Règle de devis
Comparez le prix commercial au point médian en vigueur :
Une transaction au-dessus du point médian était probablement un acheteur qui retirait son offre ; en dessous, un vendeur atteint l'enchère.
L'algorithme Lee-Ready (1991) est la règle de cotation avec une règle de repli pour les transactions exactement au point médian, où la règle de cotation est indéterminée. La règle du tick - signe du dernier changement de prix, faisant avancer le côté précédent sur un tick zéro - est déjà dérivée et implémentée dans [Au-delà des barres de temps] (/en/blog/post/beyond-time-bars-candle-construction), qui fournit un _tick_sign() fonctionnel. La précision de la classification Lee-Ready déclarée est de 72 à 85 % selon le marché et la période.
Classification des volumes en vrac (BVC)
Lorsque des transactions individuelles ne peuvent pas être attribuées (barres agrégées, plupart des API de bougies publiques), Easley, Lopez de Prado et O'Hara's Bulk Volume Classification estiment la fraction d'achat du volume d'une barre à partir de sa variation de prix normalisée :
avec le CDF normal standard et estimé à partir des changements de prix récents. Moins précis que la classification au niveau des ticks, mais cela fonctionne sur OHLCV.
Ce qu'OFI n'est pas

Trois grandeurs voisines se confondent avec OFI. Chacun est correctement couvert ailleurs sur ce blog ; ce sont les distinctions qui comptent ici.
Le déséquilibre statique du carnet de commandes (OBI) est la version instantanée : volume d'offre au repos par rapport au volume demandé au repos, pas de suivi des événements. Voir Fonctionnalités LOB traditionnelles de DeepLOB pour la formule et sa forme à plusieurs niveaux. Le contraste est tout l'intérêt d'OFI : OBI vous indique l'état du livre, OFI vous raconte comment il est arrivé là.
Le déséquilibre commercial (TI) et ses variantes pondérées en volume sont des flux commerciaux signés, définis et mis en œuvre en tant que fonctionnalités continues dans modélisation du spread avec apprentissage automatique. Un résultat publié sur la relation est ici significatif : lorsque OFI et TI sont entrés ensemble comme régresseurs sur les variations de prix médianes, TI devient statistiquement non significatif. Son contenu est intégré. C'est le cas empirique du suivi des événements comptables plutôt que des transactions : les événements qui sont presque devenus des transactions contiennent les mêmes informations que ceux qui l'ont été.
Le milieu pondéré en fonction du volume est un estimateur de juste valeur, pas une mesure de déséquilibre, et il est couvert dans DeepLOB — y compris la mise en garde de dénomination selon laquelle il ne s'agit pas du micro-prix de Stoikov, qui est un estimateur ajusté à la martingale construit précisément parce que le milieu pondéré naïf est biaisé.
Implémentation Python

Une implémentation directe de la décomposition CKS, généralisée aux niveaux .
Calcul OFI de base
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
Agrégation dans Windows
@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
Notez ce que l'agrégateur émet : mlofi et delta_mid sur la même fenêtre. C'est la régression contemporaine. Pour en obtenir un prédictif, associez le mlofi de la fenêtre avec le delta_mid de la fenêtre et attendez-vous à un ajustement bien pire.
Classification commerciale (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
Normaliser le signal
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}
Où OFI se connecte

Tenue de marché. OFI saisit comme terme de biais prédictif sur la juste valeur, en plus du biais de stock déjà dérivé et codé dans [le teneur de marché Avellaneda-Stoikov] (/en/blog/post/market-making-avellaneda-stoikov) : . Les deux termes répondent à des questions différentes – le terme de flux indique où va le prix, le terme d’inventaire indique ce que vous pouvez vous permettre de détenir – et l’article publié couvre la seconde, notamment pourquoi un inventaire positif fait baisser les deux cotations. Un grand est également un déclencheur de sélection adverse raisonnable pour les réponses défensives (élargir, rétrécir, tirer d'un côté) cataloguées dans [empreintes digitales d'identification du commerçant] (/en/blog/post/digital-fingerprint-trader-identification) et [détection d'anomalies] (/en/blog/post/anomaly-detection-algotrading).
Exécution. OFI est une entrée dans l'estimation de probabilité de remplissage de la couche tactique, et non un contrôleur d'urgence distinct. La décision post-contre-croix est une arithmétique explicite du seuil de rentabilité – avec – et réside dans les [tactiques d’exécution des ordres enfants] (/en/blog/post/child-order-execution-tactics), qui soutient également que la couche tactique ne devrait jamais dériver l’urgence de sa propre vision du marché, car cela crée deux contrôleurs en désaccord.
Considérations pratiques

Demi-vie du signal et recalibrage roulant. Il s'agit de la partie spécifique à l'OFI. Le contenu prédictif de l’OFI décroît sur une échelle de temps allant de quelques millisecondes à quelques secondes dans les instruments liquides, ce qui signifie que la fenêtre d’agrégation n’est pas un paramètre libre – c’est un pari à l’horizon. Et n'est pas une constante : il se déplace au cours de la journée, en profondeur et autour des événements planifiés, de sorte que tout ajustement de production le réestime sur des fenêtres mobiles plutôt que d'épingler une valeur à partir d'un backtest.
Tout le reste ici est couvert ailleurs. Budgets de latence et échelle de co-location / FPGA / kernel-bypass : [section production de DeepLOB] (/en/blog/post/deeplob-deep-learning-order-book). Le modèle de liquidité intrajournalière en forme de U et les statistiques de normalisation à la dérive : [modélisation du spread] (/en/blog/post/spread-modeling-machine-learning). Recalibrage par site et pourquoi un modèle installé sur le Nasdaq ne sera pas transféré vers une paire crypto intacte : DeepLOB à nouveau, avec le côté fragmentation dans smart order routage.
Manipulation. Une conséquence est spécifique à ce modèle et mérite d'être explicitée : la décomposition CKS pondère chaque événement uniquement en fonction de sa taille, sans aucune notion d'intention ou de persistance. Un spoofer plaçant et annulant la taille injecte donc directement dans le signal, avec tout son poids, par construction. Les heuristiques de détection – taux d'annulation, durée de vie des commandes, comportement du mur à l'approche du prix – sont en [analyse de la position de la file d'attente et du mur du carnet d'ordres] (/en/blog/post/queue-position-order-book-wall-analysis) ; la liquidité fantôme inter-sites est dans [routage d'ordres intelligent] (/en/blog/post/smart-order-routing-crypto).
Points clés à retenir

-
OFI est une décomposition d'événements, pas un instantané. La formule de l'indicateur à trois branches mappe chaque transition en haut du livre (augmentation du prix, baisse du prix, changement de taille) sur un nombre signé, à partir de deux instantanés consécutifs et rien d'autre.
-
Le R-carré global est contemporain. Le chiffre CKS 50-65 % décompose les mouvements de prix au même intervalle ; il ne s’agit pas d’une prévision et ne doit pas être comparée à des modèles de rendement prospectifs.
-
L'OFI multiniveau ajoute des niveaux et la PCA les réduit. Les résultats incrémentiels publiés de et de variance de 89 % proviennent des actions du Nasdaq. La question de savoir si la profondeur est toujours utile sur un livre cryptographique n’a pas été testée ici.
-
L'OFI englobe le déséquilibre commercial. Entré conjointement, TI devient insignifiant — les annulations qu'OFI voit et que TI ne peut pas faire sont ce qui fait la différence.
-
Le modèle est aveugle à l'intention. Une pondération égale selon la taille est exactement ce qui le rend usurpable.
-
Rien dans cet article n'est mesuré. Chaque chiffre cité est tiré de la littérature sur les actions. Avant que ce signal n'atteigne le capital, il a besoin d'un ajustement sur vos propres données comptables, d'une spécification décalée déclarée séparément et d'une vérification pour savoir si l'avantage survit aux frais et au spread.
Lectures complémentaires

- Cont, R., Kukanov, A. et Stoikov, S. (2014). "L'impact sur les prix des événements du carnet de commandes." Journal of Financial Econometrics, 12(1), 47-88.
- Xu, K., Gould, M. et Howison, S. (2019). «Déséquilibre des flux d'ordres à plusieurs niveaux dans un carnet d'ordres limités». arXiv :1907.06230.
- Kolm, P., Turiel, J. et Westray, N. (2023). «Déséquilibre profond du flux d'ordres : extraction de l'alpha à plusieurs horizons du carnet d'ordres limités." Finance mathématique, 33(4).
- Lee, C. et Ready, M. (1991). « Déduire la direction commerciale à partir des données intrajournalières. » Journal des Finances, 46(2), 733-746.
- Easley, D., Lopez de Prado, M. et O'Hara, M. (2012). «Toxicité des flux et liquidité dans un monde à haute fréquence». Revue des études financières, 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.