avatar


1.基础

什么是BackTrader

BackTrader,量化回测框架。

BackTrader支持股票、期货、数字货币等市场的量化回测分析以及实盘交易。

入门案例

示例代码

假设,存在一个策略:股票当日收盘时,若股价上穿超过5日均线,则次日开盘买入100股;若股价跌破5日均线且有持仓,则次日开盘全部抛出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
from datetime import datetime
import backtrader as bt


class SmaCross(bt.Strategy):
# 定义参数
# 移动平均期数
params = dict(period=5)

def __init__(self):
# 移动平均线指标
self.move_average = bt.ind.MovingAverageSimple(
self.datas[0].close, period=self.params.period)

def next(self):
print(self.datetime.datetime().strftime('%Y-%m-%d'))
# 还没有仓位
if not self.position.size:
# 当日收盘价上穿5日均线,创建买单,买入100股
if (self.datas[0].close[-1] < self.move_average.sma[-1]) and (self.datas[0].close[0] > self.move_average.sma[0]):
self.buy(size=100)
# 有仓位,并且当日收盘价下破5日均线,创建卖单,卖出100股
elif (self.datas[0].close[-1] > self.move_average.sma[-1]) and (self.datas[0].close[0] < self.move_average.sma[0]):
self.sell(size=100)


# 创建大脑引擎对象
cerebro = bt.Cerebro()

# 创建行情数据对象,加载数据
data = bt.feeds.GenericCSVData(
# 文件路径
dataname='./data.csv',
# 日期行所在列
datetime=0,
# 开盘价所在列
open=1,
# 最高价所在列
high=2,
# 最低价所在列
low=3,
# 收盘价所在列
close=4,
# 成交量所在列
volume=5,
# 日期格式
dtformat='%Y-%m-%d',
# 起始日
fromdate=datetime(2024, 1, 1),
# 结束日
todate=datetime(2024, 12, 31))

print(data)

# 将行情数据对象注入引擎
cerebro.adddata(data)
# 将策略注入引擎
cerebro.addstrategy(SmaCross)
# 设置初始资金
cerebro.broker.setcash(10000.0)
# 运行
cerebro.run()
print('最终市值:%.2f' % cerebro.broker.getvalue())
# 画图
cerebro.plot(style='bar')

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<backtrader.feeds.csvgeneric.GenericCSVData object at 0x000001E5E39AF8B0>
2024-01-05
2024-01-08
2024-01-09
2024-01-10
2024-01-11

【部分运行结果略】

2024-12-24
2024-12-25
2024-12-26
2024-12-27
2024-12-30
最终市值:9938.48

回测

策略迭代表

我们可以把行情数据整理成一张表,这张表记录了每日开盘、最高、最低、收盘和五日均线价格信息。

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
2
3
4
5
6
日期,开盘,最高,最低,收盘,成交量,成交额
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

下载地址: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对象)

什么是含线对象

含有一条或多条线的对象称为含线对象,含线对象有一个属性lineslines中含有一条或多条线。

上文的行情数据对象(如self.datas[0])、指标对象如(self.move_average)都是含line对象;策略自身self其实也是含线对象。

简写

  • self.datas[0]可以简写成self.dataself.data0
  • self.datas[X]可以简写成self.dataX,其中X是整数。

线对象(line对象)

访问线对象

  1. 索引方式
    通过索引方式访问含线对象的lines属性,可以访问具体线对象。
    例如:self.datas[0].lines[0],访问含线对象中的0号线。
  2. 名称方式
    这种方式更常见,也更方便清晰。
    例如: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的数量在不断增长。

next方法中新增如下一行:

1
print(len(self), len(self.data), len(self.datas), len(self.datas[0]), self.datas[0].buflen())
运行结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<backtrader.feeds.csvgeneric.GenericCSVData object at 0x0000017F5113FA30>
5 5 1 5 261
6 6 1 6 261
7 7 1 7 261
8 8 1 8 261
9 9 1 9 261

【部分运行结果略】

257 257 1 257 261
258 258 1 258 261
259 259 1 259 261
260 260 1 260 261
261 261 1 261 261
最终市值:9938.48

