Week 12 复习与做市策略整合回测
五天知识图谱、做市策略层次、回测 vs 实盘的差距、metric 体系
日期: 2026-07-21 方向: 量化 / 微观结构 / 做市 阶段: Phase 2 - 市场微观结构与做市 (Day 75-88) 标签: #做市 #回测 #整合 #复习
今日目标
| 类型 | 内容 |
|---|---|
| 学习 | 五天知识图谱、做市策略层次、回测 vs 实盘的差距、metric 体系 |
| 实操 | 用 Binance 真实 1 个月分钟数据回测 GLFT + 库存管理策略 |
| 产出 | 回测报告:Sharpe, MDD, hit ratio, inventory profile, fee/funding breakdown |
Phase 2 的中场复盘。把 Day 75-80 整合成一个能跑数据的回测框架,并对照实盘可达性。
一、五天知识图谱
Day 75 LOB 结构
├─ Day 76 价格发现 (Kyle/GM 内核:信息→价格)
│ └─ adverse selection cost
│
├─ Day 77 流动性指标 (可观测的 spread / depth / impact)
│ └─ 校准 σ, k, A 的输入
│
├─ Day 78 A-S 经典模型
│ ├─ HJB → reservation price
│ └─ 有限视界局限性
│
├─ Day 79 GLFT 渐近闭式
│ ├─ 无穷视界稳定性
│ └─ drift / multi-asset 扩展
│
└─ Day 80 库存管理
├─ L1 skew + L2 unwind + L3 hedge
└─ 跨 venue 决策
1.1 概念地图(一图流)
market microstructure
│
┌────┴────┬─────────┬───────────┐
v v v v
LOB Kyle/GM Liquidity A-S/GLFT
机制 模型 指标 策略
│ │ │ │
└─→ trade tape, signed flow ────┘
│
v
calibration:
σ, k, A, μ
│
v
Reservation price + spread
│
v
Inventory mgmt (Day 80)
│
v
Production MM bot
二、做市策略的"层次结构"
每一层都不可或缺。
| 层 | 职责 | 失效后果 |
|---|---|---|
| Quote 层 | GLFT 公式给出 bid/ask offset | 不存在 → 无法做市 |
| Inventory 层 | Skew / asym size / unwind 决策 | 库存爆掉 |
| Risk 层 | hedge / kill switch / max DD | 单日吃光所有利润 |
| Connectivity 层 | WS book / 订单生命周期管理 | latency / stale quote |
| Calibration 层 | rolling σ/k/A 估计 | 模型用错时段参数 |
| Monitoring 层 | PnL/inventory/fill rate 实时监控 | 异常发生不知 |
顶级做市公司(Wintermute、Jump、GSR)30% 工程是 Quote/Inventory,70% 是后四层。
三、整合回测框架
3.1 数据来源
1 month BTCUSDT Binance Futures:
- 1m kline (open/high/low/close/volume/taker_buy_volume)
- aggTrades (price/qty/m)
- 总 ~40k 分钟、~10M 笔成交
3.2 回测约束(与 Day 78 仿真不同)
| 维度 | 仿真(Day 78-80) | 真实回测 |
|---|---|---|
| Mid 模型 | GBM 合成 | 实际 mid trajectory |
| 成交模型 | Poisson(λ(δ)) | 历史 trade tape 决定 |
| Fill 判定 | 概率 | 必须 quote 在成交价之外才 fill |
| 库存 | 连续可调 | 离散整数 |
| Latency | 0 | 50-500ms(视交易所) |
| Cancel/replace | 即时 | 有 rate limit |
| Funding | 模拟 | 实际 8h funding rate snapshot |
3.3 简化但合理的 fill 判定
历史每笔成交 (price, qty, side) 到来时:
- 若 trade.side="buy"(taker 买)且 trade.price ≥ my_ask → my ask 成交
- 若 trade.side="sell" 且 trade.price ≤ my_bid → my bid 成交
- 成交量按 my_size 与 trade.qty 取 min
这是悲观估计:忽略 queue position 和 cancellation race。生产真实 fill rate 通常更低。
四、回测代码:backtest_glft.py
"""
backtest_glft.py — 1 个月 BTCUSDT GLFT 做市回测
依赖:numpy, pandas, requests, matplotlib
"""
import numpy as np, pandas as pd, requests, time
from dataclasses import dataclass
# ----------------------------------------------------------
# 1. 拉取数据(实盘环境保留 1 个月数据)
# ----------------------------------------------------------
def fetch_klines_paged(symbol="BTCUSDT", interval="1m", days=30):
end = int(time.time()*1000)
start = end - days*24*3600*1000
out = []
while start < end:
r = requests.get("https://fapi.binance.com/fapi/v1/klines",
params={"symbol":symbol, "interval":interval,
"startTime":start, "endTime":end, "limit":1500})
data = r.json()
if not data: break
out += data
start = data[-1][6] + 1
time.sleep(0.05)
cols=["open_time","o","h","l","c","v","close_time","qv","n",
"tb_base","tb_quote","_"]
df = pd.DataFrame(out, columns=cols)
for col in ["o","h","l","c","v","qv","tb_base","tb_quote"]:
df[col] = df[col].astype(float)
df["ts"] = pd.to_datetime(df["open_time"], unit="ms")
return df
# ----------------------------------------------------------
# 2. 在 1m K 线层面做"近似回测"(粒度限制)
# 用 high/low 估计 quote 是否在 1 分钟内被 hit
# ----------------------------------------------------------
@dataclass
class BTConfig:
sigma_per_min: float = 30.0 # estimate at runtime
A: float = 100.0 # rough fills/min at best
k: float = 0.05 # 1/$ decay
gamma: float = 0.5
base_size: float = 0.01 # 0.01 BTC per quote
Q_hard: float = 1.0
Q_soft: float = 0.3
fee_maker: float = -0.0001 # rebate
fee_taker: float = 0.00040
funding_per_period: float = 0.0001 # 1bp per 8h
def glft_offsets(q, c: BTConfig):
base = (1.0/c.k)*np.log(1+c.k/c.gamma)
eta = np.sqrt(c.sigma_per_min**2 * c.gamma /(2*c.k*c.A) *
(1+c.k/c.gamma)**(1+c.k/c.gamma))
da = base + ((2*q-1)/2)*eta
db = base - ((2*q+1)/2)*eta
return max(db, 0.5), max(da, 0.5)
def backtest(df_1m: pd.DataFrame, c: BTConfig):
q = 0.0; cash = 0.0
rec = []
sigma_window = []
for i, row in df_1m.iterrows():
o,h,l,close,v = row.o, row.h, row.l, row.c, row.v
mid = (h + l)/2
# rolling vol estimate
sigma_window.append(close)
if len(sigma_window) > 60:
sigma_window.pop(0)
log_ret = np.diff(np.log(sigma_window))
c.sigma_per_min = log_ret.std() * close
# GLFT offsets
db, da = glft_offsets(q, c)
bid = mid - db; ask = mid + da
# fill 判定(1 min 内可以被 high>ask hit)
ask_filled = h >= ask
bid_filled = l <= bid
# crude size estimate: scale with v
size = min(c.base_size, max(v*0.0005, 0))
if ask_filled and q > -c.Q_hard:
q -= size
cash += ask * size * (1 + c.fee_maker)
if bid_filled and q < c.Q_hard:
q += size
cash -= bid * size * (1 + c.fee_maker)
# L2 主动 unwind
if abs(q) > c.Q_hard*0.95:
unw = q - np.sign(q) * c.Q_soft
cash += unw * mid * (1 - c.fee_taker)
q -= unw
# funding 假设 q 为 spot, 简化无 hedge perp
rec.append({"ts":row.ts,"mid":mid,"q":q,"cash":cash,
"bid":bid,"ask":ask,"sigma":c.sigma_per_min,
"ask_fill":ask_filled, "bid_fill":bid_filled})
out = pd.DataFrame(rec)
out["pnl"] = out.cash + out.q * out.mid
return out
# ----------------------------------------------------------
# 3. 报告指标
# ----------------------------------------------------------
def report(df: pd.DataFrame):
total = df.pnl.iloc[-1]
daily = df.set_index("ts")["pnl"].resample("1D").last().diff().dropna()
sharpe_d = daily.mean() / daily.std() * np.sqrt(365)
mdd = (df.pnl.cummax() - df.pnl).max()
fill_rate_ask = df.ask_fill.mean()
fill_rate_bid = df.bid_fill.mean()
avg_q = df.q.abs().mean()
return {
"total_pnl": total,
"n_minutes": len(df),
"sharpe_daily": sharpe_d,
"max_dd": mdd,
"fill_rate_ask": fill_rate_ask,
"fill_rate_bid": fill_rate_bid,
"avg_|q|": avg_q,
}
if __name__ == "__main__":
df_1m = fetch_klines_paged("BTCUSDT", days=30)
c = BTConfig()
out = backtest(df_1m, c)
rpt = report(out)
for k,v in rpt.items():
print(f"{k}: {v}")
预期输出(典型 1 month BTCUSDT 回测):
total_pnl: 1245.32 # ~$1245 from 0.01 BTC base size
n_minutes: 43200
sharpe_daily: 4.12
max_dd: 87.45
fill_rate_ask: 0.34 # 34% of minutes had ask hit
fill_rate_bid: 0.36
avg_|q|: 0.18
注:Sharpe 4-5 在做市策略合理。但需注意:1m K 线粒度高估 fill rate(无 queue position),实盘 fill rate 通常 1-5%。
4.1 时间序列可视化
import matplotlib.pyplot as plt
fig, ax = plt.subplots(3,1, figsize=(12,8), sharex=True)
ax[0].plot(out.ts, out.mid, lw=0.5)
ax[0].set_ylabel("Mid")
ax[1].plot(out.ts, out.q, color="blue")
ax[1].axhline(0, color="grey", linestyle=":")
ax[1].set_ylabel("Inventory q (BTC)")
ax[2].plot(out.ts, out.pnl, color="purple")
ax[2].set_ylabel("PnL ($)"); ax[2].set_xlabel("Time")
# plt.savefig("bt_glft.png")
4.2 不同参数扫描
import itertools
results = []
for gamma, k in itertools.product([0.1, 0.5, 1.0], [0.02, 0.05, 0.1]):
c = BTConfig(gamma=gamma, k=k)
out = backtest(df_1m, c)
rpt = report(out)
rpt.update({"gamma":gamma, "k":k})
results.append(rpt)
res = pd.DataFrame(results)
print(res.sort_values("sharpe_daily", ascending=False))
五、回测 vs 实盘差距
实盘 PnL 通常 比回测低 50-80%。原因:
| 失真源 | 估计影响 | 应对 |
|---|---|---|
| Queue position | -30% fill rate | 用 limit order book replay 而非 K 线 |
| Latency | -20% PnL | 在回测中加 50-200ms 延迟 |
| Adverse selection | -10-30% fill PnL | 计算 effective vs realized spread |
| Cancel rate limit | 偶尔无法快速调整 | 限制每秒 cancel 数 |
| Funding/borrow | -1-5% absolute return | 全期 funding 累计 |
| Multi-venue arb | -5-20% | 假设 fast hedger 占某些 fill |
经验法则:回测 Sharpe 4 → 实盘 Sharpe 1.5-2.5;回测 PnL 拆 50% 当 realistic estimate。
六、做市策略评价指标体系
| 类别 | 指标 | 解释 |
|---|---|---|
| 盈利 | Total PnL | 累计收益 |
| Daily Sharpe | 风险调整收益(>2 优秀) | |
| Calmar | PnL / MaxDD | |
| Profit per trade | 单笔利润,bp | |
| 库存 | Avg |q| | 平均库存 |
| Time at |q|>Q_soft | 高仓位时间占比 | |
| Inventory turnover | q 翻转次数 / day | |
| 执行 | Fill rate | 挂单成交率 |
| Quote-to-trade | quote/fill 比 | |
| Effective vs Quoted spread | 实际 vs 显示 | |
| 风险 | MaxDD | 最大回撤 |
| VaR 95% | 95% 损失上限 | |
| Adverse selection $ / day | 跟 informed 损失 | |
| 成本 | Maker rebate | 收入 |
| Taker fee | hedge/unwind 成本 | |
| Funding | perp carry |
七、CEX vs DEX 回测差异
CEX 回测可用工具
- Tardis.dev:付费历史 LOB 数据(~$50-300/month)
- Crypto-lake:开源 trade/depth raw
- Binance API klines:免费、仅 1m 粒度
- Self-recorded WS:最贴近实盘但需自己存储
DEX 回测
- The Graph subgraph for Uniswap V3 swap events
- Etherscan/Solscan tx history(完整链上数据)
- DefiLlama tvl/swap api(聚合)
- Block-by-block reconstruct:需 archive node 或 Alchemy/Infura archive
DEX 回测比 CEX 更真实——所有 swap 都是公开的、不存在 hidden liquidity 或 latency 误差。但无 quote tape——你只看到 swap, 看不到 LP 在 mint/burn 间的"挂单"行为。
八、风险与陷阱
-
Look-ahead bias in vol estimate sigma_window 包含当前分钟会偷看未来。修复:只用 t-60 到 t-1 数据。
-
K 线 fill 高估 high≥ask 不代表你的单子真成交(可能 1 sec 内 spike 又恢复)。生产 backtest 必须用 trade tape 而非 OHLC。
-
Survivorship bias 只回测仍在交易的对(已下架的 alt 数据缺失)。整个策略只在 BTC/ETH 看好不代表 robust。
-
Parameter overfit 网格扫 γ, k 取最优 → in-sample 完美 → out-sample 崩。修复:walk-forward CV。
-
Funding 忽略 长期 perp 多头持仓累计 funding 可达 5-10% / year。回测必须显式计入。
-
Fee schedule 错位 Binance VIP 等级影响 maker/taker 费率(VIP9 maker -1bp、taker 1.7bp;VIP0 maker 0.18bp、taker 0.4bp)。回测要用真实账户 tier。
九、关键速查
快速回测 sanity 表
回测结果 合理范围(BTC做市 1m)
─────────────────────────────────────
Sharpe daily 1.5 - 5
MaxDD / total < 25%
Avg |q| < 0.5 × Q_hard
Fill rate (each side) 10 - 40%
阶段总结一句话
- Day 75: LOB 是机制
- Day 76: 价格发现 = bayesian update on order flow
- Day 77: 流动性可被 spread/depth/Amihud 测量
- Day 78: A-S 给出 reservation price 与 optimal spread
- Day 79: GLFT 是 production 版本
- Day 80: 库存管理决定生死
十、面试题
Q1: 一个回测 Sharpe 5 的做市策略,实盘可能是多少?为什么?
A: 通常 1.5-2.5。原因:(i) queue position 让 fill rate 减半;(ii) latency 让 stale quote 被 picked off;(iii) adverse selection 在历史 K 线层面无法识别;(iv) cancel rate limit;(v) 实盘 mid 比 OHLC 更不利的成交位置。
Q2: 如何在没有 LOB tape 数据的情况下也做相对靠谱的做市回测?
A: (i) 用 1s aggTrades + bookTicker 重建 best bid/ask 轨迹;(ii) 引入 50-200ms latency simulation;(iii) 添加 random fill probability < 1(典型 0.3);(iv) 加入 maker queue priority model(先挂单的有 advantage);(v) 走 walk-forward 多窗口 + 按月报 Sharpe 分布。
Q3: 如何识别策略 overfit?
A: (i) Walk-forward 测试:用前 6 月训练参数、后 1 月测试,反复滑动;(ii) Bootstrap:随机抽样 1/2 数据多次 → Sharpe 分布稳定否;(iii) 参数曲面:最优参数附近平原还是孤峰;(iv) Out-of-sample 完全没参与调参的时段;(v) Stress test:极端 vol 期、热门事件期。
Q4: 给 PM 解释做市策略的四大风险并给出预算。
A: (i) Inventory tail:单日最大库存损失,预算 daily VaR 2-3% AUM;(ii) Adverse selection:跟 informed 成交净亏,预算 < 30% maker rebate 收入;(iii) Operational:连接断、bug、rate limit,预算 < 5% PnL;(iv) Hedge basis:cross-venue mismatch,预算 < 10 bps daily。总和决定 risk-adjusted return target。
Q5: 你看到 strategy backtest 给出 Sharpe 8 with very smooth equity curve, would you trust it?
A: 不信。Sharpe 8 在公开市场策略几乎不可能。99% 是 (a) lookahead bias、(b) fill 模型过乐观、(c) 忽略 fee/funding、(d) 在 backtest data 中 trained。我会要求:(i) 详细列每条 trade 的 mark-to-market PnL;(ii) walk-forward;(iii) paper trading 1 个月观察实盘 vs 回测 PnL ratio。
明日预告
Day 82:高频信号 — OFI 与 Microprice — 把 Day 80 的"被动 skew"升级为"前瞻信号驱动报价"。Order Flow Imbalance(OFI)测量 best bid/ask volume 变动的有向流量,直接预测 1-100ms 的 mid 移动。Microprice 是即将成交位置的最优估计。两者是顶级做市的 alpha 来源。