回测实操:backtrader 跑通你的第一个回测
21 年成绩单:359% 的收益,和它背后的三盆冷水
规则书锁定了(q1.7),今天让机器替我们把 21 年的历史完整走一遍:每一次金叉买入、每一次死叉卖出、每一笔佣金,一秒钟跑完。
先把成绩单最亮眼的数字给你:10 万本金,21 年后变成 45.9 万,总收益 359%。
心动了?好——这正是最危险的时刻。这张成绩单上还有三个数字:45.1% 的最大回撤、35% 的胜率、以及买入持有同期的 358%。读懂这四个数字的关系,比跑通回测本身重要十倍。
回测引擎像一台驾驶模拟器:把你的「驾驶规则」放进历史路况里跑一遍,撞了多少次、油耗多少,全有记录。
模拟器的价值是便宜地暴露问题——但没人会因为模拟器里跑得好就直接上高速。记住 q0.5:模拟器用的是「已经发生过的路况」,而真实的路,永远在生成模拟器里没有的弯道。
一、把规则书翻译给 backtrader
规则书六要素,逐条变代码。新建 backtest.py:
import pandas as pd
import backtrader as bt
df = pd.read_csv("csi300.csv", parse_dates=["date"]).set_index("date").sort_index()
df = df[df.index >= "2005-04-08"]
class SmaCross(bt.Strategy):
params = dict(fast=5, slow=20) # 规则书:5/20均线
def __init__(self):
sma_f = bt.ind.SMA(period=self.p.fast)
sma_s = bt.ind.SMA(period=self.p.slow)
self.crossover = bt.ind.CrossOver(sma_f, sma_s) # 1=金叉,-1=死叉
def next(self): # 每根K线走完调用一次→天然「信号次日执行」
if not self.position and self.crossover > 0:
self.order_target_percent(target=0.95) # 规则书:95%仓位买入
elif self.position and self.crossover < 0:
self.close() # 规则书:死叉清仓
cerebro = bt.Cerebro()
cerebro.adddata(bt.feeds.PandasData(dataname=df))
cerebro.addstrategy(SmaCross)
cerebro.broker.setcash(100000) # 10万本金
cerebro.broker.setcommission(commission=0.0005) # 规则书:单边0.05%
cerebro.addanalyzer(bt.analyzers.DrawDown, _name="dd")
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name="ta")
result = cerebro.run()[0]
print("期末资金:", round(cerebro.broker.getvalue()))
print("最大回撤:", round(result.analyzers.dd.get_analysis()["max"]["drawdown"], 1), "%")
ta = result.analyzers.ta.get_analysis()
print("交易次数:", ta.total.closed, "| 盈利笔数:", ta.won.total, "| 亏损笔数:", ta.lost.total)
print("胜率:", round(ta.won.total / ta.total.closed * 100, 1), "%")
# 对照组:买入持有(一定要算,没有对照的收益率没有意义)
close = df["close"]
bh_return = close.iloc[-1] / close.iloc[0] - 1
bh_dd = (close / close.cummax() - 1).min()
print("--- 对照:买入持有 ---")
print("总收益:", round(bh_return * 100, 1), "% | 最大回撤:", round(bh_dd * 100, 1), "%")
next()在每根 K 线走完后被调用,下单在下一根成交——backtrader 天然帮你守住了「信号次日执行」,这是它比手写循环省心的地方之一。
二、成绩单,和它的四堂课
2026-07 实测结果(2005-04-08 → 2026-07-29):
| 指标 | 双均线策略 | 买入持有(对照) |
|---|---|---|
| 期末资金(10万起) | 45.9 万(+359.1%) | 45.8 万(+358.4%) |
| 最大回撤 | -45.1% | -72.3% |
| 交易次数 | 156 次 | 1 次 |
| 胜率 | 35.3%(55胜/101负) | — |
四个数字,四堂课:
- +359% vs +358%:忙活 21 年,没多赚一分钱。156 次买卖、上百笔佣金,最后和「买了就睡觉」打平——这就是 q1.5 那个平庸信号的真实成色。任何人给你看收益率,先问对照组:跑赢「什么都不做」了吗?
- -45% vs -72%:它真正的价值藏在这里。买入持有在 2008 年要眼睁睁看着资产腰斩再腰斩;策略把最深的坑从 72% 减到 45%。趋势类策略的实际作用更多是「少挨跌」,不是「多赚钱」——市场大跌时它在场外,代价是震荡市里反复被小刀割(101 笔亏损就是这么来的)。
- 35% 胜率却没亏钱:胜率是最会骗人的单一指标。它亏 101 次、赢 55 次,但亏时割得快、赢时拿得住(趋势策略的典型形状)。反过来说:一个「胜率 90%」的策略也可能一次大亏全吐光。胜率必须和盈亏比一起看——下一批的绩效课细讲。
- 45% 的回撤,是规则书写的。q1.7 我们白纸黑字写了「无止损」——这就是代价。不是意外,是知情的选择。
成绩单已经不算好看,但真相是:它仍然偏乐观。回测里藏着三个理想化假设——
① 零滑点:假设每次都能按收盘价成交,真实市场你会买贵卖贱一点;② 随时有量:假设想买就买得到;③ 指数可直接交易:实际要通过相关基金等工具,还有管理费和跟踪误差。
156 次交易,每次「优化」一点点,累积起来足以把「打平买入持有」变成「明确跑输」。记住:回测成绩是策略的「化妆照」,实盘素颜只会更糟——化妆照都不好看的,素颜就别想了。
看到成绩平庸,你的第一冲动一定是:「把 5/20 改成别的试试?说不定 10/60 更好!」
停。这个冲动无比自然,也无比危险——它就是 q0.5 讲的「试到好看为止」。剧透下一节:我们会故意放开手脚调参,在前 14 年数据上找出「最优参数」(年化 18.72%,漂亮吧?),然后拿它去后 7 年验证——结果是年化 -3.24%,由正转负。数字已经跑好了,下一节亲手复现这场车祸。
📌 免责:本课为技术科普,回测基于历史数据与理想化假设,回测表现不代表实盘,更不代表未来;本策略为教学示例、统计上无超额收益;指数不可直接交易;不构成任何投资建议;投资有风险,盈亏自负。
这一节你跑通了人生第一个完整回测,更重要的是学会了读成绩单:收益要有对照组(vs 买入持有)、风险看回撤(45% 是规则书自己写的)、胜率必须搭配盈亏比、以及——回测永远是化妆照。
但今天的回测,我们是「守规矩」跑的。如果不守规矩呢?
下一节是整个 Q1 的灵魂课:我们故意犯规两次——先「偷看未来」(一行代码之差,收益从 724% 膨胀到 5248%),再「疯狂调参」(训练段年化 18.72% 的最优参数,测试段 -3.24% 当场翻车)。q0.5 讲过的坑,这次让它们在你自己的代码里现出原形。看完这节,你对任何回测数字的免疫力,将超过 99% 的散户。
🔎 来源与核验· 3 条,点开核对
