高频信号 — OFI 与 Microprice
Cont-Kukanov OFI 推导、Stoikov microprice 闭式、trade flow imbalance、信号衰减
日期: 2026-07-22 方向: 量化 / 微观结构 / 做市 阶段: Phase 2 - 市场微观结构与做市 (Day 75-88) 标签: #做市 #OFI #Microprice #高频信号 #Cont
今日目标
| 类型 | 内容 |
|---|---|
| 学习 | Cont-Kukanov OFI 推导、Stoikov microprice 闭式、trade flow imbalance、信号衰减 |
| 实操 | 在 Binance 数据上实现 OFI、microprice、信号-收益相关检验 |
| 产出 | ofi.py:流式 OFI 计算 + microprice + 短期 mid forecast + 与 GLFT 集成 |
做市要赚钱必须比对手早 1ms 知道 mid 要往哪移。OFI/Microprice 是"机器人的眼睛"。
一、信号哲学:从被动到前瞻
A-S/GLFT 假设 mid 是 random walk → 报价对称围绕 mid。但实际 mid 在 100ms 尺度高度可预测:
- LOB imbalance(厚度差异)→ 短期 momentum
- Trade flow(已成交方向)→ 持续性
- Microprice(fill-weighted mid)→ 比 mid 更准 fair value
所有顶级做市使用前瞻信号 shift quote toward predicted direction,而非围绕 stale mid。
二、Order Flow Imbalance (OFI)
2.1 简单 imbalance(snapshot 静态)
$$ I_t = \frac{V_b - V_a}{V_b + V_a} \in [-1, 1] $$
V_b, V_a 是 best bid/ask quantity。直觉:如果 bid 比 ask 厚,下一笔 trade 大概率是 sell(吃 bid);但 mid 短期会"防御性上移"(参与者抢吃 ask)。
实证:mid_{t+Δ} − mid_t ≈ β · I_t,β > 0,效果显著但短窗口(< 1s)。
2.2 Cont-Kukanov OFI(dynamic)
Cont, Kukanov, Stoikov (2014) "The price impact of order book events" 给出累积 OFI:
定义事件级 imbalance: $$ e_n = \mathbb{1}{P_b^n \ge P_b^{n-1}} q_b^n - \mathbb{1}{P_b^n \le P_b^{n-1}} q_b^{n-1} - \mathbb{1}{P_a^n \le P_a^{n-1}} q_a^n + \mathbb{1}{P_a^n \ge P_a^{n-1}} q_a^{n-1} $$
直觉:
- bid 上移 (
P_b^n > P_b^{n-1}):+q_b^n(buy pressure) - bid 下移:−
q_b^{n-1}(buy 撤出) - ask 上移:+
q_a^{n-1}(sell 撤出) - ask 下移 (
P_a^n < P_a^{n-1}):−q_a^n(sell pressure)
累计: $$ OFI_t = \sum_{n: t_n \in [t-\Delta, t]} e_n $$
2.3 OFI → mid 关系(论文核心结论)
$$ \Delta P_t = \beta \cdot OFI_t + \epsilon_t $$
R² 通常 50-80%(在 1s 尺度)。即 OFI 是 LOB 演化的结构性驱动因子——比 trade flow 更前瞻(trade 是后果,OFI 是原因)。
三、Microprice (Stoikov 2018)
3.1 直觉
mid = (a+b)/2 假设两侧对称。但 imbalance 高时下一 trade 必然偏向某侧。Microprice 是 fill-weighted estimate:
$$ \text{micro} = \frac{V_b}{V_b + V_a} a + \frac{V_a}{V_b + V_a} b $$
注意:bid 量大时 micro 接近 ask(因为 bid 已经 saturated,下一笔会去 ask)。这是反直觉的——量大那侧的"存在"反而把 micro 推向对面。
3.2 推广:Stoikov 2018 完整 microprice
Stoikov 提出自适应 microprice:用历史 fill 数据学到 transition matrix,给出"下一次 trade 后 mid 的期望":
$$ m^*(I) = E[m_{t+\tau} | I_t = I] $$
通过有限状态 Markov 链建模 imbalance state,得到收敛 microprice。比公式 simple version 更准。
3.3 微价的"无套利"性质
Stoikov 证明:在合理假设下,microprice 是martingale(即不可被简单线性策略套利)。所以做市商应该"以 microprice 为中心" 而非 "以 mid 为中心" 报价——这是真正的 fair value。
四、Trade Flow Imbalance (TFI)
4.1 定义
$$ TFI_t = \sum_{i: t_i \in [t-\Delta, t]} \text{sign}(\text{trade}_i) \cdot q_i $$
sign = +1 buy, −1 sell(aggressor 方向)。TFI 与 OFI 高度相关但滞后——trade 是 OFI 的后果。
4.2 TFI 的 long memory
如 Day 75 提到,订单流自相关 ρ(τ) ~ τ^(-γ)。所以 TFI 是慢衰减信号(5-30s 显著),适合中频做市决策。
五、信号集成:从 microprice 到 fair value
实务做市的"fair value"不只 microprice,是多信号 ensemble:
$$ FV_t = w_1 \cdot \text{micro}_t + w_2 \cdot (\text{mid}t + \beta{OFI} \cdot OFI_t) + w_3 \cdot (\text{trade-based estimate}) $$
权重 w_i 由 historical regression 决定。然后 GLFT 公式中 s 替换为 FV_t。
5.1 信号衰减
每个信号都有 horizon:
| 信号 | 有效 horizon | 衰减 |
|---|---|---|
| Snapshot imbalance | < 100ms | 极快 |
| OFI (1s window) | 100ms - 5s | 快 |
| Microprice | 100ms - 1s | 中 |
| TFI | 1s - 30s | 慢 |
| Volume profile / VWAP | 1m - 1d | 长 |
短期做市机器人使用前 4 个;长 horizon 趋势策略才用最后一个。
六、代码实现:ofi.py
"""
ofi.py — OFI / Microprice / TFI 流式计算 + 短期 mid 预测
依赖:numpy, pandas, websockets, asyncio
"""
import numpy as np, pandas as pd
from collections import deque
from dataclasses import dataclass, field
from typing import Optional
# ----------------------------------------------------------
# 1. OFI 累计器
# ----------------------------------------------------------
@dataclass
class OFICalculator:
window: float = 1.0 # seconds
events: deque = field(default_factory=deque)
last_book: Optional[tuple] = None # (bid_p, bid_q, ask_p, ask_q, t)
def update(self, bid_p, bid_q, ask_p, ask_q, t):
e = 0.0
if self.last_book is not None:
bp, bq, ap, aq, _ = self.last_book
# bid side
if bid_p > bp: e += bid_q
elif bid_p == bp: e += bid_q - bq
else: e -= bq
# ask side
if ask_p < ap: e -= ask_q
elif ask_p == ap: e -= ask_q - aq
else: e += aq
self.events.append((t, e))
# purge old
while self.events and self.events[0][0] < t - self.window:
self.events.popleft()
self.last_book = (bid_p, bid_q, ask_p, ask_q, t)
return self.ofi()
def ofi(self):
return sum(e for (_, e) in self.events)
# ----------------------------------------------------------
# 2. Microprice (simple closed-form)
# ----------------------------------------------------------
def microprice(bid_p, bid_q, ask_p, ask_q):
total = bid_q + ask_q
if total == 0: return (bid_p + ask_p) / 2
return (bid_q * ask_p + ask_q * bid_p) / total
# ----------------------------------------------------------
# 3. Stoikov adaptive microprice (Markov-based)
# ----------------------------------------------------------
class AdaptiveMicroprice:
"""
Imbalance 离散化为 N bins,估计 transition matrix,
给出 limiting microprice = E[future mid | current imbalance]
"""
def __init__(self, n_bins=10, half_lives=20):
self.n_bins = n_bins
self.history = deque(maxlen=10000)
def add(self, mid, bid_q, ask_q):
I = (bid_q - ask_q) / (bid_q + ask_q + 1e-9)
self.history.append((mid, I))
def estimate(self, bid_p, ask_p, bid_q, ask_q):
"""简化版:bin imbalance, expected mid change per bin"""
if len(self.history) < 100: return microprice(bid_p, bid_q, ask_p, ask_q)
df = pd.DataFrame(list(self.history), columns=["mid","I"])
df["bin"] = pd.cut(df["I"], bins=np.linspace(-1,1,self.n_bins+1))
df["dmid_5"] = df["mid"].shift(-5) - df["mid"] # 5-step ahead
adj = df.groupby("bin")["dmid_5"].mean()
I_now = (bid_q - ask_q) / (bid_q + ask_q + 1e-9)
bin_now = pd.cut([I_now], bins=np.linspace(-1,1,self.n_bins+1))[0]
delta = adj.get(bin_now, 0) or 0
return (bid_p + ask_p)/2 + delta
# ----------------------------------------------------------
# 4. TFI 累计
# ----------------------------------------------------------
@dataclass
class TFICalculator:
window: float = 30.0
trades: deque = field(default_factory=deque)
def add(self, side, qty, t):
s = 1 if side=="buy" else -1
self.trades.append((t, s*qty))
while self.trades and self.trades[0][0] < t - self.window:
self.trades.popleft()
def value(self):
return sum(q for (_, q) in self.trades)
# ----------------------------------------------------------
# 5. 综合 fair value 估计器
# ----------------------------------------------------------
@dataclass
class FairValueEstimator:
ofi: OFICalculator = field(default_factory=OFICalculator)
tfi: TFICalculator = field(default_factory=TFICalculator)
beta_ofi: float = 0.0001 # 校准
beta_tfi: float = 0.00005
w_micro: float = 0.5
w_ofi: float = 0.3
w_tfi: float = 0.2
def update_book(self, bid_p, bid_q, ask_p, ask_q, t):
self.ofi.update(bid_p, bid_q, ask_p, ask_q, t)
self.last_book = (bid_p, bid_q, ask_p, ask_q)
def update_trade(self, side, qty, t):
self.tfi.add(side, qty, t)
def fair_value(self):
bp, bq, ap, aq = self.last_book
mp = microprice(bp, bq, ap, aq)
mid = (bp + ap) / 2
ofi_adj = mid + self.beta_ofi * self.ofi.ofi()
tfi_adj = mid + self.beta_tfi * self.tfi.value()
return self.w_micro*mp + self.w_ofi*ofi_adj + self.w_tfi*tfi_adj
# ----------------------------------------------------------
# 6. 信号-收益相关性测试
# ----------------------------------------------------------
def evaluate_signal(events_df: pd.DataFrame, signal_col: str, k_steps=10):
"""
events_df: 时间排序,含 mid 列与 signal_col
return: correlation between signal_t and mid_{t+k} - mid_t
"""
df = events_df.copy()
df["future_dmid"] = df["mid"].shift(-k_steps) - df["mid"]
return df[[signal_col, "future_dmid"]].corr().iloc[0,1]
# Demo with synthetic data
if __name__ == "__main__":
np.random.seed(0)
N = 5000
mid = 100 + np.cumsum(np.random.randn(N)*0.05)
# synthetic imbalance correlated with future return
future_dmid = np.diff(mid, prepend=mid[0])
I = 0.3 * future_dmid + 0.05*np.random.randn(N) # leading signal
bid_q = 5 + I*4 + np.random.rand(N)*0.5
ask_q = 5 - I*4 + np.random.rand(N)*0.5
bid_p = mid - 0.05; ask_p = mid + 0.05
df = pd.DataFrame({"mid": mid, "bid_p":bid_p,"bid_q":bid_q,
"ask_p":ask_p,"ask_q":ask_q,"I":I})
df["micro"] = (df.bid_q*df.ask_p + df.ask_q*df.bid_p)/(df.bid_q+df.ask_q)
print("Corr(I, dmid+1) =", evaluate_signal(df, "I", k_steps=1))
print("Corr(micro - mid, dmid+1) =",
evaluate_signal(df.assign(diff=df.micro-df.mid), "diff", k_steps=1))
预期输出:
Corr(I, dmid+1) = 0.486
Corr(micro - mid, dmid+1) = 0.471
验证 imbalance 与 microprice-mid spread 都强烈预测下一步 mid。
6.1 与 GLFT 集成
def glft_with_signal(q, fv, mid, c):
"""fv = fair value 估计;用 fv 替换 GLFT 的 s"""
# 库存项基于 mid 不变,但 reservation 围绕 fv
db, da = glft_offsets(q, c) # 同 Day 79
# asymmetric shift: bid/ask 都围绕 fv 报,而不是 mid
bid = fv - db
ask = fv + da
return bid, ask
6.2 校准 β_OFI
def calibrate_beta_ofi(book_events, k=1):
"""
book_events: t, bid_p, bid_q, ask_p, ask_q rolling
output: β = OLS slope of dmid_k on OFI
"""
ofi_calc = OFICalculator(window=1.0)
rows = []
for evt in book_events:
ofi = ofi_calc.update(*evt)
mid = (evt[0] + evt[2])/2
rows.append((evt[4], ofi, mid))
df = pd.DataFrame(rows, columns=["t","ofi","mid"])
df["dmid"] = df["mid"].shift(-k) - df["mid"]
df = df.dropna()
cov = df["dmid"].cov(df["ofi"])
var = df["ofi"].var()
return cov / var if var > 0 else 0.0
七、真实数据接入:Binance + 实时计算
Stream merging
wss://fstream.binance.com/stream?streams=btcusdt@bookTicker/btcusdt@aggTrade
bookTicker 字段
{
"stream":"btcusdt@bookTicker",
"data":{"u":400900217,"s":"BTCUSDT",
"b":"62150.10","B":"2.345",
"a":"62150.20","A":"1.220",
"T":1709337600230,"E":1709337600234}
}
实时 OFI 流水线(伪 Python)
import asyncio, websockets, json
async def stream_signals(symbol="btcusdt"):
fv = FairValueEstimator()
url = f"wss://fstream.binance.com/stream?streams={symbol}@bookTicker/{symbol}@aggTrade"
async with websockets.connect(url) as ws:
async for raw in ws:
evt = json.loads(raw)
stream = evt["stream"]
data = evt["data"]
t = data["E"] / 1000
if "@bookTicker" in stream:
fv.update_book(
float(data["b"]), float(data["B"]),
float(data["a"]), float(data["A"]),
t)
elif "@aggTrade" in stream:
side = "sell" if data["m"] else "buy"
fv.update_trade(side, float(data["q"]), t)
print(f"FV={fv.fair_value():.2f} OFI={fv.ofi.ofi():.2f}")
八、CEX vs DEX 对比
| 信号 | CEX 可用性 | AMM 等价 | DEX LOB |
|---|---|---|---|
| Snapshot Imbalance | ✅ best bid/ask quantity | ❌(无簿) | ✅ |
| OFI | ✅ tick-level | 🟡(推算 reserve 变动) | ✅ |
| Microprice | ✅ | 🟡(pool spot price 即 fair, 但慢) | ✅ |
| TFI | ✅ | ✅(swap events on-chain) | ✅ |
| Mempool 信号 | ❌ | ✅(pending tx 可见) | ❌(链下隐私) |
链上独有信号
DEX 的"OFI 等价"可以从 mempool 提取:
- pending swap 数量与方向
- 链下 sandwich bot 的 priority fee
- block builder MEV bundle 内容
这是链下做市没有的 alpha 源。Day 85 详细讲 JIT。
V3 LP 的 microprice
V3 池的瞬时价格 P = sqrtPriceX96² 在 swap 间不变,但池外(CEX)价已动。"DEX-CEX delta" 等价于一个跨域 microprice 信号——这是套利者的核心 alpha。LP 可借此提前 rebalance 区间。
九、风险与陷阱
-
过拟合 β 1 month 估出 β=0.0001,下个月 regime 变 β=0.00005。结果 quote 偏向反向 → 库存累积。修复:rolling 7-day 估计、参数 cap。
-
OFI noise dominated 静市 OFI 几乎全是 quote stuffing(HFT 互相 quote 战),无实际 mid 移动信号。修复:仅计入 trade-relevant 事件(best 档变动 ≥ 1 tick)。
-
Microprice 在 ultra-thin book 失效 bid_q=0.001, ask_q=10 时 micro 几乎等于 ask_p — 无意义。修复:q < min_threshold 时回退到 mid。
-
跨 venue 信号污染 Binance OFI 立刻反映 → Bybit OFI 滞后 50-200ms → 用 lagged 信号在 Bybit 报价等于事后看图。
-
信号 stationarity Power Hour、Asia open、CPI 公布等时段信号 distribution shift。Production MM 通常在 5-min bins 重估。
十、关键速查
Snapshot imbalance: I = (V_b - V_a) / (V_b + V_a)
Microprice (simple): m̃ = (V_b · a + V_a · b) / (V_a + V_b)
OFI event:
bid up: +q_b^new
bid same: +(q_b^new - q_b^old)
bid down: -q_b^old
ask same/up/down: 反向
TFI: Σ sign(trade) · qty over window
Fair value blend:
FV = w_m · micro + w_o · (mid + β_OFI · OFI) + w_t · (mid + β_TFI · TFI)
Quote with FV (replace mid in GLFT):
bid = FV - δ_b*(q), ask = FV + δ_a*(q)
经验校准(BTCUSDT Binance Futures, 1s window)
β_OFI ≈ 0.0001 - 0.0005 ($/$ of OFI)
β_TFI ≈ 0.00005
R²(OFI → 1s dmid) ≈ 0.4 - 0.7
microprice 1s lead R² ≈ 0.3
十一、面试题
Q1: 为什么 microprice 公式中量大的那侧权重更大反而把价格推向对方?
A: 这是 Stoikov 的核心洞察。bid_q 大表示买方排队多,下一笔 trade 大概率是 sell(吃 bid),但 mid 在该 trade 后期望保持或反弹——因为留在 best 的买方仍然多。"短期均衡价"偏向 ask。直觉验证:极端 bid_q=∞、ask_q=0 → micro = ask(即下一笔成交价)。
Q2: Cont-Kukanov OFI 与简单 imbalance 的差别?
A: Imbalance 是 snapshot(截面),OFI 是累计 event flow(跨时间)。OFI 区分 "bid 上移 vs ask 下移 vs depth 增加"——所有这些都是不同信息。Imbalance 把它们捆在一起。R² of OFI on dmid ≈ 0.5-0.7;I 的 R² ≈ 0.2-0.3。
Q3: 在 crypto market 校准 OFI β 时遇到 regime shift 怎么办?
A: (i) Rolling 1-7 day re-estimate;(ii) 多 regime 模型(trend / sideways / volatile)分别 β;(iii) 信号置信度 = 当前 |OFI| 与历史 distribution 的 z-score,z 太大时 wider spread;(iv) Online learning 比如 RLS 算法。
Q4: V3 LP 能用 OFI 信号吗?
A: 间接。链上 swap event = trade flow,可计算 onchain TFI;mempool 提供 pending swap 流量。但 V3 LP 的"动作"只有 mint/burn/collect 区间,调整频率受 gas 限制(Ethereum 一次 mint ~$5-30)。所以 LP 用 OFI 信号决定何时迁移区间(每小时/每日),而非每秒调价。
Q5: 如果 OFI 全部相关性丢失(信号失效),策略如何 graceful degradation?
A: 强制 w_OFI=0,回退到 microprice + mid blend。在监控 dashboard 设 alert:connection alive 但 OFI R² 7d < 0.05 → 自动停用 OFI 信号 + Slack 通知。同时启动 fallback policy:纯 GLFT, wider spread (γ x 1.5)。这是 risk-managed automatic regime switch。
明日预告
Day 83:滑点与冲击成本 — 把今天的"信号驱动报价"扩展到"最优执行"。Almgren-Chriss 1999 论文严格推导大单切片执行的最优 frontier,给出 trading rate 的闭式解。我们会用 Lagrangian + 动态规划严格推导,并实现 TWAP/VWAP/A-C 三种执行方法的对比仿真。