backtrader量化回测实战:从数据清洗到双均线策略的完整指南

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:《backtrader量化基础与实战教程.pdf》是一份面向量化初学者的实战型教程,由jgku整理,内容涵盖backtrader基本概念、核心源码分析和常用策略编写,能够帮助读者快速掌握这一量化框架,并理解其与股票、期货、QMT等系统的对接方式。全包为1个PDF文件,大小17.85MB,目前已有392人学习。教程按系列文章编排,覆盖数据获取、指标自定义、策略编写、绩效分析等回测全流程,既包含双均线、网格交易、均值回归等经典策略,也涉及多因子选股、配对交易、强化学习等进阶主题;同时讲解回测与实盘的差异处理、佣金滑点设置、一字涨跌停等现实问题,并演示币安数字货币、天勤期货行情、盈透证券等接口的接入方法,兼顾股票、期货、数字货币多个市场。整体内容深度与广度兼备,适合希望从回测走向实盘、减少重复踩坑的量化交易爱好者。

1. 为什么我劝新手直接上手 backtrader,而不是自己造轮子

第一次接触量化交易的人,脑子里通常都有同一个念头:K线数据我有了,交易逻辑我也能写,那我直接用 Pandas 算信号、用 For 循环模拟买卖不就行了?说实话,三年前我也是这么干的。直到我把滑点、手续费、复权、持仓周期、多标的同时回测这些因素全部手工实现了一遍之后,才意识到自己正在重复造一个早就被别人造好并且造得比我好得多的轮子。

backtrader 这个框架,是我在对比了聚宽、Qlib、vn.py 之后最终留在本地环境里长期使用的工具。它的定位很明确:一个纯粹的本地回测引擎。你不需要把策略代码托管到某个网站上,不需要受限于平台的数据格式,甚至不需要联网——只要你手头有行情数据,不管是日线、分钟线还是 Tick 级数据,backtrader 都能吃进去,按你的规则跑出完整的交易模拟结果。这一点对我来说是刚需,因为我不愿意让策略源码和交易想法暴露在第三方平台上,也不想被平台的函数命名习惯绑架。

它的核心价值,我认为是四个层面:一是把“数据加载、策略逻辑、交易执行、绩效分析”这四个回测环节彻底解耦,你写策略的时候只需要关注信号和仓位,其他脏活累活框架替你干;二是内置了经纪人模拟器,会真实地扣手续费和滑点,不会让你回测出那种“理论上暴富、实盘就亏光”的成绩单;三是支持多品种、多策略组合运行,单股回测只是它的入门玩法;四是有庞大的社区积累,你遇到的 90% 的坑,Stack Overflow 和 GitHub Issue 里都有人踩过并给出了答案。

这篇内容我不打算念官方文档,而是把我从零开始用 backtrader 做完第一版双均线策略到后面的多股轮动策略的完整路径,拆开揉碎了讲清楚。包括框架里最容易被忽视的底层机制、策略代码的标准写法、数据源怎么处理才不出幺蛾子、多股回测的坑,以及判断策略是否真的能用的绩效指标阅读方法。无论你是想验证一个想法,还是准备踏入量化交易这个领域,照着这个路径走,能帮你省下至少两周的试错时间。

2. 先理解 backtrader 的五个核心概念,否则代码就是抄了个壳

2.1 Cerebro:整个回测的“总导演”

在 backtrader 里,一切操作的入口都是一个叫 Cerebro 的对象。它负责把数据源、策略、经纪人设置、分析器全部组装到一起,然后按下启动按钮。我习惯把它类比成一个剧组的导演:数据源是演员,策略是剧本,经纪人模拟器是财务,而 Cerebro 负责喊“Action”和“Cut”。

import backtrader as bt

cerebro = bt.Cerebro()  # 创建导演
cerebro.broker.setcash(100000.0)  # 设定起始资金
cerebro.broker.setcommission(commission=0.001)  # 手续费万分之一

