某周五下午,深圳某 量化 私募 的 风控 周会。一位 研究员 端着一份 价值-动量 复合 策略 的 回测 报告 进 会议室:L1 都做对了——事件驱动 引擎、信号 计算 处处 .shift(1) 纪律。在 沪深300 成分股 上 2014-2023 回测,年化 夏普比率 1.3,曲线 干净、可上线。风控 总监 不问 信号本身 的 任何 一个 字,连珠炮 问了 五个 问题:股票池 里 把 退市 / ST 戴帽 / 借壳 / 被 收购 的 名字 算 进去 没?成交 是 下根 K 线 开盘 还是 当根 K 线 收盘?双边 成本 假设 多少?空头 端 是不是 假设 零 融券 成本?基本面 字段 用 的 是 报告 期 末 还是 公告 日期? 问到 第三 个,研究员 已经 沉默 了:股票池 是 当前 沪深300 倒推 到 2014(典型 survivorship),成交 用 当日 收盘价(典型 look-ahead),成本 为 零,融券 免费,基本面 用 report_period_end。等到 研究员 把 这些 全 改 对,夏普 落到 0.18——5 倍 的 折损。L1 教 给 你 的 引擎 杜绝 了 头条 前视;本课 教 的 十 项 真实性 清单(realism checklist)才能 抓 住 引擎 诚实 之后 仍然 藏 在 暗处 的 那 1.5x-3x 通胀。
五大 经典 偏差,按 夏普 通胀 量级 降序
1. look-ahead bias — 1.5-3x; fix: available_at <= t + .shift(1)
2. survivorship bias — 1.5-2x; fix: universe(date, symbol) from 4.1.1 L4
3. unrealistic-fill bias — 1.2-1.8x; fix: next-bar-open + <=10% cap + slippage
4. no-cost bias — 1.2-1.5x; fix: 10 bps round-trip placeholder, 4.5.2 for real models
5. borrow-and-short-availability bias — 1.1-1.3x; fix: is_hard_to_borrow + borrow_cost_bps
这是 五大 家族。百分比 是 A 股 / US 股票 策略 的 经验 典型 值;病态 案例 可以 更大。L1 的 .shift(1) 纪律 解决 的 是 look-ahead bias 最 粗暴 的 那一种;剩下 4.5 个 家族 一个 都 没 解决——而 look-ahead bias 本身 在 加 .shift(1) 之后 仍然 以 更 隐蔽 的 形式 存在。每 一种 都 需要 工程 修复 加 诊断 测试。
look-ahead bias 是 最深 的 家族。L1 抓 到 的 向量化 一根 K 线 平移 是 其中 一种;更 隐蔽 的 物种 到处 都是。基本面 字段 时间戳 错用 报告 期 末(refers_to = 2023-09-30)而 非 公告 日期(available_at = 2023-11-08)会 漏 出 3-6 周 的 未来 信息——A 股 季报 的 法定 披露 上限 是 1 个 月(Q1 / Q3)、2 个 月(半年报)、4 个 月(年报),所以 这 个 错位 在 A 股 上 可以 漏 到 1-4 个 月。另类 数据 用 事件 时间(一条 微博 09:30:15 发出)而 非 可用 时间(09:30:15 + 100 ms 落地 延迟)会 漏 出 落地 窗口。幸存者 过滤 后 的 股票池——只 保留 现在 仍 上市 的 股票——是 一种 通过 选择 泄露 哪些 公司 后来 破产 的 信息 泄漏。修复:每一行 输入 都 带 available_at,引擎 拒绝 提供 available_at > t 的 行。诊断 测试:assert all(feature_timestamps <= label_timestamps) 在 回测 日志 的 每一步。
survivorship bias 是 股票 策略 第二大 家族、也 是 最 容易 被 忽视 的。基于 今 天 的 沪深300 倒推 到 2010 的 回测 会 漏 掉 所有 在 2010 年 在 沪深300 里、之后 退市、被 收购、破产、被 借壳 的 名字。被 漏 掉 的 名字 通过 选择 都是 业绩 差 的——把 它们 算 回 股票池 会 拉 低 实现 收益。修复 是 4.1.1 L4 的 universe(date, symbol) 表——按 日期 × symbol 索引 的 长格式 表,每 一行 一个 (日期, 当日 可交易 symbol) 元组。引擎 在 每一步 用 universe(t) 过滤 可交易 集合。诊断 测试:assert len(universe(t1)) != len(universe(t2)) 至少 在 一些 (t1, t2) 对 上 成立——股票池 cardinality 完全 不随 时间 变化,按 构造 就是 幸存者 过滤 后 的 股票池。
unrealistic-fill bias 是 第三 家族。引擎 假设 在 收盘价 成交、在 中间价 成交、不 限 成交量。修复 是 经典 fill model:next-bar-open 成交、成交量 占比 上限、滑点 加 在 成交价 上。部分 成交 规则:如果 order_size > cap * bar_volume,按 cap * bar_volume 成交 并 把 未 成交 部分 滚 到 下根 K 线。诊断 测试:assert order_size <= 0.10 * bar_volume 在 回测 日志 的 每一次 成交 上;如果 任何 一次 成交 越过 上限,回测 就 假设 了 策略 不 付 代价 地 推动 了 市场。
no-cost bias 是 第四 家族。引擎 忽略 佣金、买卖 价差、市场 冲击、印花税 0.1% 卖方、过户费 万分之 0.5 双边。本课 的 修复 是 占位:在 每一次 成交 上 加 一个 平均 ~10 bps 的 双边 成本(每边 ~5 bps)。完整 的 成本 模型——线性 vs 平方根 市场 冲击、Almgren-Chriss、价差 + 佣金 + 税费 分解——是 4.5.2 的 工作。诊断 测试:assert backtest_pnl == 0 当 策略 不 持仓 但 在 再 平衡;任何 在 零 净 头寸 再 平衡 上 出现 的 非 零 PnL 都是 成本 层 在 漏。
borrow-and-short-availability bias 是 多空 策略 最 常见 的 一种。修复 是 一张 借券 表:is_hard_to_borrow(date, symbol) 布尔 标志 和 borrow_cost_bps(date, symbol) 年化 利率 曲线。A 股 融券 市场 高度 选择 性——交易所 公布 的 融券 标的 名单 上 才 可 做空、利率 通常 8-9% 年化、部分 中小盘 完全 没法 借。诊断:assert short_position_value <= borrow_capacity(date, symbol)。
时点 数据(PIT)纪律 的 工程 化
时点 数据 纪律 是 数据层 的 结构性 铁律。每一个 输入 都 有 available_at 时间戳——数据 对 公众 可见 的 时刻,不是 数据 所指 的 时刻。引擎 拒绝 在 时刻 t 提供 available_at > t 的 行。
engine.get(symbol, field, as_of=t) -> row with max{r.available_at : r.available_at <= t}
# a 2023-Q3 earnings number with refers_to = 2023-09-30 and available_at = 2023-11-08
# is *unavailable* for as_of between 2023-09-30 and 2023-11-07
经典 案例 是 基本面 数据。A 股 上市 公司 2023 年 三季度 报告 所指 期间 是 2023-09-30,但 可用 时刻 是 公告 日期——按 中国 证监会 信息披露 管理 办法,三季报 法定 披露 上限 是 1 个 月,即 2023-10-31;实务 上 不少 公司 集中 在 10 月 下旬 披露。一份 回测 如果 把 三季报 数据 join 到 报告 期 末 的 日期 而 不是 公告 日期,就 在 每一次 财报 刷新 时 结构 性 地 用 了 几 周 的 未来 信息。Wind 万得 / Choice 数据 / 同花顺 vintage 财务 表 都 提供 显式 的 公告 日期 字段,引擎 join 时 用 公告 日期 即可。其他 PIT 案例:沪深300 指数 调整(中证 指数 公司 的 提前 公告 是 available_at,调入 / 调出 生效 日 是 refers_to——指数 调整 效应 信号 应 用 公告 日期);分析师 评级 / 盈利 预测 修正(available_at 是 报告 发布 时刻 而 非 分析师 私下 改 view 的 时刻);宏观 数据(CPI、PPI、PMI、社融、新增 信贷——都 有 国家 统计局 / 人民 银行 公布 的 精确 时间)。时点 数据库 的 构建 是 4.1.2 L3/L4 的 主题;本课 使用 这 一 纪律 并 假设 数据层 强制 执行。
成交 模型 的 三 个 参数
1. fill_bar = 'next-bar-open' — 日频 策略 的 现实 默认;same-bar-close 除非 market-on-close 否则 即 前视
2. volume_participation_cap = 0.10 — 10% bar 成交量;超过 0.20 必须 上 4.5.2 的 真实 冲击 模型
3. slippage_bps = 5-10 — 基点 或 与 成交量 成 比例;510300 / 沪深300 流动 名字 5-10 bps;流动 较差 名字 20-50 bps
# 部分 成交 规则:
# if order_size > cap * bar_volume: fill = cap * bar_volume; roll remainder to next bar
fill_bar 控制 订单 在 哪 根 K 线 上 成交。next-bar-open 是 日频 策略 的 现实 默认:信号 在 t-1 收盘 之后 算 出、订单 隔夜 挂 出、成交 在 t 开盘。same-bar-close 除非 策略 明确 跑 集合 竞价 收盘 单 / 尾盘 委托(14:57-15:00 的 集合 竞价 收盘 窗口),否则 就 是 前视。next-bar-close 用 得 较 少;日内 VWAP 风格 策略 用 自定 日内 K 线。
volume_participation_cap 是 订单 可以 消耗 的 K 线 成交量 最大 比例。常规 安全 上限 是 0.10。超过 0.20 意味着 策略 会 推 动 市场 而 简单 fill model 抓 不 到,必须 上 4.5.2 的 真实 市场 冲击 模型。
slippage_bps 是 加 在 成交价 上 的 基点 或 与 成交量 成 比例 的 滑点。常规 占位:流动 名字 如 510300 / 上证50ETF / 招商 银行 等 5-10 bps;流动 较 差 的 中 / 小 盘 名字 20-50 bps。滑点 公式:
sign = +1 买入、-1 卖出。
真实 性 fill simulator 类 的 参考 实现:
class RealisticFillSimulator:
def __init__(self, fill_bar='next-bar-open', volume_participation_cap=0.10, slippage_bps=5.0):
self.fill_bar = fill_bar
self.volume_participation_cap = volume_participation_cap
self.slippage_bps = slippage_bps
def fill(self, orders, bar):
fills = []
for order in orders:
sign = 1 if order.qty > 0 else -1
max_qty = self.volume_participation_cap * bar.volume
filled_qty = sign * min(abs(order.qty), max_qty)
unfilled = order.qty - filled_qty
fill_price = bar.open * (1 + sign * self.slippage_bps / 10000)
fills.append(Fill(symbol=order.symbol, qty=filled_qty, price=fill_price))
if abs(unfilled) > 0:
self.log_remainder(order.symbol, unfilled)
return fills
前视 诊断 测试 作为 回归 检查 与 fill simulator 同级 部署:
# run after every backtest; passes only if every feature is computed before its label
def assert_no_lookahead(backtest_log):
for entry in backtest_log:
assert entry.feature_timestamp <= entry.label_timestamp, \
f'look-ahead at {entry.timestamp}: feature from {entry.feature_timestamp} predicts label at {entry.label_timestamp}'
成本 占位 规则 与 借券 约束 层
在 4.5.2 教 真实 成本 模型 之前,统一 用 一个 平均 ~10 bps 的 双边 成本(每边 5 bps)占位 在 每一次 成交 上。这个 占位 对 流动 大 盘 故意 偏 悲观(A 股 510300 真实 双边 成本 约 ~5-10 bps 含 印花税 + 过户费 + 价差 + 冲击)、对 流动 较 差 的 中 / 小 盘 故意 偏 乐观(真实 双边 可达 30-50 bps)。占位 的 目的 不 是 给 准 数,而 是 让 回测 报告 不再 出 一个 没人 能 复制 的 零 成本 夏普;真实 成本 模型 是 4.5.2。
多空 策略 加 一张 借券 表:borrow_pnl_daily = -short_value * borrow_cost_bps / 252 / 10000。CFFEX 沪深300 IF / 中证500 IC / 中证1000 IM / 上证50 IH 股指 期货 提供 A 股 市场 中性 主要 对冲 渠道;只 做 多 借券 层 no-op。
十 项 真实性 清单
本课 的 工程 交付物。在 回测 报告 为 「可信」之 前 必 跑 一遍:
1. PIT data discipline — available_at <= t
2. survivorship-bias-free universe — universe(date, symbol) from 4.1.1 L4
3. next-bar-open fills with volume-participation cap <= 10%
4. slippage applied (5-10 bps placeholder)
5. transaction costs applied (10 bps round-trip placeholder)
6. borrow constraints applied — HTB flag + borrow-cost curve
7. corporate actions handled — dividends / splits / mergers
8. regional microstructure constraints honored — T+1 在 A 股;10% 涨跌停板 在 主板;LULD 带 在 美 股;Reg SHO 在 美 股
9. tradeable-window honored — 集合 竞价 / 盘后 / 假期 默认 不 交易
10. capacity reality-check — order size consistent with strategy capacity
任 一 项 未 满足 的 回测 都 是 草稿、不 是 证据。草稿 与 证据 之间 的 差距 就 是 真实性 税;真实性 税 是 真 金 白 银。
第 1-6 项 是 五大 偏差 修复 翻 成 工程 任务。第 7 项(corporate actions)是 数据 层 的 工作——分红 复权 因子 / 拆股 复权 因子 / 并购 / 分立 / 要约 收购 / 现金 选择 权——4.1.1 涵盖,本课 假设 数据 层 已 处理。第 8 项(地区 微观 结构 约束)在 A 股 是 这些 具体 项:A 股 现货 T+1 结算(当日 买入 不能 当日 卖出,但 510300 ETF 套利 份额 是 T+0);主板 10% 涨 跌 停 板 / 创业板 + 科创板 20% / ST 戴帽 5%;最小 申报 100 股;印花税 0.1% 卖方(2023 年 减半 至 0.05%);过户费 万分之 0.5 双边;集合 竞价 窗口 09:15-09:25 早 与 14:57-15:00 收 盘。fill simulator 必 强制 这些 约束:试图 在 涨停板 / 跌停板 成交 的 订单 被 拒(或 在 涨停板 买 时 按 涨停板 成交量 部分 成交——典型 涨停板 买 端 成交 概率 接近 零)。第 9 项 是 日历 层:默认 不 交易 盘前 / 盘后,除非 策略 显式 opt-in;交易所 休市 日 不 交易。第 10 项 是 容量 检查:如果 平均 日 单 量 超 过 策略 典型 名字 中位 日 成交量 的 10%,策略 已 受 容量 约束,回测 对 规模化 偏 乐观。
真实性 税 曲线:从 0.40 落到 0.18-0.22
把 L1 的 5 日 动量 回测 拿 来——事件驱动 引擎、无 前视、基线 夏普 ~0.40 在 510300 沪深300 ETF 上 2014-2023。逐 项 应用 清单 看 夏普 怎么 跌:
PIT discipline ~0.40 -> ~0.40 (negligible on price-only signal)
survivorship universe ~0.40 -> ~0.40 (negligible on ETF; ~0.30 on single-name)
fill model ~0.40 -> ~0.36
slippage ~0.36 -> ~0.31
costs ~0.31 -> ~0.23
microstructure ~0.23 -> ~0.18-0.22
规律 稳定:每 一项 真实性 减 掉 剩余 夏普 的 大约 5-15%。一个 真实 性 回测 报告 的 头条 数字 通常 是 朴素 回测 头条 数字 的 30-60%。L1 向量化 bug 给 出 的 朴素 1.8 压缩 到 真实 性 0.18-0.22——一个 数量 级 的 通胀,与 过拟合 无关,纯 工程 纪律 问题。
Formula Explorer
\text{realism tax} = \text{Sharpe}_\text{naive} - \text{Sharpe}_\text{credible}真实性 税 是 朴素 回测 给 出 的 数字 减 去 信誉级 回测 给 出 的 数字。下面 的 练习 让 你 在 自己 的 策略 上 算 一遍。
Exercise
Exercise
你 正在 把 L1 的 5 日 动量 信号 跑 在 510300 沪深300 ETF 上,窗口 2014-01-01 到 2023-12-31,事件驱动 引擎。起点 夏普 ~0.40(L1 的 诚实 基线)。逐 项 应用 十 项 真实性 清单,做 四 个 计算。
(i) 对 五 大 偏差(look-ahead bias、survivorship bias、unrealistic-fill bias、no-cost bias、borrow-and-short-availability bias)每 一 个 给 出 一 条 针对 你 工作 例 数据集 的 具体 工程 或 流程 修复(例:对 look-ahead bias:assert all(signal_timestamps <= bar.open_timestamp) 在 每一步);答 五 行。
(ii) 用 常规 安全 默认 配置 fill model(fill_bar='next-bar-open'、volume_participation_cap=0.10、slippage_bps=5)并 加 上 10 bps 双边 transaction-cost 占位;报告 配置 完 之 后 的 夏普(应在 ~0.23-0.31 区间)。
(iii) 对 你 的 地区,列 出 第 8 项 里 三 条 必 须 遵守 的 地 区 特定 微观 结构 约束 —— A 股 T+1 结算 / 10% 涨跌停板 / 印花税 0.1% 卖方 —— 并 对 每 一 条 说明 只 做 多 5 日 动量 信号 是 否 受 影响(是 / 否)以及 一 句 原因。
(iv) 报告 应用 完 十 项 清单 之 后 的 最终 可信 夏普;与 L1 bug 版 向量化 回测 的 ~1.8 比较 并 给 出 真实性 税 的 绝对 夏普 和 占 原 头条 的 百分比。
把 四 个 答案 报告 成 一张 表。
提示
提示
通向 L3 的 桥
你 刚 拿 出 的 诚实 基线 ~0.18-0.22 是 动量 策略 的 一 个 参数 配置。朴素 答案 是 把 全部 跑 一遍 报 最 好 的。这 就 是 参数 网格 过 拟合——即使 引擎 完全 真实 也 能 再 通胀 报告 夏普比率 2-5x。L3 教 三 种 统计 验证 方法 量化 参数 扫描 偏差 并 产 出 可信 估计。本课 反复 用 到 的 概念 包括 夏普比率、最大回撤、Alpha 衰减、交易成本、市场冲击 等 在 L3-L4 都 会 继续 出 现。
Components covered
- Inline-code listing of the FIVE canonical backtest biases (
look-ahead bias,survivorship bias,unrealistic-fill bias,no-cost bias,borrow-and-short-availability bias) with inflation ranges and fixes. - Inline-code listing of the THREE core fill-model parameters (
fill_bar,volume_participation_cap,slippage_bps) with safe defaults. - Inline-code listing of the TEN-item realism checklist in exact order.
- Fenced ```text block — PIT rule
engine.get(symbol, field, as_of=t) -> row with max{r.available_at : r.available_at <= t}. - Fenced ```python code block — the
RealisticFillSimulatorclass with slippage formula. - Fenced ```python code block — the
assert_no_lookaheaddiagnostic. - Inline-code listing of the realism-tax curve from ~0.40 to ~0.18-0.22.
- Exercise — four sub-task computations (i)/(ii)/(iii)/(iv) on
510300沪深300 ETF. - Two progressive Hints kept short.
- FormulaExplorer — the realism-tax difference.