第 5 章 · 02 事件驱动回测 backtrader 本节摘要:本节讲策略评估的「实盘校正器」——事件驱动回测框架 backtrader。上一节的向量化回测快但失真,backtrader 则按事件序列逐根 K 线模拟撮合:订单生成、broker 撮合、手续费、滑点、现金账户、持仓管理全部按实盘逻辑跑。本节讲清 backtrader 的四大核心组件(Cerebro 引擎、Data Feed 数据馈送、Strategy 策略类、Analyzer 分析器),并演示如何把第 7 章 ML 模型的预测信号喂给 backtrader 做多空组合回测,再用 PyFolio 分析器把结果导出给 pyfolio 做绩效分析。 内容来源:原项目 、 ,汉化并套用体系化模板。
本节摘要:本节讲策略评估的「实盘校正器」——事件驱动回测框架 backtrader。上一节的向量化回测快但失真,backtrader 则按事件序列逐根 K 线模拟撮合:订单生成、broker 撮合、手续费、滑点、现金账户、持仓管理全部按实盘逻辑跑。本节讲清 backtrader 的四大核心组件(Cerebro 引擎、Data Feed 数据馈送、Strategy 策略类、Analyzer 分析器),并演示如何把第 7 章 ML 模型的预测信号喂给 backtrader 做多空组合回测,再用 PyFolio 分析器把结果导出给 pyfolio 做绩效分析。
内容来源:原项目
ch08/03_backtesting_with_backtrader.ipynb、ch05/02_backtest_with_pf_optimization.ipynb,汉化并套用体系化模板。
⚠️ 风险提示:事件驱动回测比向量化更接近实盘,但仍假设历史会重演、流动性充足。回测盈利不代表实盘盈利,务必做 paper trading 验证。
阅读完本节,你应当能够:
bt.Strategy 的策略类,在 next() 里读预测下单。order_target_percent 做目标权重调仓。上一节的向量化回测有四个硬伤:没有撮合(假设信号即成交)、没有成本(手续费/滑点/税)、没有容量(假设任意金额都能成交)、没有账户(没有现金/持仓状态机)。这些在低频大资金策略里影响不大,但在高频、低流动性、有最小下单量的场景下会严重高估收益。
事件驱动回测(event-driven backtesting)按真实交易的事件序列推进:每一根 K 线到来是一个事件,策略在事件回调里读数据、生成订单,broker 模拟撮合(限价/市价、部分成交、拒单),账户更新现金和持仓,还要扣手续费和滑点。这样得到的曲线才接近实盘。
💡 核心心法:backtrader 的设计哲学是「像真实交易那样推进」。Cerebro 是事件循环,Strategy 是你的大脑,Broker 是交易所,Analyzer 是会计。你只写「大脑」部分,其余交给引擎。
| 维度 | 向量化回测 | backtrader 事件驱动 |
|---|---|---|
| 速度 | 极快(毫秒) | 慢(秒到分钟) |
| 撮合 | 无 | 限价/市价/部分成交/拒单 |
| 成本 | 难建模 | CommInfo 自定义手续费/滑点 |
| 多资产 | 矩阵天然支持 | 每个资产一个 Data Feed |
| 学习曲线 | 低 | 中(要懂 cerebro/lines/params) |
| 用途 | 信号初筛 | 实盘前最终验证 |
经验法则:先用向量化筛掉 90% 噪音信号,再用 backtrader 验证剩下的 10%。
Cerebro 是 backtrader 的「主循环」,负责按时间推进、调度数据馈送、调用策略、协调 broker。
import backtrader as bt cerebro = bt.Cerebro() cerebro.broker.setcash(10000) # 初始资金 1 万美元
backtrader 用 PandasData 把 pandas DataFrame 喂进引擎。默认认 open/high/low/close/volume,但 ML 策略需要额外的 predicted 列,要自定义:
OHLCV = ['open', 'high', 'low', 'close', 'volume'] class SignalData(bt.feeds.PandasData): cols = OHLCV + ['predicted'] lines = tuple(cols) # 声明额外 line params = {c: -1 for c in cols} # -1 表示「自动从 dataname 取该列」 params.update({'datetime': None}) params = tuple(params.items())
lines 是 backtrader 的核心概念——每个 line 是一条按时间索引的序列,策略在 next() 里用 data.predicted[0] 取当前值、[1] 取昨天。
策略类继承 bt.Strategy,核心是 next() 方法,每根 K 线调用一次:
class MLStrategy(bt.Strategy): params = (('n_positions', 25), # 多空各持仓数 ('min_positions', 20), # 少于这个数就不开仓 ('log_file', 'bt_log.csv')) def next(self): today = self.datas[0].datetime.date() up, down = {}, {} for data in self.datas: if data.datetime.date() == today: if data.predicted[0] > 0: up[data._name] = data.predicted[0] elif data.predicted[0] < 0: down[data._name] = data.predicted[0] # 预测值排序,取 top/bottom n_positions longs = sorted(up, key=up.get, reverse=True)[:self.p.n_positions] shorts = sorted(down, key=down.get)[:self.p.n_positions] # 调仓(见下节)
Analyzer 在回测过程中收集统计,最常用的是 PyFolio,它把回测产物直接转成 pyfolio 需要的格式:
cerebro.addanalyzer(bt.analyzers.PyFolio, _name='pyfolio') results = cerebro.run() pyfolio_analyzer = results[0].analyzers.getbyname('pyfolio') returns, positions, transactions, gross_lev = pyfolio_analyzer.get_pf_items()
策略调仓用 order_target_percent:告诉 broker「把这个资产调到目标百分比」,引擎自动算差额下单。这比手算「要买多少股」简洁得多。
def next(self): # ... 排出 longs/shorts 列表 long_target = 1 / max(self.p.n_positions, len(longs)) short_target = -1 / max(self.p.n_positions, len(shorts)) for ticker in longs: self.order_target_percent(data=ticker, target=long_target) for ticker in shorts: self.order_target_percent(data=ticker, target=short_target) # 不在信号里的持仓清零 for ticker in current_positions: if ticker not in longs + shorts: self.order_target_percent(data=ticker, target=0)
注意 long_target 是正数(做多占用资金)、short_target 是负数(做空释放资金),多空市值抵消,组合总杠杆可控。
backtrader 默认零手续费,实盘必须建模成本。自定义一个固定手续费方案:
class FixedCommisionScheme(bt.CommInfoBase): params = (('commission', 0.02), # 每股 0.02 美元 ('stocklike', True), ('commtype', bt.CommInfoBase.COMM_FIXED)) def _getcommission(self, size, price, pseudoexec): return abs(size) * self.p.commission cerebro.broker.addcommissioninfo(FixedCommisionScheme())
next())、Analyzer(统计收集)。lines 是时间序列,[0] 取当前、[1] 取昨天;params 是策略参数,用 self.p.xxx 引用。order_target_percent(data, target) 让引擎自动算差额下单,正数做多、负数做空。下一节,我们用 pyfolio 把 backtrader/向量化的回测结果做成标准化的 tear sheet,系统化输出夏普、最大回撤、回撤期等核心绩效指标。