解释说明:

  • len(self)
    当前已经被"next"的bar的数量。
    统计的是datetime线的长度。
  • len(self.data)等同于len(self.datas[0])
    对只涉及一个数据对象的策略,在next方法中,len(self)len(self.data)长度是一样的,但若策略加载了多个数据对象,则不同数据对象和策略自身的当前线长有可能不一样。
  • len(self.datas),不是线的长度,是行情数据对象列表的长度,在本文只注入了一个行情数据对象。
  • self.datas[0].buflen()
    对于数据馈送对象,如果数据是预加载的(即一次性将所有数据都注入到策略,而不是动态加载数据),则可用方法buflen取得其总长度。
    一般在BackTrader中,是预加载数据,但是在有重采样或实盘交易时,无法预加载数据,此时self.datas[0].buflen()会动态增长。

简写(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)会报错。

对行情数据对象的datetime线,也可进行类似操作。

小结

  • 针对策略自身
    • 访问日期时间:self.datetime.datetime(0)
    • 访问日期:self.datetime.date(0)
  • 针对行情数据对象
    • 访问日期时间:self.datas[0].datetime.datetime(0)
    • 访问日期:self.datas[0].datetime.date(0)

注意

  1. 最后用的是圆括号,而不是方括号。
  2. 无法通过策略自身的datetime线访问下一个bar的时间,self.datetime.datetime(1)会报错。
    但可以用数据对象的datetime线访问下一根bar的时间,self.datas[0].datetime.datetime(1)是可以的。

简写

规则

  1. 含线对象,可当作,默认线的当前值。
  2. 线对象,可当作,当前值。

例子一

  • 含线对象self.dataself.datas[0],线对象self.data.closeself.datas[0].close,都可当作,当前值self.datas[0].close[0]
  • 含线对象self.move_average、线对象self.move_average.sma,都可当作,当前值self.move_average.sma[0]

例子二

如下代码是等效的:

1
2
3
if self.data.close[0] < self.move_average.sma[0]
if self.data.close < self.move_average.sma
if self.data < self.move_average

在init中用法

访问线整体

例如,可以用self.datas[0].close,访问线整体。

进行线整体运算(构造新指标)

我们可以在init方法中,对不同线或含线对象进行整体的加减等操作。

示例代码:

1
self.dif=self.data.close-self.move_average.sma

解释说明:dif可以当作线对象使用,其值是收盘价和5日均线的差价系列。

这种整体操作是一种矢量化操作,性能极快,通常用于构造新指标。

然后,在next中,可以像一般线对象那样 用self.dif[0]访问dif的当前值。

简写

规则

含线对象的lines列表里的第一根线是其默认线。

例子一

假设是closelines列表里的第一根线,则self.dataself.datas[0],可当作,默认线对象self.data[0].close

例子二

如下三行代码是等效的:

