第 2 · 25 分钟
市场数据基础
价格、K 线、调整后收盘价,以及策略每天消费的数据面板。
每个策略都从市场数据开始。Quant Buffet 实验室通过 load_daily_prices() 加载流动 ETF 的日频调整后收盘价,并缓存到磁盘,使重跑既快速又可复现。
| Layer | What you get | In Quant Buffet |
|---|---|---|
| Exchange / vendor | Tick-by-tick trades, quotes | Not used in lab |
| Yahoo Finance (yfinance) | Daily OHLCV | Downloaded via load_daily_prices |
| Quant Buffet cache | CSV per symbol + date range | backtest/data_cache/*.csv |
| Your strategy | pd.DataFrame panel | Columns = ASSETS, index = dates |
| Indicators | Rolling SMA, returns, z-scores | Computed inside make_on_day |
核心词汇
| 术语 | 含义 | 实验室示例 |
|---|---|---|
| 代码 symbol | 标的标识 | SPY、TLT、BTC-USD |
| OHLCV | 开高低收量 | 每个交易日一行 |
| 调整后收盘 | 经拆股与分红修正的收盘价 | load_daily_prices 默认值 |
| 面板 panel | 日期 × 标的表格 | make_on_day 中的 prices |
| 回看 lookback | 指标所需历史长度 | 200 日 SMA 需 200 行 |
ready | 信号开始可信的首个日期 | 由 make_on_day 返回 |
面板长什么样
面板就是一张表格:日期在行、标的在列。示意如下:
| 日期 | SPY | TLT | GLD | XLC |
|---|---|---|---|---|
| 2015-10-05 | 185.42 | 112.08 | 108.31 | NaN |
| 2015-10-06 | 184.90 | 112.55 | 109.02 | NaN |
| 2018-06-19 | 268.41 | 108.77 | 116.94 | 48.30 |
| 2018-06-20 | 269.03 | 108.32 | 117.10 | 48.55 |
注意那些 NaN。XLC 在 2018 年 6 月之前并不存在,任何数据清洗都无法为它在 2015 年凭空造出价格。这不是数据缺陷,而是所有想在长历史上交易通信服务板块的策略都必须面对的真实约束。
在代码中读取价格面板
# 在 make_on_day 内部 —— prices 已按 ASSETS 加载完成
cols = [c for c in ASSETS if c in prices.columns]
close = prices[cols]
daily_return = close.pct_change() # 简单收益率
sma_200 = close.rolling(200, min_periods=200).mean() # 趋势过滤
mom_12m = close.pct_change(252) # 12 个月动量
vol_20d = daily_return.rolling(20).std() * (252 ** 0.5) # 年化波动率
# 在所有标的数据齐备前不要开始交易
ready = close.dropna().index[0] if not close.dropna().empty else None毁掉新手回测的三种偏差
1. 存活偏差
如果你用今天的赢家构建资产池,就已经悄悄删掉了所有失败者。安然、雷曼兄弟、华盛顿互惠、柯达、贝尔斯登都曾是体面的标普 500 成分股。只在「活到 2026 年」的公司上做回测,等于享受了现实投资者从未获得的顺风。SPY 这类宽基 ETF 能缓解该问题:指数本身会替换掉衰败成分,ETF 则继续存续。
2. 前视偏差
这是新手最常见、也几乎最难察觉的错误。任何在全样本上计算的统计量,都会把未来泄露进过去:
# 错误 —— 全样本均值包含了未来的价格
z = (close - close.mean()) / close.std()
# 正确 —— 只用截至当日可得的数据
roll_mean = close.rolling(60).mean()
roll_std = close.rolling(60).std()
z = (close - roll_mean) / roll_std错误版本常常产出 Sharpe 大于 3、净值曲线近乎直线的结果。如果你的第一次回测就这么漂亮,请先怀疑数据泄露,再考虑庆祝。
3. 公司行动与未复权价格
苹果在 2020 年 8 月 1 拆 4;英伟达在 2024 年 6 月 1 拆 10;特斯拉在 2022 年 8 月 1 拆 3。在未复权数据上,这三次分别看起来像隔夜暴跌 75%、90% 与 67%——用未复权价格跑动量策略,会在最糟的时点把它们全部卖出。分红的累计影响同样巨大:SPY 每年分配约 1.3%–2%,在 20 年回测中忽略它,等于把年化 8% 变成约 6.5%——1 万美元的终值大约是 4.66 万对 3.52 万。
真实示例:当「价格序列」不等于「资产本身」
白名单里有两只著名的「反面教材」商品 ETF,正因为它们表现异常,才值得在 IDE 中亲自加载一次。
| ETF | 发生了什么 | 对回测的启示 |
|---|---|---|
USO | 2020 年 4 月 WTI 近月合约结算价转负(约 -37 美元),USO 被迫重构持仓,后续还做了反向拆股。 | 基金的投资授权可能在历史中途改变;「油价」与「USO 价格」是两条不同的序列。 |
UNG | 长期升水(contango)使每次展期都「卖便宜、买贵」,无论现货天然气如何都在持续损耗净值。 | 此处的长期做空结论可能是结构性成本,而非可交易的 edge。 |
SVXY | 2018 年 2 月 5 日「波动率末日」,单日跌约 90%;姊妹产品 XIV 直接清算终止。 | 日频收盘回测看不到这种量级的盘中跳空风险。 |
你真的会遇到的数据起始日
| 标的 | 数据实际起始 | 后果 |
|---|---|---|
SPY | 1993 年 | 长周期回测的可靠锚点 |
XLRE | 2015 年 10 月 | 房地产板块回测很短 |
XLC | 2018 年 6 月 | 无法出现在 2008 年压力测试中 |
BTC-USD | 约 2014 年 9 月 | 任何加密策略都跨不过金融危机 |
ETH-USD | 约 2017 年 11 月 | 仅有约两轮完整周期 |
进入第 3 课前 — 您应能够
- 解释调整后收盘价,以及未复权价格为何会破坏动量信号。
- 在一行 pandas 代码中识别前视偏差。
- 说明 ETF 资产池为何能缓解但无法消除存活偏差。
- 预测在
ASSETS中加入新上市 ETF 后ready会如何变化。