什么是BackTrader
BackTrader,量化回测框架。
- 官网:https://www.backtrader.com
- 官方文档:https://www.backtrader.com/docu
- Github地址:https://github.com/mementum/backtrader
- PyPi地址:https://pypi.org/project/backtrader
BackTrader支持股票、期货、数字货币等市场的量化回测分析以及实盘交易。
入门案例
示例代码
假设,存在一个策略:股票当日收盘时,若股价上穿超过5日均线,则次日开盘买入100股;若股价跌破5日均线且有持仓,则次日开盘全部抛出。
1 | from datetime import datetime |
运行结果:
1 | <backtrader.feeds.csvgeneric.GenericCSVData object at 0x000001E5E39AF8B0> |
策略迭代表
我们可以把行情数据整理成一张表,这张表记录了每日开盘、最高、最低、收盘和五日均线价格信息。
BackTrader做的事情,就是逐行遍历表。
这张表,也被称为"策略迭代表"。
Date | Open | High | Low | Close | 5 Day Average | Action |
---|---|---|---|---|---|---|
2025-01-10 | … | … | … | … | ||
2025-01-11 | … | … | … | … | ||
2025-01-12 | … | … | … | … | ||
2025-01-13 | … | … | … | … | ||
2025-01-16 | … | … | … | 2.07 | 2.11 | |
2025-01-17 | … | … | … | 2.14 | 2.1 | 买入 |
2025-01-18 | … | … | … | 2.14 | 2.11 | |
2025-01-19 | … | … | … | 2.21 | 2.13 | |
2025-01-20 | … | … | … | 2.17 | 2.15 | |
2025-01-23 | … | … | … | 2.15 | 2.16 | 卖出 |
2025-01-24 | … | … | … | 2.12 | 2.15 |
整体逻辑
定义策略类
上述代码首定义了策略类SmaCross,实现了一个基于五日均线的策略。
创建Cerebro(大脑引擎对象)
cerebro=bt.Cerebro()
,创建Cerebro引擎对象,该对象负责协调回测涉及的各个组件的活动。
创建行情数据对象并注入引擎
data=bt.feeds.GenericCSVData()
,创建行情数据对象(也被称为数据对象、数据馈送对象)。
在本例中,该对象从一个CSV文件获取行情数据。
在bt.feeds.GenericCSVData()
中,定义了开盘价、收盘价等在所在的列号(列从0
开始编号)。
如果列号为-1
,其含义该列在CSV文件中不存在,而不是最后一列。
cerebro.adddata
,将数据对象注入引擎,即可被策略使用。
注入策略到引擎
cerebro.addstrategy()
,将策略注入引擎,引擎内将实例化策略对象。
设置初始资金
cerebro.broker.setcash()
,设置初始资金。
执行回测
cerebro.run
,执行回测。
回测执行完毕后,通过cerebro.broker.getvalue()
获取市值。
附录:data.csv
在本例中,data.csv
文件内容如下:
1 | 日期,开盘,最高,最低,收盘,成交量,成交额 |
下载地址:data.csv
策略的两个方法
在上述代码中,cerebro.run()
会对策略类SmaCross
进行回测。
主要会执行两个方法:
__init__
方法
该方法只会被执行一次。
这里定义了一个5日移动平均线指标。next
方法
需要注意的是,在next
方法中会自动跳过不必要的bar。
例如,在本例中,由于使用的是5日均线,因此前4个bar被跳过,从第5个bar才有5日均线值,next方法会自动从第5个bar开始迭代。
line
概念
行情数据对象self.datas[0]
self.datas[0]
,指向,向cerebro
注入的第一个行情数据对象。
类似的,self.datas[1]
,指向第二个数据对象。
线line
我们可以认为行情数据对象,即如下的表。
表中每一列在BackTrader中称为一条线line,线由一系列数据点组成。
例如,close线由一系列收盘价构成。
注意,line
的含义是列,而非行。
日期 | 开盘 | 最高 | 最低 | 收盘 | 成交量 | 成交额 |
---|---|---|---|---|---|---|
2024-01-01 | 100.35860525386902 | 101.24472657828743 | 100.35860525386902 | 101.01342830602246 | 142229 | 571828 |
2024-01-02 | 100.54787705080938 | 101.29984886795457 | 100.54787705080938 | 100.75429997021092 | 186652 | 870188 |
2024-01-03 | 102.02803482411015 | 102.57801557323417 | 102.02803482411015 | 102.07959893530625 | 349474 | 358517 |
2024-01-04 | 104.94686330256391 | 105.22459736136955 | 104.68517418193267 | 105.20942039326589 | 803724 | 540097 |
2024-01-05 | 105.07970966557303 | 105.07970966557303 | 104.60276771150295 | 104.73775946058915 | 360116 | 408557 |
含线对象(含line对象)
什么是含线对象
含有一条或多条线的对象称为含线对象,含线对象有一个属性lines
,lines
中含有一条或多条线。
上文的行情数据对象(如self.datas[0]
)、指标对象如(self.move_average
)都是含line对象;策略自身self
其实也是含线对象。
简写
self.datas[0]
可以简写成self.data
、self.data0
。self.datas[X]
可以简写成self.dataX
,其中X是整数。
线对象(line对象)
访问线对象
- 索引方式
通过索引方式访问含线对象的lines
属性,可以访问具体线对象。
例如:self.datas[0].lines[0]
,访问含线对象中的0号线。 - 名称方式
这种方式更常见,也更方便清晰。
例如:self.datas[0].lines.close
可访问收盘线对象。
如果想知道含线对象具体含有哪些线,可通过lines
的方法getlinealiases
获取。
例如:
self.datas[0].lines.getlinealiases()
,返回元组('close', 'low', 'high', 'open', 'volume', 'openinterest', 'datetime')
。self.move_average.lines.getlinealiases()
,返回元组('sma',)
,只含一根线sma
。self.lines.getlinealiases()
,返回元组('datetime',)
,即策略自身只含一条datetime
线。
线对象的长度
线由一系列的数据点组成。
策略在策略迭代表上迭代过程中,点的个数动态增长。即已经被"next"的bar的数量在不断增长。
简写(lines可省略)
lines可省略,例如:
self.datas[0].lines.close
,可以被简写成self.datas[0].close
,还可进一步简写为self.data.close
。self.move_average.lines.sma
,可以被简写成self.move_average.sma
。
在next中用法
在next方法中通过索引访问line中的点
在next
方法中,可以通过索引访问线中的点,例如self.datas[0].close[0]
访问close线中当前bar的收盘价,索引-1
访问上一个bar的收盘价,1
访问下一个bar的收盘价。
在next方法中访问datetime线中的点
策略自身含有datetime
线,线上的每个点代表一个日期时间。
self.datetime.datetime(0)
,当前的日期时间;self.datetime.datetime(-1)
上一根bar的日期时间。如果省略参数,如self.datetime.datetime()
,则默认参数为0
。
但!无法访问通过策略自身的datetime
线访问下一个bar的时间,即self.datetime.datetime(1)
会报错。
小结
- 针对策略自身
- 访问日期时间:
self.datetime.datetime(0)
- 访问日期:
self.datetime.date(0)
- 访问日期时间:
- 针对行情数据对象
- 访问日期时间:
self.datas[0].datetime.datetime(0)
- 访问日期:
self.datas[0].datetime.date(0)
- 访问日期时间:
注意
- 最后用的是圆括号,而不是方括号。
- 无法通过策略自身的
datetime
线访问下一个bar的时间,self.datetime.datetime(1)
会报错。
但可以用数据对象的datetime
线访问下一根bar的时间,self.datas[0].datetime.datetime(1)
是可以的。
简写
规则
- 含线对象,可当作,默认线的当前值。
- 线对象,可当作,当前值。
例子一
- 含线对象
self.data
、self.datas[0]
,线对象self.data.close
、self.datas[0].close
,都可当作,当前值self.datas[0].close[0]
。 - 含线对象
self.move_average
、线对象self.move_average.sma
,都可当作,当前值self.move_average.sma[0]
。
例子二
如下代码是等效的:
1 | if self.data.close[0] < self.move_average.sma[0] |
在init中用法
访问线整体
例如,可以用self.datas[0].close
,访问线整体。
进行线整体运算(构造新指标)
我们可以在init
方法中,对不同线或含线对象进行整体的加减等操作。
示例代码:
1 | self.dif=self.data.close-self.move_average.sma |
解释说明:dif
可以当作线对象使用,其值是收盘价和5日均线的差价系列。
这种整体操作是一种矢量化操作,性能极快,通常用于构造新指标。
然后,在next中,可以像一般线对象那样 用self.dif[0]
访问dif
的当前值。
简写
规则
含线对象的lines
列表里的第一根线是其默认线。
例子一
假设是close
是lines
列表里的第一根线,则self.data
、self.datas[0]
,可当作,默认线对象self.data[0].close
。
例子二
如下三行代码是等效的:
1 | self.move_average=bt.ind.MovingAverageSimple(self.datas[0].close, self.params.period=5) |
另,也可省略第一个参数,写成:self.move_average=bt.ind.MovingAverageSimple(self.params.period=5)
,这样会默认使用self.datas[0].close
当做其第一个参数。
注意
不能在init方法中访问线中的点。
更多用法
线的切片
切片,是指将一段数据按照特定的规则或条件进行分割。可以根据不同的维度,如属性、时间等。
线的切片,是指对一根线,按照时间进行切片。
在下文的代码中,self.data.close.get(ago=-1, size=10)
,获取当前时间点之前的10个收盘价,ago=-1
表示从当前时间点开始往回数,size=10
表示获取10个数据点。
示例代码:
1 | my_slice = self.data.close.get(ago=-1, size=10) |
运行结果:
1 | <backtrader.feeds.csvgeneric.GenericCSVData object at 0x00000191712A6370> |
生成时间错位的线
注意,需要使用圆括号()
,而不是方括号[]
。
下文的代码,在策略类的init
方法中,定义一些错位的线。
1 | def __init__(self): |
为行情数据对象提供名字
命名方法
例如,cerebro.adddata(data,name='600000')
,在注入行情数据对象的同时,对行情数据对象进行命名。
使用方法
通过名称获取数据:
1 | self.data_by_name = self.getdatabyname('dname') |
示例代码:
1 | def __init__(self): |
order
notify 方法
notify_order
,订单通知方法,每当订单状态发生变化时,该方法都会被调用。
另有,notify_trade
,交易通知方法,每当交易状态发生变化时,该方法都会被调用。
示例代码:
1 | from datetime import datetime |
运行结果:
1 | <backtrader.feeds.csvgeneric.GenericCSVData object at 0x7f9028ee3990> |
解释说明:buy
指令下达的买单,将在次日以min(最高价, 开盘价+滑点)
的价格执行,sell
指令创建的卖单将在次日以max(最低价, 开盘价-滑点)
的价格执行。
订单状态流转
如图所示,是BackTrader中订单状态流转。
注意:
- Partial,部分完成,也是未决状态。这个状态一般只在实盘交易才会出现,回测时一般不出现(使用filler功能除外)。
- 在BackTrader中,一般订单可分为两种:
- 市价单
根据BackTrader的规则,市价单会在迭代到下一根Bar,并总是以下一根Bar的OPEN价格成交。
在上文的例子中,我们创建的都是市价单, - 限价单
对于限价单,在下一根Bar,订单可能无法成交,此时该订单是未决订单,其状态是Accepted。
- 市价单
trade
定义
其他软件的定义
在一些"炒股软件"中,尤其是实盘中,一个订单可能会有多个成交,比如卖10000手,先成交了2000手,再成交了8000手。
这里的订单被称为order、成交被称为trade。
即,一个order由一个或多个trade组成。
BackTrader中的定义
在BackTrader中,trade
的含义,更类似于:“我每把交易能赚XXX”,包括了"买"和"卖"两部分。
在技术上,当仓位大于0的时候,交易打开,当仓位等于0的时候,交易关闭。这被称为一次交易。
例如:
- 不能做空的情况下
报了一个买单,仓位从0变为100,打开了一个交易;此后,无论如何调仓或者补单,只要仓位依旧大于0,都不算交易关闭;直到仓位重新回到0,交易关闭。 - 可以做空的情况下
类似,当仓位从0变为非0(长仓为正值,短仓为负值),则打开了一个交易;此后仓位从非0值变为0值,则交易关闭。
notify_trade的触发机制
只有在交易打开,或关闭时,会触发notify_trade
。
其他时刻,无论如何调仓或者补单,都不会触发notify_trade
。
trade对象的常用属性
属性 | 说明 |
---|---|
trade.pnl |
该交易的净利润(已实现盈亏),计算方式:平仓价格 - 开仓价格 ± 手续费 |
trade.pnlcomm |
包含佣金的净利润 |
trade.status |
交易状态(如 Open , Closed ) |
trade.size |
交易的头寸大小(正数为多单,负数为空单) |
trade.price |
最近一次成交价格 |
trade.value |
当前持仓价值 |
trade.baropen |
开仓时的 Bar 索引(即时间点) |
trade.barclose |
平仓时的 Bar 索引 |
trade.dtopen |
开仓时间(datetime 对象) |
trade.dtclose |
平仓时间(datetime 对象) |
策略自身的_trades属性
什么是_trades属性
策略自身有_trades
属性,记录发生过的交易trade列表。
例如,self._trades[self.data0][0]
,返回的是与data0
相关的,tradeid
为0
的trade
列表,访问这个列表中每个trade的信息。
示例代码:
1 | def stop(self): |
运行结果:
1 |
|
tradeid的作用
默认情况下所有交易使用tradeid=0
。
如果需要区分不同批次或策略逻辑分支的交易,可以手动指定tradeid
(通过buy/sell
方法的tradeid
参数)。
_trades属性的结构
_trades
是BackTrader策略(bt.Strategy
)的内置属性,用于记录策略运行期间所有交易的完整历史。
其数据结构是"嵌套的字典",具体层级如下:
- 第一层键(Key):
Data Feed
对象(如self.data0
,self.data1
),表示交易所属的数据源。 - 第二层键(Key):
tradeid
(整数),用于区分同一数据源上的不同交易组(例如多空对冲或分批次交易)。
值(Value):与该tradeid
关联的Trade
对象列表,按时间顺序记录每一次开仓到平仓的完整交易周期。
示例结构:
1 | { |
trade的history属性
trade的history属性,一个列表,记录了每笔交易的详细历史。
需要手动指定启用记录交易历史功能:
1 | cerebro = bt.Cerebro(tradehistory=True) |
示例代码:
1 | def notify_trade(self, trade): |
运行结果:
1 |
|
解释说明:trade.history
列表中每个元素含有status
和event
两个属性,这两个属性都是有序字典,可以进一步利用。操作符访问其内部键值,比如h.status.status
、h.status.dt
、h.status.size
、h.event.order
、h.event.size
等。
传参
传递方式
两种方式
整体思路,以类属性的方式定义其所需参数。
有两种形式:
- 字典
- 元组
可以通过cerebro.addstrategy(SmaCross, period_fast=3)
,覆盖默认值。
字典方式
定义参数:
1 | params = dict(period_fast=5, period_slow=10, ) |
示例代码:
1 | from datetime import datetime |
运行结果:
1 | 3 |
元组方式
定义参数:
1 | params = (('period_fast', 5), ('period_slow', 10),) |
示例代码:
1 | from datetime import datetime |
核心参数配置
数据预加载与指标计算
参数名 | 默认值 | 功能说明 | 相互关系 |
---|---|---|---|
preload | True | 是否一次性加载全部行情数据到内存 | 开启时允许runonce生效 |
runonce | True | 是否使用向量化方式预计算指标(需preload=True) | 关闭时转为动态逐K线计算 |
exactbars | 0 | 内存优化模式,控制数据存储策略 | 设置非0值会禁用preload/runonce |
内存优化模式(exactbars)
1 | # 三种工作模式示例 |
模式值 | 内存策略 | 允许预加载 | 允许绘图 |
---|---|---|---|
0 | 全量存储所有数据 | 是 | 是 |
1 | 仅缓存最小必需数据(如计算30日均线时保留最近30根K线) | 否 | 否 |
-1 | 主数据全量存储,次级指标动态缓存 | 是 | 是 |
建议:
- 回测大数据量时推荐组合:
runonce=False + exactbars=1
。 - 常规回测建议保持
preload=True + runonce=True
。
参数设置方式
两种等效配置方法:
1 | # 方式一:创建时配置 |
DataFeed
GenericCSVData
基本用法
GenericCSVData,我们在上文已经使用过了。
用于加载CSV文件。
示例代码:
1 | # 创建行情数据对象,加载数据 |
注意:如果不指定fromdate
和todate
,则默认读取全部记录。
扩展GenericCSVData
默认的GenericCSVData类只包含datetime
、open
、high
、low
、close
、volume
、openinterest
共7根线数据。
如果要使用更多的数据,我们可以扩展GenericCSVData。
示例代码:
1 | from backtrader.feeds import GenericCSVData |
GenericCSVData_PE
,只是继承了GenericCSVData
,增加了一些成员变量,使用方法和GenericCSVData
没有差异。
示例代码:
1 | # 创建行情数据对象,加载数据 |
PandasData
如果想用Pandas.dataframe加载数据,可以考虑BackTrader的PandasData类的数据馈送对象。
示例代码:
1 | import backtrader as bt |
同样,我们扩展PandasData,方法和上文的"扩展GenericCSVData"一样,在这里略。
PandasDirectData
PandasDirectData,读取速度比PandasData快了几乎1倍。
使用PandasDirectData,需要遵循如下规则:
- df的日期时间列要设为索引列。
- df里不能有字符串列,比如股票代码列。
data=bt.feeds.PandasDirectData(...)
时,不能设置datetime列,会采用自动索引。
具体使用案例略。
策略类
成员属性
核心环境引用
env
:策略所属的cerebro
实体,可访问全局环境属性。broker
:关联的经纪行对象引用,用于访问账户信息与交易操作。
数据对象相关
datas
:注入策略的行情数据对象数组。data/data0
:datas[0]
的别名。dataX
:datas[X]
的别名(如data1
)。dnames
:通过名称访问数据对象的字典。1
2
3
4# 示例:通过名称访问
sma_days = bt.ind.SMA(self.dnames.days, period=30)
# 或
sma_days = bt.ind.SMA(self.dnames['days'], period=30)
交易相关
position
:data0
的仓位对象。1
2self.position.size # 当前持仓数量
self.position.price # 平均持仓价格_orderspending
:待处理的未决订单列表(触发notify_order
前)。tradespending
:待处理的交易列表(触发notify_trade
前)。
监控与分析
stats
:访问观察者对象(OBSERVERS)analyzers
:访问分析者对象。
历史记录
orders
:已通知策略的订单历史。trades
:已通知策略的交易历史。
成员方法
生命周期方法
方法 | 触发时机 | 说明 |
---|---|---|
start() |
策略启动时 | 初始化操作 |
prenext() |
数据未填充完成时 | 每个bar周期触发 |
nextstart() |
数据首次填充完成时 | 只触发一次 |
next() |
每个完整bar周期 | 主要交易逻辑 |
stop() |
策略结束时 | 清理操作 |
通知回调方法
1 | # 订单状态变化时触发 |
交易指令方法
基础指令
1 | # 做多 |
目标仓位管理
1 | # 调整至目标数量 |
组合指令
1 | # 带止损止盈的买入组合订单 |
订单控制
1 | # 取消指定订单 |
仓位管理方法
1 | # 获取指定数据仓位 |
使用示例:
1 | pos = self.getposition(data=self.data0) |
数据操作方法
1 | # 获取数据对象名称列表 |
定时器方法
1 | # 添加定时器 |
资金管理
1 | # 获取当前sizer |
解释说明:
Sizer
BackTrader中负责计算下单数量(即买入或卖出多少单位的资产)的一个组件。getsizing(data=None, isbuy=True)
用来计算具体的下单量。该方法通常由Sizer内部调用,但也可以被策略直接调用来手动计算下单量。参数data
指定要操作的数据源(通常是股票代码或其他资产),isbuy
则表示这是一次买入操作还是卖出操作(默认为买入)。返回值是根据Sizer规则计算出的应下单的数量。