1
2
3
self.move_average=bt.ind.MovingAverageSimple(self.datas[0].close, self.params.period=5)
self.move_average=bt.ind.MovingAverageSimple(self.datas[0], self.params.period=5)
self.move_average=bt.ind.MovingAverageSimple(self.data, 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
2
3
4
5
my_slice = self.data.close.get(ago=-1, size=10)
my_slice_len = len(my_slice)
print('my_slice_len', my_slice_len)
for i in range(my_slice_len):
print(my_slice[i])

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<backtrader.feeds.csvgeneric.GenericCSVData object at 0x00000191712A6370>
my_slice_len 0
my_slice_len 0
my_slice_len 0
my_slice_len 0
my_slice_len 0
my_slice_len 0
my_slice_len 10
101.01342830602246
100.75429997021092
102.07959893530625
105.20942039326589
104.73775946058915
104.26824740692571
107.58233610751734
109.255100994185
108.25110258539372
109.44740726465001
my_slice_len 10
100.75429997021092
102.07959893530625
105.20942039326589
104.73775946058915
104.26824740692571
107.58233610751734
109.255100994185
108.25110258539372
109.44740726465001
108.45489944692514

【部分运行结果略】

100.68580077013765
103.25713720150107
101.81635016684713
102.74047492302023
104.35274842882232
最终市值:9938.48

生成时间错位的线

注意,需要使用圆括号(),而不是方括号[]

下文的代码,在策略类的init方法中,定义一些错位的线。

1
2
3
4
5
6
7
8
9
10
11
12
13
def __init__(self):
# 移动平均线指标
self.move_average = bt.ind.MovingAverageSimple(
self.datas[0].close, period=self.params.period)

# 收盘线下移1天(1个bar),则当前dataearly的值是昨日的收盘价
self.close_early =self.data.close(-1)
# 收盘线上移1天
self.close_late = self.data.close(1)
# 移动平均线下移两天
self.sma_early = self.move_average.sma(-2)
# 生成比较值,结果为1或0
self.cmp_val = self.data.close(-1) > self.move_average.sma

为行情数据对象提供名字

命名方法

例如,cerebro.adddata(data,name='600000'),在注入行情数据对象的同时,对行情数据对象进行命名。

使用方法

通过名称获取数据:

1
self.data_by_name = self.getdatabyname('dname')

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def __init__(self):
# 通过名称获取数据
self.data_by_name = self.getdatabyname('dname')
# 使用命名数据创建移动平均线指标
self.move_average = bt.ind.MovingAverageSimple(
self.data_by_name.close, period=self.params.period)
# 收盘线下移1天(1个bar),则当前dataearly的值是昨日的收盘价
self.close_early = self.data_by_name.close(-1)
# 收盘线上移1天
self.close_late = self.data_by_name.close(1)
# 移动平均线下移两天
self.sma_early = self.move_average.sma(-2)
# 生成比较值,结果为1或θ
self.cmp_val = self.data_by_name.close(-1) > self.move_average.sma

def next(self):
my_slice = self.data_by_name.close.get(ago=-1, size=10)
my_slice_len = len(my_slice)
print('my_slice_len', my_slice_len)
for i in range(my_slice_len):
print(my_slice[i])
# 还没有仓位
if not self.position.size:
# 当日收盘价上穿5日均线,创建买单,买入100股
if (self.data_by_name.close[-1] < self.move_average.sma[-1]) and (self.data_by_name.close[0] > self.move_average.sma[0]):
self.buy(size=100)
# 有仓位,并且当日收盘价下破5日均线,创建卖单,卖出100股
elif (self.data_by_name.close[-1] > self.move_average.sma[-1]) and (self.data_by_name.close[0] < self.move_average.sma[0]):
self.sell(size=100)

order

notify 方法

notify_order,订单通知方法,每当订单状态发生变化时,该方法都会被调用。

另有,notify_trade,交易通知方法,每当交易状态发生变化时,该方法都会被调用。

其实我们还可以特别关注和交易相关的日志。
在一些"炒股软件"中,尤其是实盘中,一个订单可能会有多个成交,比如卖10000手,先成交了2000手,再成交了8000手。
这里的订单被称为order、成交被称为trader。

但在BackTrader中,trade的含义不是这个。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
from datetime import datetime
import backtrader as bt


class SmaCross(bt.Strategy):
# 定义参数
# 移动平均期数
params = dict(period=5)

def __init__(self):
# 移动平均线指标
self.move_average = bt.ind.MovingAverageSimple(
self.datas[0].close, period=self.params.period)

def notify_order(self, order):
if order.status in [order.Submitted, order.Accepted]:
# 订单状态 submitted accepted
return

# 订单完成
if order.status in [order.Completed]:
if order.isbuy():
print('买单执行,%.2f' % order.executed.price)
if order.issell():
print('卖单执行,%.2f' % order.executed.price)

if order.status in [order.Canceled, order.Margin, order.Rejected]:
print('订单 Canceled/Margin/Rejected')

def notify_trade(self, trade):
if trade.isopen:
print('交易打开')
if trade.isclosed:
print('交易关闭', '毛收益 %.2f,扣佣后收益 %.2f,佣金 %.2f' % (trade.pnl, trade.pnlcomm, trade.commission))

def next(self):
# 还没有仓位
print(self.data.open[0], self.data.close[0], self.data.high[0], self.data.low[0])
if not self.position.size:
# 当日收盘价上穿5日均线,创建买单,买入100股
if (self.datas[0].close[-1] < self.move_average.sma[-1]) and (self.datas[0].close[0] > self.move_average.sma[0]):
self.buy(size=100)
# 有仓位,并且当日收盘价下破5日均线,创建卖单,卖出100股
elif (self.datas[0].close[-1] > self.move_average.sma[-1]) and (self.datas[0].close[0] < self.move_average.sma[0]):
self.sell(size=100)


# 创建大脑引擎对象
cerebro = bt.Cerebro()

# 创建行情数据对象,加载数据
data = bt.feeds.GenericCSVData(
# 文件路径
dataname='./data.csv',
# 日期行所在列
datetime=0,
# 开盘价所在列
open=1,
# 最高价所在列
high=2,
# 最低价所在列
low=3,
# 收盘价所在列
close=4,
# 成交量所在列
volume=5,
# 日期格式
dtformat='%Y-%m-%d',
# 起始日
fromdate=datetime(2024, 1, 1),
# 结束日
todate=datetime(2024, 12, 31))

print(data)

# 将行情数据对象注入引擎
cerebro.adddata(data)
# 将策略注入引擎
cerebro.addstrategy(SmaCross)
# 设置初始资金
cerebro.broker.setcash(10000.0)
# 佣金费率
cerebro.broker.setcommission(0.001)
# 滑点 cerebro.broker.set_slippage_perc() 设置百分比滑点
cerebro.broker.set_slippage_fixed(0.05)
# 运行
cerebro.run()
print('最终市值:%.2f' % cerebro.broker.getvalue())
# 画图
cerebro.plot(style='bar')

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<backtrader.feeds.csvgeneric.GenericCSVData object at 0x7f9028ee3990>
105.07970966557303 104.73775946058915 105.07970966557303 104.60276771150295
103.97001073687754 104.26824740692571 104.55112193572745 103.97001073687754
107.48765407977714 107.58233610751734 107.78843991312507 107.48765407977714
109.28366914413587 109.255100994185 109.79968962080618 108.83662711039871
108.56314143073028 108.25110258539372 108.56314143073028 107.90002589802468

【部分运行结果略】

93.53580784735871 93.41210580610705 93.81608568766603 93.3254645487778
96.47402620784483 96.16897498481009 96.88206153912235 96.10488785342751
买单执行,95.57
交易打开
95.51690381211485 95.75395527193399 95.98287288988018 95.51690381211485
卖单执行,96.26
交易关闭 毛收益 69.47,扣佣后收益 50.28,佣金 19.18
96.3115540757283 95.90242791681402 96.3115540757283 95.54113773287229
买单执行,92.88
交易打开
92.82504647690111 93.18887219783844 93.24563690942941 92.82504647690111
卖单执行,92.14
交易关闭 毛收益 -73.38,扣佣后收益 -91.88,佣金 18.50
92.16491471272701 92.19290172942823 92.29555277452123 92.14121701204968
92.52460550605386 92.41586581802804 92.58331925994285 92.29395143859558

【部分运行结果略】

104.46615068774564 104.35274842882232 104.54389733083781 104.0761113993628
102.3033070167997 102.43906413227614 102.61109736106486 102.23859409674591
最终市值:9893.81

解释说明: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的时候,交易关闭。这被称为一次交易。

例如:

  1. 不能做空的情况下
    报了一个买单,仓位从0变为100,打开了一个交易;此后,无论如何调仓或者补单,只要仓位依旧大于0,都不算交易关闭;直到仓位重新回到0,交易关闭。
  2. 可以做空的情况下
    类似,当仓位从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相关的,tradeid0trade列表,访问这个列表中每个trade的信息。

示例代码:

1
2
3
4
def stop(self):
print(self._trades)
for t in self._trades[self.data0][0]:
print(t.pnl)

运行结果:

1
2
3
4
5
6
7
8
9
10
11

【部分运行结果略】

defaultdict(<class 'backtrader.utils.autodict.AutoDictList'>, {<backtrader.feeds.csvgeneric.GenericCSVData object at 0x7fabb86e3ad0>: {0: [<backtrader.trade.Trade object at 0x7fabb8807cd0>, <backtrader.trade.Trade object at 0x7fabb881b1d0>, <backtrader.trade.Trade object at 0x7fabb881b890>, <backtrader.trade.Trade object at 0x7fabb881bd90>, <backtrader.trade.Trade object at 0x7fabb8822210>, <backtrader.trade.Trade object at 0x7fabb8822a50>, <backtrader.trade.Trade object at 0x7fabb8822f50>, <backtrader.trade.Trade object at 0x7fabb8827610>, <backtrader.trade.Trade object at 0x7fabb8827bd0>, <backtrader.trade.Trade object at 0x7fabb882e190>, <backtrader.trade.Trade object at 0x7fabb882e750>, <backtrader.trade.Trade object at 0x7fabb882ed10>, <backtrader.trade.Trade object at 0x7fabb8835310>, <backtrader.trade.Trade object at 0x7fabb88358d0>, <backtrader.trade.Trade object at 0x7fabb8835d90>, <backtrader.trade.Trade object at 0x7fabb883c390>, <backtrader.trade.Trade object at 0x7fabb883ca50>, <backtrader.trade.Trade object at 0x7fabb883cf10>, <backtrader.trade.Trade object at 0x7fabb8842610>, <backtrader.trade.Trade object at 0x7fabb8842bd0>, <backtrader.trade.Trade object at 0x7fabb88491d0>, <backtrader.trade.Trade object at 0x7fabb8849790>, <backtrader.trade.Trade object at 0x7fabb8849c50>, <backtrader.trade.Trade object at 0x7fabb8850250>, <backtrader.trade.Trade object at 0x7fabb8850810>, <backtrader.trade.Trade object at 0x7fabb8850ed0>, <backtrader.trade.Trade object at 0x7fabb8857750>, <backtrader.trade.Trade object at 0x7fabb8857d10>, <backtrader.trade.Trade object at 0x7fabb885f310>, <backtrader.trade.Trade object at 0x7fabb885fad0>, <backtrader.trade.Trade object at 0x7fabb88650d0>, <backtrader.trade.Trade object at 0x7fabb8865690>, <backtrader.trade.Trade object at 0x7fabb8865ed0>]}})
69.4650263613454
-73.3829464851425
-42.76824170053857
38.02705709489089
-287.6142773853317

【部分运行结果略】

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
2
3
4
5
6
7
8
9
10
11
{
data0: {
// tradeid=0 的交易记录
0: [trade1, trade2, ...],
// tradeid=1 的交易记录
1: [trade3, trade4, ...]
},
data1: {
0: [trade5, ...]
}
}

trade的history属性

trade的history属性,一个列表,记录了每笔交易的详细历史。

需要手动指定启用记录交易历史功能:

1
cerebro = bt.Cerebro(tradehistory=True)

示例代码:

1
2
3
4
def notify_trade(self, trade):
for history_item in trade.history:
print('status:', history_item.status)
print('event:', history_item.event)

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

【部分运行结果略】

买单执行,95.57
status: AutoOrderedDict([('status', 1), ('dt', 738915.9999999999), ('barlen', 0), ('size', 100), ('price', 95.56690381211484), ('value', 9556.690381211485), ('pnl', 0.0), ('pnlcomm', -9.556690381211485), ('tz', None)])
event: AutoOrderedDict([('order', <backtrader.order.BuyOrder object at 0x7f8697f08d10>), ('size', 100), ('price', 95.56690381211484), ('commission', 9.556690381211485)])
95.51690381211485 95.75395527193399 95.98287288988018 95.51690381211485
卖单执行,96.26
status: AutoOrderedDict([('status', 1), ('dt', 738915.9999999999), ('barlen', 0), ('size', 100), ('price', 95.56690381211484), ('value', 9556.690381211485), ('pnl', 0.0), ('pnlcomm', -9.556690381211485), ('tz', None)])
event: AutoOrderedDict([('order', <backtrader.order.BuyOrder object at 0x7f8697f08d10>), ('size', 100), ('price', 95.56690381211484), ('commission', 9.556690381211485)])
status: AutoOrderedDict([('status', 2), ('dt', 738916.9999999999), ('barlen', 1), ('size', 0), ('price', 95.56690381211484), ('value', 0.0), ('pnl', 69.4650263613454), ('pnlcomm', 50.282180572561074), ('tz', None)])
event: AutoOrderedDict([('order', <backtrader.order.SellOrder object at 0x7f8697f1b050>), ('size', -100), ('price', 96.2615540757283), ('commission', 9.626155407572831)])
96.3115540757283 95.90242791681402 96.3115540757283 95.54113773287229
买单执行,92.88
status: AutoOrderedDict([('status', 1), ('dt', 738917.9999999999), ('barlen', 0), ('size', 100), ('price', 92.8750464769011), ('value', 9287.50464769011), ('pnl', 0.0), ('pnlcomm', -9.287504647690112), ('tz', None)])
event: AutoOrderedDict([('order', <backtrader.order.BuyOrder object at 0x7f8697f1b390>), ('size', 100), ('price', 92.8750464769011), ('commission', 9.287504647690112)])
92.82504647690111 93.18887219783844 93.24563690942941 92.82504647690111
卖单执行,92.14
status: AutoOrderedDict([('status', 1), ('dt', 738917.9999999999), ('barlen', 0), ('size', 100), ('price', 92.8750464769011), ('value', 9287.50464769011), ('pnl', 0.0), ('pnlcomm', -9.287504647690112), ('tz', None)])
event: AutoOrderedDict([('order', <backtrader.order.BuyOrder object at 0x7f8697f1b390>), ('size', 100), ('price', 92.8750464769011), ('commission', 9.287504647690112)])
status: AutoOrderedDict([('status', 2), ('dt', 738918.9999999999), ('barlen', 1), ('size', 0), ('price', 92.8750464769011), ('value', 0.0), ('pnl', -73.3829464851425), ('pnlcomm', -91.88457283403758), ('tz', None)])
event: AutoOrderedDict([('order', <backtrader.order.SellOrder object at 0x7f8697f1b710>), ('size', -100), ('price', 92.14121701204968), ('commission', 9.214121701204968)])
92.16491471272701 92.19290172942823 92.29555277452123 92.14121701204968

【部分运行结果略】

解释说明:trade.history列表中每个元素含有statusevent两个属性,这两个属性都是有序字典,可以进一步利用。操作符访问其内部键值,比如h.status.statush.status.dth.status.sizeh.event.orderh.event.size等。

传参

传递方式

两种方式

整体思路,以类属性的方式定义其所需参数。
有两种形式:

  1. 字典
  2. 元组

可以通过cerebro.addstrategy(SmaCross, period_fast=3),覆盖默认值。

字典方式

定义参数:

1
params = dict(period_fast=5, period_slow=10, )

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from datetime import datetime
import backtrader as bt


# 创建策略类
class SmaCross(bt.Strategy):
# 定义参数
params = dict(period_fast=5, period_slow=10, )

def __init__(self):
# 移动平均线指标
self.move_average = bt.ind.MovingAverageSimple(period=self.params.period_fast)
print(self.params.period_fast)

def next(self):
# 每次数据点触发
print("当前period_fast:", self.params.period_fast)


# 创建大脑引擎对象
cerebro = bt.Cerebro()

# 创建行情数据对象,加载数据
data = bt.feeds.GenericCSVData(dataname='./data.csv', datetime=0, open=1, high=2, low=3, close=4, volume=5, dtformat='%Y-%m-%d', fromdate=datetime(2024, 1, 1), todate=datetime(2024, 12, 31))

# 将行情数据对象注入引擎
cerebro.adddata(data)
# 将策略注入引擎
cerebro.addstrategy(SmaCross, period_fast=3)

cerebro.run()

运行结果:

1
2
3
4
5
6
7
8
3
当前period_fast: 3
当前period_fast: 3
当前period_fast: 3
当前period_fast: 3
当前period_fast: 3

【部分运行结果略】

元组方式

定义参数:

1
params = (('period_fast', 5), ('period_slow', 10),)

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from datetime import datetime
import backtrader as bt


# 创建策略类
class SmaCross(bt.Strategy):
# 定义参数
params = (('period_fast', 5), ('period_slow', 10),)

def __init__(self):
# 移动平均线指标
self.move_average = bt.ind.MovingAverageSimple(period=self.params.period_fast)
print(self.params.period_fast)

def next(self):
# 每次数据点触发
print("当前period_fast:", self.params.period_fast)


# 创建大脑引擎对象
cerebro = bt.Cerebro()

# 创建行情数据对象,加载数据
data = bt.feeds.GenericCSVData(dataname='./data.csv', datetime=0, open=1, high=2, low=3, close=4, volume=5, dtformat='%Y-%m-%d', fromdate=datetime(2024, 1, 1), todate=datetime(2024, 12, 31))

# 将行情数据对象注入引擎
cerebro.adddata(data)
# 将策略注入引擎
cerebro.addstrategy(SmaCross, period_fast=3)

cerebro.run()

核心参数配置

数据预加载与指标计算

参数名 默认值 功能说明 相互关系
preload True 是否一次性加载全部行情数据到内存 开启时允许runonce生效
runonce True 是否使用向量化方式预计算指标(需preload=True) 关闭时转为动态逐K线计算
exactbars 0 内存优化模式,控制数据存储策略 设置非0值会禁用preload/runonce

内存优化模式(exactbars)

1
2
3
4
5
6
7
# 三种工作模式示例
# 标准模式(默认)
cerebro.run(exactbars=0)
# 内存优化模式
cerebro.run(exactbars=1)
# 混合存储模式
cerebro.run(exactbars=-1)
模式值 内存策略 允许预加载 允许绘图
0 全量存储所有数据
1 仅缓存最小必需数据(如计算30日均线时保留最近30根K线)
-1 主数据全量存储,次级指标动态缓存

建议:

  1. 回测大数据量时推荐组合:runonce=False + exactbars=1
  2. 常规回测建议保持 preload=True + runonce=True

参数设置方式

两种等效配置方法:

1
2
3
4
5
6
# 方式一:创建时配置
cerebro = bt.Cerebro(preload=True, runonce=False)

# 方式二:运行时配置
cerebro = bt.Cerebro()
cerebro.run(preload=True, runonce=False)

DataFeed

GenericCSVData

基本用法

GenericCSVData,我们在上文已经使用过了。

用于加载CSV文件。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 创建行情数据对象,加载数据
data = bt.feeds.GenericCSVData(
# 文件路径
dataname='./data.csv',
# 日期行所在列
datetime=0,
# 开盘价所在列
open=1,
# 最高价所在列
high=2,
# 最低价所在列
low=3,
# 收盘价所在列
close=4,
# 成交量所在列
volume=5,
# 日期格式
dtformat='%Y-%m-%d',
# 起始日
fromdate=datetime(2024, 1, 1),
# 结束日
todate=datetime(2024, 1, 31))

注意:如果不指定fromdatetodate,则默认读取全部记录。

扩展GenericCSVData

默认的GenericCSVData类只包含datetimeopenhighlowclosevolumeopeninterest共7根线数据。

如果要使用更多的数据,我们可以扩展GenericCSVData。

示例代码:

1
2
3
4
5
6
7
from backtrader.feeds import GenericCSVData

class GenericCSVData_PE(GenericCSVData):
# 增加pe线
lines =('pe',)
# 默认第8列
params =(('pe',8),)

GenericCSVData_PE,只是继承了GenericCSVData,增加了一些成员变量,使用方法和GenericCSVData没有差异。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 创建行情数据对象,加载数据
data = GenericCSVData_PE(
# 文件路径
dataname='./data.csv',
# 日期行所在列
datetime=0,
# 开盘价所在列
open=1,
# 最高价所在列
high=2,
# 最低价所在列
low=3,
# 收盘价所在列
close=4,
# 成交量所在列
volume=5,
pe=6,
# 日期格式
dtformat='%Y-%m-%d',
# 起始日
fromdate=datetime(2024, 1, 1),
# 结束日
todate=datetime(2024, 1, 31))

PandasData

如果想用Pandas.dataframe加载数据,可以考虑BackTrader的PandasData类的数据馈送对象。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import backtrader as bt
import pandas as pd

from backtrader.feeds import PandasData

class SmaCross(bt.Strategy):
# 定义参数
# 移动平均期数
params = dict(period=5)

def __init__(self):
# 通过名称获取数据
self.data_by_name = self.getdatabyname('dname')

def next(self):
print(self.data_by_name.close[0])


# 创建大脑引擎对象
cerebro = bt.Cerebro()

# 创建示例数据
data = {
'datetime': ['2023-01-01', '2023-01-02', '2023-01-03', '2023-01-04', '2023-01-05'],
'open': [100, 101, 102, 103, 104],
'high': [105, 106, 107, 108, 109],
'low': [95, 96, 97, 98, 99],
'close': [102, 103, 104, 105, 106],
'volume': [1000, 2000, 3000, 4000, 5000]
}

# 转换为 DataFrame
df = pd.DataFrame(data)

# 将 datetime 列转换为 datetime 类型
df['datetime'] = pd.to_datetime(df['datetime'])

# 创建行情数据对象,加载数据
data = PandasData(
# 文件路径
dataname=df,
# 日期行所在列
datetime=0,
# 开盘价所在列
open=1,
# 最高价所在列
high=2,
# 最低价所在列
low=3,
# 收盘价所在列
close=4,
# 成交量所在列
volume=5)

print(data)

# 将行情数据对象注入引擎
cerebro.adddata(data, name='dname')
# 将策略注入引擎
cerebro.addstrategy(SmaCross)
# 运行
cerebro.run()

同样,我们扩展PandasData,方法和上文的"扩展GenericCSVData"一样,在这里略。

PandasDirectData

PandasDirectData,读取速度比PandasData快了几乎1倍。

使用PandasDirectData,需要遵循如下规则:

  1. df的日期时间列要设为索引列。
  2. df里不能有字符串列,比如股票代码列。
  3. data=bt.feeds.PandasDirectData(...)时,不能设置datetime列,会采用自动索引。

具体使用案例略。

策略类

成员属性

核心环境引用

  • env:策略所属的cerebro实体,可访问全局环境属性。
  • broker:关联的经纪行对象引用,用于访问账户信息与交易操作。

数据对象相关

  • datas:注入策略的行情数据对象数组。
  • data/data0datas[0]的别名。
  • dataXdatas[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)

交易相关

  • positiondata0的仓位对象。
    1
    2
    self.position.size  # 当前持仓数量
    self.position.price # 平均持仓价格
  • _orderspending:待处理的未决订单列表(触发notify_order前)。
  • tradespending:待处理的交易列表(触发notify_trade前)。

如果要访问非data0datas[1]的仓位信息,可通过self.getposition(self.datas[1])获取对应position,然后获取相关的值。如self.getposition(self.datas[1]).sizeself.getposition(self.datas[1]).price

监控与分析

  • stats:访问观察者对象(OBSERVERS)
  • analyzers:访问分析者对象。

历史记录

  • orders:已通知策略的订单历史。
  • trades:已通知策略的交易历史。

成员方法

生命周期方法

方法 触发时机 说明
start() 策略启动时 初始化操作
prenext() 数据未填充完成时 每个bar周期触发
nextstart() 数据首次填充完成时 只触发一次
next() 每个完整bar周期 主要交易逻辑
stop() 策略结束时 清理操作

通知回调方法

1
2
3
4
5
6
7
8
9
10
# 订单状态变化时触发
def notify_order(self, order):
# 交易状态变化时触发
def notify_trade(self, trade):
# 每bar更新现金与总资产
def notify_cashvalue(self, cash, value):
# 每bar更新基金价值
def notify_fund(self, cash, value, fundvalue, shares):
# 接收store通知
def notify_store(self, msg, *args, **kwargs):

交易指令方法

基础指令

1
2
3
4
5
6
# 做多
buy()
# 平多/做空
sell()
# 平仓
close()

目标仓位管理

1
2
3
4
5
6
# 调整至目标数量
order_target_size()
# 调整至目标价值
order_target_value()
# 调整至账户比例
order_target_percent()

组合指令

1
2
3
4
# 带止损止盈的买入组合订单
buy_bracket()
# 带止损止盈的卖出组合订单
sell_bracket()

订单控制

1
2
# 取消指定订单
cancel(order)

仓位管理方法

1
2
3
4
5
6
# 获取指定数据仓位
getposition(data=None, broker=None)
# 获取全部仓位字典
getpositions(broker=None)
# 按名称获取仓位
getpositionbyname(name=None, broker=None)

使用示例:

1
2
pos = self.getposition(data=self.data0)
all_pos = self.getpositions()[self.data0]

数据操作方法

1
2
3
4
# 获取数据对象名称列表
getdatanames()
# 按名称获取数据对象
getdatabyname(name)

定时器方法

1
2
3
4
# 添加定时器
add_timer()
# 定时器触发回调
notify_timer()

资金管理

1
2
3
4
5
6
# 获取当前sizer
getsizer()
# 设置sizer
setsizer(sizer)
# 计算下单量
getsizing(data=None, isbuy=True)

解释说明:

  • Sizer
    BackTrader中负责计算下单数量(即买入或卖出多少单位的资产)的一个组件。
  • getsizing(data=None, isbuy=True)
    用来计算具体的下单量。该方法通常由Sizer内部调用,但也可以被策略直接调用来手动计算下单量。参数data指定要操作的数据源(通常是股票代码或其他资产),isbuy则表示这是一次买入操作还是卖出操作(默认为买入)。返回值是根据Sizer规则计算出的应下单的数量。
文章作者: Kaka Wan Yifan
文章链接: https://kakawanyifan.com/20301
版权声明: 本博客所有文章版权为文章作者所有,未经书面许可,任何机构和个人不得以任何形式转载、摘编或复制。

留言板