Order Flow Imbalance: The Cont-Kukanov-Stoikov Event Decomposition
이 블로그에서 주문서로부터 얻는 신호 대부분은 스냅샷입니다. 즉, 지금 각 호가 쪽에 얼마나 많은 수량이 대기 중인지를 보여줍니다. 주문 흐름 불균형은 다른 대상입니다. 두 스냅샷 사이의 업데이트 스트림을 하나의 부호 있는 수량으로 분해하며, 매수호가 취소를 시장 매도와 같은 방식으로 계산합니다.
이 분해가 이 글의 주제입니다. 구체적으로는 이를 정의하는 Cont-Kukanov-Stoikov(2014) 이벤트 지표, Xu-Gould-Howison(2019)의 다중 레벨 확장과 주성분 축소, 그리고 피드에 거래 방향이 표시되지 않을 때 필요한 Lee-Ready 및 Bulk Volume Classification 규칙을 다룹니다.
먼저 하나의 프레임을 짚고 가겠습니다. 아래의 모든 숫자를 어떻게 읽어야 하는지 결정하기 때문입니다. CKS 논문의 유명한 50-65% R 제곱은 동시적 회귀입니다. 같은 구간의 주문 흐름으로 그 구간의 가격 변화를 회귀한 값입니다. 가격 변화를 예측한 것이 아니라 가격 변동을 분해한 것입니다. 이를 일일 주식 수익률 모델이 설명하는 분산 1-2%와 비교하는 것은 사과와 오렌지를 비교하는 일입니다. 이 블로그는 DeepLOB와 예측-수익 격차와 정직한 부정적 결과에서 둘을 혼동하면 튜토리얼이 함정이 된다고 자세히 설명했습니다. 엄격한 시차 OFI 회귀는 정당하지만 훨씬 작은 수치이며, 이 글에서는 보고하지 않습니다.
거래가 아니라 주문 흐름인 이유

연속 지정가 주문서에서 가격 변화는 정확히 네 가지 메커니즘으로 발생합니다. 시장 매수는 대기 중인 매도호가를 소진하고, 시장 매도는 대기 중인 매수호가를 소진하며, 매수호가 취소는 지지를 없애 매수호가를 낮추고, 매도호가의 신규 지정가 주문은 저항을 더합니다. 거래인 것은 앞의 두 가지뿐입니다. 뒤의 두 가지는 VWAP, OBV, 부호 있는 거래 흐름 등 거래량 기반 지표에는 보이지 않습니다. 대부분의 거래소에서 주문 대 거래 비율이 10:1을 넘으므로 보이지 않는 부분이 더 큽니다. OFI의 핵심 가치는 네 메커니즘을 같은 척도로 가격에 반영한다는 데 있습니다. (주문서를 자료구조로 보는 관점과 표준 스냅샷 특성 벡터는 DeepLOB를 참조하세요.)
Cont-Kukanov-Stoikov OFI 모델

