返回 Expert 笔记
Expert Day 82

高频信号 — OFI 与 Microprice

Cont-Kukanov OFI 推导、Stoikov microprice 闭式、trade flow imbalance、信号衰减

2026-07-22
Phase 2 - 市场微观结构与做市 (Day 75-88)
做市OFIMicroprice高频信号Cont

日期: 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 $$

通常 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
Microprice100ms - 1s
TFI1s - 30s
Volume profile / VWAP1m - 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. 过拟合 β 1 month 估出 β=0.0001,下个月 regime 变 β=0.00005。结果 quote 偏向反向 → 库存累积。修复:rolling 7-day 估计、参数 cap。

  2. OFI noise dominated 静市 OFI 几乎全是 quote stuffing(HFT 互相 quote 战),无实际 mid 移动信号。修复:仅计入 trade-relevant 事件(best 档变动 ≥ 1 tick)。

  3. Microprice 在 ultra-thin book 失效 bid_q=0.001, ask_q=10 时 micro 几乎等于 ask_p — 无意义。修复:q < min_threshold 时回退到 mid。

  4. 跨 venue 信号污染 Binance OFI 立刻反映 → Bybit OFI 滞后 50-200ms → 用 lagged 信号在 Bybit 报价等于事后看图。

  5. 信号 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 三种执行方法的对比仿真。