这两行设置了初始资金和手续费。 setcash 没什么好说的, setcommission 这里我多解释一句:0.001 是 0.1% 的意思,即每笔成交按成交金额的千分之一计费。不同券商的费率不一样,做回测时务必填自己实际使用的费率,否则回测收益会被系统性高估或低估。如果是做 ETF,通常还要额外设置一个最低佣金,比如 5 元起收,这些细节在 setcommission 里都有对应参数,往下阅读的时候我会专门提。

2.2 Data Feed:一切策略建立在数据之上

没有数据,策略就是空中楼阁。backtrader 设计了一套统一的 Data Feed 数据接口,既可以加载本地 CSV 文件,也可以直接从 Pandas DataFrame 喂进去,还能通过扩展支持从交易所或数据服务商拉取实时行情。

import pandas as pd

df = pd.read_csv('600519.csv', parse_dates=['date'], index_col='date')
data = bt.feeds.PandasData(dataname=df)
cerebro.adddata(data)

这里有一个新手特别容易踩的坑: PandasData 默认要求 DataFrame 的列名是 open high low close volume openinterest ,如果列名是中文或者是大写,必须手动指定字段映射,否则数据会全部读取失败,而且报错信息并不直观。我的习惯是在读入数据之前统一做一次列名标准化。

2.3 Strategy:你写的每一个策略都长成这个模板

策略是整个框架的灵魂。在 backtrader 中,你通过继承 bt.Strategy 并重写关键生命周期方法来实现信号生成和交易执行。一个最小可运行的策略如下:

class MaCrossStrategy(bt.Strategy):
    params = (('short', 5), ('long', 20),)

    def __init__(self):
        self.ma_short = bt.indicators.SimpleMovingAverage(
            self.data.close, period=self.params.short)
        self.ma_long = bt.indicators.SimpleMovingAverage(
            self.data.close, period=self.params.long)

    def next(self):
        if not self.position:
            if self.ma_short[0] > self.ma_long[0]:
                self.buy()
        elif self.ma_short[0] < self.ma_long[0]:
            self.close()

__init__ 阶段用于创建指标,框架会预先计算好整条指标序列; next 方法在每一个新 K 线周期被调用一次,你在这个方法里判断买卖逻辑并执行下单操作。 self.data.close 是指当前 K 线的收盘价, self.data.close[0] 是当前值, [-1] 是上一个周期的值。这个索引规则是 backtrader 的老传统,和 Python 里负数索引的语义一致,但和 Pandas 的切片操作不是一回事,新手上手时务必要区分清楚。

2.4 Broker:回测结果到底相不相信,全看它模拟得真不真实

Cerebro 自带一个模拟券商(Broker),你在策略里调用 self.buy() self.sell() 时,并不是立即成交的,而是由 Broker 根据当前 K 线的价格、手续费、滑点、成交量限制等因素撮合成交。这就是 backtrader 比那类“理想成交模型”靠谱得多的原因。

cerebro.broker.set_slippage_perc(perc=0.001)  # 设置滑点

滑点设置是很多入门教程一带而过但实盘至关重要的参数。默认情况下 Broker 按 K 线收盘价成交,不扣滑点,这在流动性好的股票上误差还不算大,但在小市值股票或加密货币上,回测和实盘的成交价偏差可能直接改写策略的盈亏结果。我常规的操作是给 Broker 设置一个 0.1%~0.2% 的滑点比例,模拟最坏情况下的冲击成本。

2.5 Analyzer:没有绩效分析的回测等于没做

跑完回测只是第一步,关键是你能不能看懂成绩单。backtrader 提供了 Analyzer 机制,可以在回测结束时输出一整套绩效指标——总收益率、年化收益率、夏普比率、最大回撤、胜率、交易次数等等,涵盖了大多数交易者关心的维度。

cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe')
cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')

回测结束后通过 result = cerebro.run() 拿到的返回值里取出对应分析器,再调用 get_analysis() 方法就能拿到结果了。后面第 5 节我会专门讲这些指标怎么读,这里先记住: 跑回测不等于做交易,看懂报告才能做决策,否则只是给自己一个心理安慰。

3. 数据源处理:花 30 分钟解决 80% 的回测致命误差

3.1 用 Pandas 清洗数据,别把脏数据喂给框架