기초 모델은 Cont, Kukanov, Stoikov가 2014년에 발표한 "주문서 이벤트의 가격 영향"(Journal of Financial Econometrics)에서 나왔습니다.
주문 흐름 불균형 정의
최우선 매수 가격 , 최우선 매도 가격 , 그리고 수량 , 를 생각해 보세요. 과 사이의 연속 관측에서:
매수측과 매도측 이벤트 기여는 다음과 같습니다.
쉽게 말해 최우선 매수 가격이 오르면 새로운 매수 관심이 나타난 것이므로 전체 수량을 양수로 셉니다. 가격이 내려가면 매수 관심이 사라진 것이므로 이전 수량을 뺍니다. 가격이 그대로면 수량 변화만 셉니다. 매도 측은 반대 방향으로 적용됩니다.
세 지시함수 분기가 호가창 최상단에서 가능한 모든 변화를 포괄하므로 모든 업데이트가 정확히 하나의 부호 있는 숫자로 매핑됩니다. 거래 분류도, 매수·매도 라벨도, 메시지 단위 피드도 필요하지 않습니다. 연속된 두 스냅샷이면 충분합니다.
구간 집계
구간에 개의 주문서 업데이트가 포함된 경우:
선형 가격 영향 모델
여기서 는 중간 가격의 변화, 는 가격 영향 계수, 는 잔차 잡음입니다. 10초에서 1분 구간의 미국 주식에 대해 CKS는 종목별 동시 를 50-65%로 보고합니다. 다시 말해 같은 구간의 회귀이며 미래를 내다본 값이 아닙니다.
단면적 확장
CKS는 영향 계수가 깊이에 반비례한다는 점도 보였습니다.
여기서 는 최우선 매수·매도호가의 평균 대기 수량입니다. 같은 흐름이라도 얕은 주문서에서는 가격을 더 멀리 밀어냅니다. 이는 숫자가 아니라 관계를 예측하므로 계수를 다시 맞추지 않고도 거래소 간에 옮길 수 있는 모델의 부분이며, 깊이가 다른 암호화폐 페어에서 직접 검증할 수 있습니다.
다중 레벨 주문 흐름 불균형(MLOFI)

원래 모델은 주문서 최상단만 사용합니다. Xu, Gould, Howison(2019)은 이를 개 레벨로 확장했습니다.
정의
여기서 는 번째 매수·매도 가격 쌍에 동일한 세 분기 공식을 적용합니다.
다중 레벨 가격 영향
나스닥 주식에서 발표된 결과에 따르면 레벨을 하나 추가할 때마다 표본 외 가 증가합니다. 1레벨에서 5레벨로 갈 때 약 10-15%포인트 증가하고, 10레벨에서도 한계 이득이 남아 있습니다. 계수는 단조롭게 감소합니다. : 주문서 최상단이 지배적이지만 더 깊은 레벨에도 무시할 수 없는 추가 신호가 있습니다.
이 결과가 암호화폐 주문서에서도 유지되는지는 이 글에서 가장 흥미로운 열린 질문입니다. 암호화폐 주문서는 더 얇고 깊은 레벨의 변동도 훨씬 많으므로, 나스닥 주문서를 벗어나면 2-5레벨이 아무것도 추가하지 않는 것도 충분히 가능합니다.
주성분 축소
인접 레벨의 OFI는 상관성이 높으므로(발표된 주식 결과에서 0.8 이상) 주성분 분해가 자연스럽습니다. 보고된 첫 번째 주성분은 전체 분산의 89% 이상을 포착하며 단일 집계 신호로 사용할 수 있습니다.
여기서 는 OFI 공분산 행렬의 주 고유벡터입니다. 실용적인 장점은 공선 회귀를 조건이 좋은 하나의 스칼라로 줄인다는 데 있습니다. 데이터에서 차지하는 분산 비율이 더 낮더라도 시도할 가치가 있습니다.
거래 분류: 구매 및 판매 개시

OFI 자체에는 거래 분류가 필요하지 않습니다. 하지만 거래 기반 지표와 비교하거나 집계된 봉만 가지고 있다면 방향을 추론해야 합니다.
견적 규칙
거래 가격 를 현재 중간 가격 와 비교합니다.
중간 가격보다 높은 거래는 매수자가 매도호가를 들어 올린 것일 가능성이 높고, 그보다 낮은 거래는 매도자가 매수호가를 친 것입니다.
Lee-Ready 알고리즘(1991)은 중간 가격에서 거래 방향이 불확실할 때 tick 규칙을 대체 수단으로 사용하는 호가 규칙입니다. tick 규칙은 마지막 가격 변화의 부호를 사용하고, 변화가 0이면 이전 방향을 이어갑니다. 이 규칙은 이미 시간 봉을 넘어에서 도출·구현되었으며, 실행 가능한 _tick_sign()도 제공합니다. 보고된 Lee-Ready 분류 정확도는 시장과 기간에 따라 72-85%입니다.
대량 거래량 분류(BVC)
개별 거래를 배정할 수 없는 경우(집계 봉, 대부분의 공개 캔들 API), Easley, Lopez de Prado, O'Hara의 대량 거래량 분류는 정규화된 가격 변화로부터 봉 거래량의 매수 비율을 추정합니다.
는 표준 정규 CDF이고 는 최근 가격 변화로 추정합니다. tick 수준 분류보다 정확도는 낮지만 OHLCV에 적용할 수 있습니다.
OFI가 아닌 것

