终于要写一个会“自己买卖”的策略了。但更重要的是:这一关你会亲眼看到量化最大的坑——未来函数——是怎么把回测做得假漂亮的。
快线(20 日均线)上穿慢线(60 日均线)= 金叉 → 买入持有;下穿 = 死叉 → 空仓。直觉是“短期势头转强就上车,转弱就下车”。
ma_fast = px.rolling(20).mean()
ma_slow = px.rolling(60).mean()
signal = (ma_fast > ma_slow).astype(int) # 想持有=1, 空仓=0
你今天收盘才算得出信号,所以最早明天(T+1)才能成交。代码上就是把信号往后挪一天(shift(1))。这一步看着不起眼,却是防未来函数的命门:
pos = signal.shift(1).fillna(0) # T+1 才持有(防未来函数!)
trade = pos.diff().abs() # 仓位变化=发生买卖
ret_strat = pos*ret - trade*0.001 # 持仓吃收益, 调仓扣单边千1成本
我们故意做两个“偷看未来”的版本对比:
| 策略 | 年化 | 夏普 | 回撤 |
|---|---|---|---|
| 买入持有 | 26.8% | 0.94 | -47% |
| 双均线·正确(T+1) | 14.1% | 0.73 | -37% |
| 双均线·不延迟(轻微偷看) | 15.4% | 0.78 | -36% |
| 偷看明天(作弊) | 530% | 9.59 | 0% |
“偷看明天”= 只在会涨的日子持有(ret.where(ret>0,0)),用了当天才知道的真实涨跌——年化 530%、夏普 9.59、零回撤。现实绝不可能。
如果你的回测夏普 > 3、几乎不回撤、曲线笔直向上——99% 是 bug(代码里混进了未来信息),不是你发现了圣杯。第一反应永远是“我哪里偷看了未来”,而不是“我要发财了”。
“双均线·正确版”(14.1%)反而跑不赢躺着不动的买入持有(26.8%)。在茅台这种大牛票上,择来择去会错过大涨、还交手续费。说明择时是有代价的,简单技术指标不是印钞机。但注意它把回撤从 -47% 降到 -37%——牺牲收益换了平稳。这又回到第 02 关:收益和风险要一起看。
1. 改参数(5/20、10/30…),看结果随之大变——这其实埋了“过拟合”的雷(下一关讲)。
2. 故意把 signal.shift(1) 的 shift 去掉(制造未来函数),看夏普怎么虚高,再改回来。亲手造一遍、修一遍,记得最牢。
信号只能用截至T日的信息、成交最早T+1;漂亮到离谱的回测基本是未来函数bug;简单择时常跑不赢躺平,但能降回撤。