很多人找数据时习惯“下载下来直接用”,然后把一堆停牌导致的空值、复权数据缺失、成交量为 0 的脏数据直接塞给 backtrader。框架不会帮你识别这些,它会默认数据是完整、连续、干净的——一旦遇到缺失时间戳,它只是跳过,但你的指标计算和策略信号就会错位。

我在清洗阶段必做三件事:

第一,统一时间索引。把日期列转成 pd.DatetimeIndex 并排序,避免时间乱序导致策略早早就用未来数据做决策。第二,处理缺失值。前复权数据偶尔会出现某天因停牌没有成交记录,我会用前一个交易日的价格填充缺失的 OHLC,量能填充为 0,这样指标计算不会断开,同时策略逻辑里可以加一个“量能为 0 不交易”的过滤条件。第三,检查复权。如果做的是股票,务必确认数据源给出的价格是前复权还是后复权,没有复权的原始价格回顾测长周期策略时会出现大量虚假的突破信号,因为除权除息产生的价格断层会被指标误判成趋势反转。

3.2 多股数据如何喂给 Cerebro

如果你想回测一个同时在贵州茅台和招商银行之间做资金轮动的策略,就需要给 Cerebro 同时添加两份 Data Feed。backtrader 的做法是为每个数据源自动分配一个编号: data0 data1 data2 ……在策略里, self.data 默认指向 data0 self.datas[0] self.datas[1] 则用于索引更具体的数据。

cerebro.adddata(data_maotai)
cerebro.adddata(data_zhaohang)

class RotateStrategy(bt.Strategy):
    def next(self):
        print(len(self.datas))  # 2
        print(self.datas[0].close[0])  # 第一只股票当前收盘价

需要注意:多股回测时, next 的调用频率由所有数据源中最小时间粒度决定。如果两个股票数据都是日线且交易日完全对齐,那每个交易日 next 都会被调用一次;如果一只股票停牌导致缺失日期, next 依然会在其他数据源有数据的日期被调用,这时你就要在代码里用 self.datas[i].close[0] 是否为有效值来判断该股票当天是否可交易。否则你会在停牌股票上买入,接着框架报出“数据不足”之类的神秘错误。

3.3 一个高效的多股数据加载模板

多股加载最容易出现的问题是内存占用过高和数据源长度不一致。我的标准做法是:把所有标的数据提前按统一时间范围裁剪好,存成 dict ,再循环生成 Data Feed:

import os
import glob

symbols = ['600519', '000001', '601318']
data_dict = {}

for sym in symbols:
    file_path = f'data/{sym}.csv'
    df = pd.read_csv(file_path, parse_dates=['date'], index_col='date')
    df = df.loc['2015-01-01':'2023-12-31']
    data_dict[sym] = bt.feeds.PandasData(dataname=df)

for sym in symbols:
    cerebro.adddata(data_dict[sym], name=sym)

name 参数给数据源起了个可读的别名,在策略里可以直接用 self.getdatabyname('600519') 拿到对应数据源。这套模板极大简化了后面写多因子选股、轮动策略时获取行情的代码量。

4. 手写一个完整的双均线策略并进行回测

4.1 策略逻辑确认

在写代码前,先把策略逻辑用一句话说清楚:当短期均线(5 日)上穿长期均线(20 日)时全仓买入;当短期均线下穿长期均线时清仓离场。这是一个趋势跟踪策略,逻辑简单、参数透明,最适合拿来说明框架的完整用法。

4.2 完整回测代码

把上面几节的内容串起来,一个可直接复跑的完整回测脚本如下:

import backtrader as bt
import pandas as pd

class MaCrossStrategy(bt.Strategy):
    params = (('short', 5), ('long', 20),)

    def __init__(self):
        self.ma_short = bt.indicators.SimpleMovingAverage(
            self.data.close, period=self.params.short)
        self.ma_long = bt.indicators.SimpleMovingAverage(
            self.data.close, period=self.params.long)
        self.crossover = bt.indicators.CrossOver(
            self.ma_short, self.ma_long)

    def next(self):
        if not self.position:
            if self.crossover > 0:
                self.buy()
        elif self.crossover < 0:
            self.close()