OFI는 서로 가까운 세 가지 양과 자주 혼동됩니다. 각각은 이 블로그의 다른 글에서 제대로 다루며, 여기서는 차이가 중요합니다.
**정적 주문서 불균형(OBI)**은 스냅샷 버전입니다. 대기 중인 매수량과 대기 중인 매도량을 비교하며 이벤트를 추적하지 않습니다. 공식과 다중 레벨 형식은 DeepLOB의 전통적 LOB 특성을 참조하세요. 이것이 OFI의 핵심 대비입니다. OBI는 주문서의 상태를 알려주고, OFI는 그 상태에 어떻게 도달했는지를 알려줍니다.
**거래 불균형(TI)**과 거래량 가중 변형은 부호 있는 거래 흐름이며, 머신러닝을 이용한 스프레드 모델링에서 롤링 특성으로 정의·구현됩니다. 여기서 중요한 발표 결과가 하나 있습니다. OFI와 TI를 중간 가격 변화의 회귀 변수로 함께 넣으면 TI가 통계적으로 유의하지 않게 됩니다. TI의 정보가 OFI에 흡수되는 것입니다. 거래가 아니라 주문서 이벤트를 추적해야 한다는 실증적 근거입니다. 거래가 될 뻔한 이벤트도 실제 거래 이벤트와 같은 정보를 전달합니다.
거래량 가중 중간값은 불균형 척도가 아니라 공정가치 추정량이며 DeepLOB에서 다룹니다. 명칭에 주의해야 합니다. 이것은 Stoikov의 micro-price가 아닙니다. micro-price는 순진한 가중 중간값이 편향되어 있기 때문에 만들어진 마팅게일 조정 추정량입니다.
Python 구현

