第 6 · 28 分钟
绩效指标与调试
Sharpe、回撤、CAGR — 以及实验室常见错误的修复方法。
没有指标的回测只是一张图。Quant Buffet 报告风险调整后的统计量,让你能诚实地比较策略;代码出错时,还会用AI 调试器给出行号。
Drag the sliders to see how Sharpe and Calmar react. These are the same families of stats Quant Buffet reports after a lab run.
Weak — check overfitting or costs
| Lab error | Likely fix |
|---|---|
| Signal never ready | Shorten lookback or extend start date; check ASSETS history. |
| Import blocked | Only backtest.*, numpy, pandas — see API docs. |
| Symbol not in whitelist | Use ETFs from backtest.universes (SPY, TLT, GLD…). |
| Equity curve too short | Ensure on_day actually calls set_target_weights. |
指标词汇
| 指标 | 含义 | 审慎看待 |
|---|---|---|
| CAGR | 路径重演时的年均复合增长 | 单个优异十年可能夸大它 |
| Volatility | 净值波动程度 | 低波可能掩盖杠杆或流动性差 |
| Sharpe | 单位风险的收益 | 只有两年数据时毫无意义 |
| Sortino | 单位*下行*风险的收益 | 对有上行尖峰的策略更友好 |
| Max drawdown | 最大峰谷亏损 | 你必须在情绪上能扛住的数字 |
| Alpha / Beta | 相对 SPY 的超额收益;市场敏感度 | 代理基准不等于你的真实组合 |
| Win rate | 上涨天数占比 | 70% 胜率依然可能亏钱 |
先建立参照,才知道什么叫「好」
| 基准 | 大致长期 CAGR | 大致波动率 | 大致 Sharpe | 最差回撤 |
|---|---|---|---|---|
| SPY 买入持有 | 约 10% | 约 15–19% | 约 0.4–0.5 | 约 -55% |
| 经典 60/40 | 约 8% | 约 10% | 约 0.5–0.6 | 约 -30% |
BIL(现金) | 政策利率 | 约 0.3% | 不适用 | 约 -0.1% |
先对照这些数字再决定要不要兴奋。仅做多的日频 ETF 策略若 Sharpe 达到 0.8,已经相当不错;1.5 属于极优且需要仔细审查;而简单日频回测中 Sharpe 超过 2,几乎总意味着前视偏差、不现实的成本假设,或一个「挑过存活者」的资产池。一个参照:麦道夫伪造的收益隐含 Sharpe 约 2.5,而这种不合理正是最早的公开疑点之一。
为何回撤比 CAGR 更重要
回撤是回测与人性交汇的地方。两个事实足以说明问题:亏损 50% 需要上涨 100% 才能回本,而不是 50%;而回本时间可能长达一段投资生涯——纳斯达克综合指数在 2000 年 3 月见顶,直到 2015 年才稳定收复该点位,也就是大约十五年被埋在水下。与之对照,现代史上最快的崩盘(2020 年 2 月至 3 月)只用 33 天就跌了约 34%。
| 回撤 | 回本所需涨幅 | 现实反应 |
|---|---|---|
| -10% | +11% | 不舒服 |
| -20% | +25% | 开始质疑模型 |
| -35% | +54% | 多数人在此放弃策略 |
| -50% | +100% | 职业或资本风险 |
多重检验:反复尝试的统计学
如果你只测一个策略,Sharpe 1.0 是证据;如果你测了一千个并报告最好的那个,Sharpe 1.0 大致就是纯随机所能给出的结果——众多噪声样本的最大值天然偏大。正因如此,学界提出了去偏 Sharpe 比率(Bailey 与 López de Prado),按「产生该结果所用的尝试次数」对其进行惩罚。
- 统计你的尝试次数。 换十个回看窗口重跑十次,就是十次检验,即使你只截图了一张。
- 偏好参数平台区。 若 150、200、250 天都有效,效应大概是真的;若只有 187 天有效,那是噪声。
- 预留一段样本。 用 2000–2015 年建立直觉,再对 2016 年至今只看一次。
- 要求给出理由。 库中每个策略都附经济逻辑,正因为「数字好看」不是理由。
调试流程
- 阅读 IDE 行号栏高亮处的错误信息与行号。
- 对照契约检查文件:模块级
ASSETS,以及返回(on_day, ready)的make_on_day(prices)。 - 点击 Ask AI for syntax — 获得「在哪里、为什么」以及可直接粘贴的代码片段。
- 查阅 API 文档,确认导入项、白名单标的与模板示例。
- 用更短的 `ASSETS` 和更晚的
start重跑,以隔离数据问题。
你真正会遇到的五种错误
| 错误 | 真实原因 | 修复 |
|---|---|---|
NameError: ASSETS is not defined | ASSETS 定义在函数内部 | 移到文件顶部的模块级 |
Symbol not in whitelist | 该标的在实验室中无法模拟 | 换成白名单代理,例如用 EEM 替代单只新兴市场股票 |
TypeError: cannot unpack non-sequence | make_on_day 只返回了 on_day | 返回元组 (on_day, ready) |
KeyError: <date> | 索引的序列在 dt 这一行不存在 | 重建索引到 prices.index,或用 .reindex(close.index).ffill() |
Timeout: exceeded 90s | 在长历史上逐日循环计算 | 把 rolling/shift 向量化,放到 on_day 之外 |
# 慢:每一天都重算一遍 200 日均值(会超时)
def on_day(engine, dt):
window = prices.loc[:dt, "SPY"].tail(200)
if prices.loc[dt, "SPY"] > window.mean():
engine.set_target_weights(dt, {"SPY": 1.0})
# 快:算一次,然后查表
sma = prices["SPY"].rolling(200, min_periods=200).mean()
def on_day(engine, dt):
if prices.loc[dt, "SPY"] > sma.loc[dt]:
engine.set_target_weights(dt, {"SPY": 1.0})# 运行成功后平台报告的内容
metrics = compute_metrics(result.equity, benchmark=spy_bh, trades_count=len(result.trades))
print(metrics["sharpe"], metrics["max_drawdown"], metrics["cagr"])进入第 7 课前 — 您应能够
- 大致说出 SPY 与 60/40 的 Sharpe 和回撤水平。
- 解释为何日频 ETF 回测中 Sharpe 超过 2 是危险信号。
- 把回撤换算成回本所需涨幅。
- 无需帮助即可诊断五类常见实验室错误。
- 说明多重检验如何抬高你找到的最优结果。