# 数据准备
df = pd.read_csv('data/600519.csv', parse_dates=['date'], index_col='date')
df = df.sort_index()

cerebro = bt.Cerebro()
cerebro.broker.setcash(100000.0)
cerebro.broker.setcommission(commission=0.001)

data = bt.feeds.PandasData(dataname=df)
cerebro.adddata(data)
cerebro.addstrategy(MaCrossStrategy, short=5, long=20)

# 添加分析器
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe')
cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')
cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')

result = cerebro.run()
strategy = result[0]
print('期末总资金: %.2f' % cerebro.broker.getvalue())

运行这个脚本,你会看到类似这样的输出(数值取决于你用的数据和时间段):期末总资金是 11 万还是 19 万,完全取决于你选择的股票和时间区间。不要因为回测赚钱了就开始激动,先冷静看另外几个指标。

4.3 参数优化不是让你挖坟掘墓

上面代码里 addstrategy(MaCrossStrategy, short=5, long=20) 是写死了参数。想找最优参数的话,backtrader 提供两种方式:一种是手动循环参数组合跑多次回测,另一种是用它自带的 Optimization 功能,在运行时自动遍历参数网格。

cerebro.optstrategy(MaCrossStrategy, short=range(3, 11), long=range(15, 35))

但参数优化有一个天坑:优化出来的参数是对历史数据的完美拟合,拿到未来行情上大概率失效,这就是过拟合。我的实践经验是,优化得到的参数组必须满足以下几个条件才有参考价值:在样本外测试中不显著衰减;参数曲面平滑,而不是孤峰——即最优参数附近的参数组表现也接近;策略逻辑有市场行为支撑,而不是纯靠运气拟合出的一段行情。你可以做优化,但别把优化的结果当成圣杯,否则实盘会教你做人。

5. 绩效指标阅读指南:回测成绩单不能只看收益率

5.1 我每次回测必看的四个分析器

日常复盘回测结果时,我只关注四个核心指标:收益率(Returns)、回撤(DrawDown)、夏普比率(SharpeRatio)、交易次数。如果你正在做高频或日内策略,还可以加上平均持仓周期和单位时间交易次数,但新手阶段这四个已经足够判断策略靠不靠谱。

returns = strategy.analyzers.returns.get_analysis()
drawdown = strategy.analyzers.drawdown.get_analysis()
sharpe = strategy.analyzers.sharpe.get_analysis()

print('年化收益率: %.2f%%' % (returns['rnorm100']))
print('最大回撤: %.2f%%' % (drawdown['max']['drawdown']))
print('夏普比率: %.2f' % (sharpe['sharperatio']))

rnorm100 是年化收益率(百分数), max.drawdown 是最大回撤百分比, sharperatio 是夏普比率。这几个字段名不太直观,官方文档也写得比较分散,我第一次用的时候翻了半天源码才找齐。现在直接复制这段代码就能省掉这个折腾。

5.2 怎么判断一个策略到底能不能用

先说收益率。回测年化 30%,看着很诱人对不对?但如果最大回撤高达 40%,意味着你在实盘中要承受 40% 的浮亏。大部分人会在回撤到 20% 的时候就割肉离场了,所以回测赚钱和你实际能赚到钱之间差了整整一个“拿得住”。

收益回撤比是我更看重的指标。年化 15%、最大回撤 8% 的策略,比年化 30%、最大回撤 40% 的策略更容易被坚持执行,坚持执行才是最稀缺的能力。夏普比率则衡量单位风险换来了多少超额收益,大于 1 说明策略风险收益比基本合格,大于 2 已经是相当优秀的水平,高于 3 的必须怀疑是否有未来函数或者过度拟合。

交易次数同样重要。如果一年只交易了五次,这个策略的统计样本太小,任何结论都站不住脚,你需要拉长回测区间或者放宽策略的交易频率。

5.3 常见的“绩效虚高”来源

回测结果比实盘好,这是必然的,但好多少是有差别的。导致绩效虚高的头号元凶是未来函数——你在信号形成的当根 K 线就用收盘价买入,实际上你根本不可能在收盘那一刻精确成交。解决方法是把下单价格设置成下一根 K 线的开盘价,backtrader 里可以通过 self.buy(exectype=bt.Order.Market) 配合 COC 参数实现更保守的成交假设。