개 레벨로 일반화한 CKS 분해의 직접 구현입니다.
OFI 핵심 계산
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
윈도우로 집계
@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
집계자는 같은 윈도우에서 mlofi와 delta_mid를 출력합니다. 이것이 동시적 회귀입니다. 예측 회귀를 얻으려면 윈도우 의 mlofi를 윈도우 의 delta_mid와 짝지어야 하며, 적합도가 크게 나빠질 것으로 예상해야 합니다.
거래 분류(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
신호 정규화
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가 연결되는 곳

시장 조성. OFI는 공정가치에 대한 예측 편향 항으로 들어가며, Avellaneda-Stoikov 시장 조성자에서 이미 도출하고 구현한 재고 편향 위에 더해집니다: . 두 항은 서로 다른 질문에 답합니다. 흐름 항은 가격이 어디로 향하는지를, 재고 항은 얼마를 보유할 수 있는지를 말합니다. 해당 글은 양의 재고가 양쪽 호가를 낮추는 이유를 포함해 두 번째 항을 다룹니다. 큰 는 트레이더 식별의 디지털 지문과이상 탐지에 정리된 방어 대응(호가 폭 확대, 축소, 한쪽 철회)을 작동시키는 합리적인 역선택 트리거이기도 합니다.
실행. OFI는 별도의 긴급성 제어기가 아니라 전술 계층의 체결 확률 추정 에 대한 입력입니다. 지정가 주문과 교차 주문 사이의 결정은 명시적인 손익분기 계산 - , - 을 따르며 자식 주문 실행 전술에 있습니다. 이 글은 전술 계층이 자체 시장 관점에서 긴급성을 다시 계산해서는 안 된다고도 주장합니다. 서로 다른 두 제어기가 충돌하기 때문입니다.
실무 고려 사항

신호 반감기와 롤링 재보정. 이것이 OFI에 특유한 부분입니다. 유동성 높은 상품에서 OFI의 예측 정보는 밀리초에서 초 단위로 쇠퇴하므로 집계 창은 자유 파라미터가 아니라 예측 시계에 대한 베팅입니다. 도 상수가 아닙니다. 장중 시간대, 깊이, 예정된 이벤트 전후에 변하므로 실제 환경의 적합은 백테스트 값을 고정하지 않고 롤링 윈도우에서 다시 추정해야 합니다.
그 밖의 내용은 다른 곳에서 다룹니다. 지연 예산과 공동 배치/FPGA/커널 바이패스 단계는 DeepLOB의 프로덕션 부분을, U자형 장중 유동성 패턴과 변하는 정규화 통계는 스프레드 모델링을 참조하세요. 거래소별 재보정과 나스닥에서 적합한 모델이 손대지 않은 암호화폐 페어로 이전되지 않는 이유는 다시 DeepLOB에 있으며, 분할 문제는 스마트 주문 라우팅에 있습니다.
조작. 이 모델에 특유한 결과가 하나 있으며 분명히 밝혀둘 가치가 있습니다. CKS 분해는 의도나 지속성 개념 없이 크기만으로 모든 이벤트에 가중치를 줍니다. 따라서 스푸퍼가 대량 주문을 넣고 취소하면 그 수량이 구성상 전체 가중치로 신호에 직접 들어갑니다. 탐지 휴리스틱 - 취소율, 주문 수명, 가격 접근 시 벽의 행동 - 은 대기열 위치와 주문서 벽 분석에 있고, 거래소 간 유령 유동성은 스마트 주문 라우팅에 있습니다.
핵심 요점

-
OFI는 스냅샷이 아니라 이벤트 분해입니다. 세 분기 지표 공식은 주문서 최상단의 모든 변화(가격 상승, 가격 하락, 수량 변화)를 연속된 두 스냅샷만으로 하나의 부호 있는 숫자에 매핑합니다.
-
헤드라인 R 제곱은 동시적입니다. CKS의 50-65% 수치는 같은 구간의 가격 변화를 분해한 값입니다. 예측이 아니며 미래 수익률 모델과 비교해서는 안 됩니다.
-
다중 레벨 OFI는 레벨을 추가하고 PCA는 이를 축소합니다. 발표된 증분 와 89% 분산 결과는 나스닥 주식에서 나온 것입니다. 깊이가 암호화폐 주문서에도 도움이 되는지는 여기서 검증하지 않았습니다.
-
OFI는 거래 불균형을 흡수합니다. 함께 넣으면 TI는 유의하지 않게 됩니다. OFI는 볼 수 있지만 TI는 볼 수 없는 취소 이벤트가 차이를 만듭니다.
-
모델은 의도를 보지 못합니다. 수량에 동일한 가중치를 주는 것이 바로 스푸핑을 가능하게 하는 이유입니다.
-
이 글의 어떤 내용도 측정되지 않았습니다. 인용한 모든 숫자는 주식 문헌에서 나온 것입니다. 이 신호에 자본을 투입하기 전에 자신의 주문서 데이터로 적합하고, 시차 사양을 별도로 보고하며, 수수료와 스프레드를 감안해도 우위가 남는지 확인해야 합니다.
추가 읽을거리

- 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). "한도 주문서의 다단계 주문 흐름 불균형." arXiv:1907.06230.
- Kolm, P., Turiel, J., & Westray, N. (2023). "심각한 주문 흐름 불균형: 한도 주문서에서 여러 지점의 알파 추출." 수학적 금융, 33(4).
- Lee, C., & Ready, M. (1991). "장중 데이터에서 거래 방향을 추론합니다." 재정저널, 46(2), 733-746.
- Easley, D., Lopez de Prado, M., & O'Hara, M. (2012). "고주파 세계에서의 흐름 독성 및 유동성." 금융학개론, 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.