第二个元凶是忽略了停牌不可交易的问题。我的处理方式是在策略的 next 里过滤掉成交量为 0 或者价格异常波动的标的,绝不为了贪图信号数量放过这些不合格的标的。

6. 常见问题与排查技巧:我踩过的 backtrader 深坑

6.1 数据加载后策略没反应, next 根本没被调用

这是所有入门者都会遇到的第一个问题。回测跑完了,日志一条没有,资金一分没变。排查顺序:先检查 Cerebro 是否真正 adddata 了数据源;再检查 PandasData 的列名映射是否正确;最后看数据的时间范围是否覆盖了回测起点。backtrader 默认要求数据源包含至少一个完整的指标周期,如果数据太短,比如只有 10 天,但指标周期是 20, next 就永远不会被调用。

6.2 下单成功却买在了意想不到的价格上

如果你在策略里直接 self.buy() 而没有指定成交类型,默认是按下一根 K 线的开盘价成交的。这个开盘价可能跟你的信号价格差很远,尤其在跳空行情里。如果想按当前 K 线价格立即成交,你需要显式设置:

self.buy(exectype=bt.Order.Close)

但这又回到了“未来函数”的问题上。我现在的习惯是:信号确认后,以市价单在下一根 K 线买入,以限价单在目标位卖出。这样虽然会让回测收益率打一点折扣,但换来的模拟真实度值得。

6.3 回测日志太多看不见关键信息,如何做日志分级

策略里 print 多了会刷屏,少了又看不到关键的下单时机。我的做法是只在 notify_order 回调函数里打印订单状态变化,比如提交、成交、取消,这样既不会错过关键信息,也不会被属性变化刷爆控制台。

def notify_order(self, order):
    if order.status in [order.Completed, order.Canceled, order.Rejected]:
        self.log(f'订单{order.ref}状态: {order.status}, 成交价: {order.executed.price}')

6.4 多股回测资金分配

多股回测最常见的需求是等权重买入多只股票。固定每只股票投入总资金的 20%,用 backtrader 内置的 FixedSize PercentSizer 都可以实现,但要记住:每个数据源对应策略的一个独立交易单元,你在 next 里调用哪个数据源的 buy ,就使用的是哪个数据源的价格和账户整体资金。资金分配逻辑要写清楚,否则会出现某只股票仓位过重,资金不足导致下单失败却不报错的情况。

class PercentSizer(bt.Sizer):
    params = (('percents', 20),)

    def _getsizing(self, comminfo, cash, data, isbuy):
        position_value = self.broker.getvalue() * self.params.percents / 100
        return int(position_value / data.close[0])

addsizer 挂到策略上后,每次 self.buy() 就会自动按总资金的 20% 计算仓位。注意这里分母用的是 data.close[0] ,如果想用下一根 K 线开盘价成交,应该用 self.data.open[1] 之类的值来估算。

7. 从回测到实盘,还差这最后一步

回测跑到这里,其实你已经掌握了 backtrader 最核心的 80% 功能。但我想多提醒一句:任何回测框架都只是辅助决策的工具,它不能保证实盘盈利。从回测到实盘,中间还有滑点偏差、交易延迟、情绪干扰、黑天鹅事件这些回测根本无法量化的变量。

如果是第一次使用 backtrader,我的建议是先把热情用在打磨数据清洗流程上。数据干净,策略就有坚实的底层;数据稀烂,哪怕你在框架里写出了花来,结果也只是笑话。别问我怎么知道的,我第一版回测跑出来年化 200% 的策略,换了个数据源之后立刻变成了 -30%,那个画面我到现在还记得。

工具只是工具,真正决定量化交易成败的,始终是你对市场的理解深度和执行纪律的强度。backtrader 帮你把回测这件事做扎实了,剩下的路,还得靠你自己一步步走出来。拿它练手,跑通一个最简单的双均线策略,再逐步加入资金管理、多标的轮动、止盈止损逻辑,这条路走顺了,你会发现量化交易并没有想象中那么神秘。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值