量化投资与机器学习公众号 独家撰写
公众号为全网读者带来Backtrader系列自推出第一期以来,受到了众多读者的喜爱与点赞,QIML也会继续把这个系列做好。
让那些割韭菜的课程都随风而去吧!!!
公众号将为大家多维度、多策略、多场景来讲述Backtrader在量化投资领域的实践应用。同时,我们对每段代码都做了解读说明,愿你在Quant的道路上学有所获!

预定系列
此系列将由浅入深,每期1~2周,大家敬请期待!

前言
无论是实盘,还是模拟或回测,都会涉及到 “交易”,区区 “交易” 2 字,背后却关联着许许多多的概念和复杂的运行逻辑,在 Backtrader 中,交易流程大致如下:
  • step1:设置交易条件:初始资金、交易税费、滑点、成交量限制等;
  • step2:在 Strategy 策略逻辑中下达交易指令 buy、sell、close,或取消交易 cancel;
  • step3:Order 模块会解读交易订单,解读的信息将交由经纪商 Broker 模块处理;
  • step4:经纪商 Broker 会根据订单信息检查订单并确定是否接收订单;
  • step5:经纪商 Broker 接收订单后,会按订单要求撮合成交 trade,并进行成交结算;
  • step6:Order 模块返回经纪商 Broker 中的订单执行结果。 
Broker 经纪商模块和 Order 订单模块是交易相关的核心模块,特别是 Broker 模块,小到交易条件的设置,大到交易订单的执行,交易的方方面面都与 Broker 有关。《Backtrader 交易篇》主要会从 “交易条件”、“交易函数”、“交易订单”、“交易执行”、“交易结算”  5 个方面来讲述 Backtrader 中交易相关的操作 ,分上下 2 篇,今天的《上篇》主要介绍各种交易条件的设置和管理。
Broker 中的交易条件
回测过程中涉及的交易条件设置,最常见的有初始资金、交易税费、滑点、期货保证金比率等,有时还会对成交量做限制、对涨跌幅做限制、对订单生成和执行时机做限制,上述大部分交易条件都可以通过 Broker 来管理,主要有 2 中操作方式:
  • 方式1:通过设置 backtrader.brokers.BackBroker() 类中的参数,生成新的 broker 实例,再将新的实例赋值给 cerebro.broker ;
  • 方式2:通过调用 broker 中的 ”set_xxx“ 方法来修改条件,还可通过 ”get_xxx“ 方法查看当前设置的条件取值。
资金管理
Broker 默认的初始资金 cash 是 10000,可通过 “cash” 参数、set_cash() 方法修改初始资金,此外还提供了add_cash() 方法增加或减少资金。Broker 会检查提交的订单现金需求与当前现金是否匹配,cash 也会随着每次交易进行迭代更新用以匹配当前头寸。
# 初始化时
cerebro.broker.set_cash(
100000000.0
)
# 设置初始资金
cerebro.broker.get_cash()
# 获取当前可用资金

# 简写形式
cerebro.broker.setcash(
100000000.0
)
# 设置初始资金
cerebro.broker.getcash()
# 获取当前可用资金

# 在 Strategy 中添加资金或获取当前资金
self.broker.add_cash(
10000
)
# 正数表示增加资金
self.broker.add_cash(
-10000
)
# 负数表示减少资金
self.broker.getcash()
# 获取当前可用资金
持仓查询
Broker 在每次交易后更新 cash 外,还会同时更新当前总资产 value 和当前持仓 position,通常在 Strategy 中进行持仓查询操作:
classTestStrategy(bt.Strategy):
defnext(self):
        print(
'当前可用资金'
, self.broker.getcash())

        print(
'当前总资产'
, self.broker.getvalue())

        print(
'当前持仓量'
, self.broker.getposition(self.data).size)

        print(
'当前持仓成本'
, self.broker.getposition(self.data).price)

# 也可以直接获取持仓
        print(
'当前持仓量'
, self.getposition(self.data).size)

        print(
'当前持仓成本'
, self.getposition(self.data).price)

# 注:getposition() 需要指定具体的标的数据集

# 部分返回结果
2019-01-16
, Close,
32.6331
2019-01-16
, 当前可用资金,
1000000.00
2019-01-16
, 当前总资产,
1000000.00
2019-01-16
, 当前持仓数量,
0.00
2019-01-16
, 当前持仓成本,
0.00
2019-01-17
, BUY EXECUTED, ref:
63
,Price:
32.64
, Cost:
3263.63
, Comm
0.98
, Size:
100.00
, Stock:
600466.
SH

2019-01-17
, Close,
32.0779
2019-01-17
, 当前可用资金,
996735.39
2019-01-17
, 当前总资产,
999943.18
2019-01-17
, 当前持仓数量,
100.00
2019-01-17
, 当前持仓成本,
32.64
......
  • 当前总资产 = 当前可用资金 + 当前持仓总市值,而当前持仓总市值为当前持仓中所有标的各自持仓市值之和,如果只有一个标的(如上例),就有 当前总资产 999943.18 = 当前可用资金 996735.39 + 当前持仓数量 100.00 × 当前 close 32.0779;
  • 在计算当前可用资金时,除了考虑扣除购买标的时的费用外,还需要考虑扣除交易费用 。
滑点管理
在实际交易中,由于市场波动、网络延迟等原因,交易指令中指定的交易价格与实际成交价格会存在较大差别,出现滑点。为了让回测结果更真实,在交易前可以通过 brokers 设置滑点,滑点的类型有 2 种:百分比滑点和固定滑点。不论哪种设置方式,都是起到相同的作用:买入时,在指定价格的基础上提高实际买入价格;卖出时,在指定价格的基础上,降低实际卖出价格;买的 “更贵”,卖的 “更便宜” 。
注:在 Backtrader 中,如果同时设置了百分比滑点和固定滑点,前者的优先级高于后者,最终按百分比滑点的设置处理。
百分比滑点
假设设置了 n% 的滑点,如果指定的买入价为 x,那实际成交时的买入价会提高至 x * (1+ n%) ;同理,若指定的卖出价为 x,那实际成交时的卖出价会降低至 x * (1- n%),下面时将滑点设置为 0.01% 的例子:
# 方式1:通过 BackBroker 类中的 slip_perc 参数设置百分比滑点
cerebro.broker = bt.brokers.BackBroker(slip_perc=
0.0001
)

# 方式2:通过调用 brokers 的 set_slippage_perc 方法设置百分比滑点
cerebro.broker.set_slippage_perc(perc=
0.0001
)


# 部分输出结果

......

2019-01-17
, BUY EXECUTED, ref:
185
,Price:
32.6363
, Cost:
3263.6337
, Comm
0.9791
, Size:
100.00
, Stock:
600466.
SH

2019-01-17
, Close,
32.0779
2019-01-17
, Open,
32.6331
2019-01-17
, 当前可用资金,
996735.39
2019-01-17
, 当前总资产,
999943.18
2019-01-17
, 当前持仓数量,
100.00
2019-01-17
, 当前持仓成本,
32.6363
......

2019-01-29
, SELL EXECUTED, ref:
186
, Price:
33.9251
, Cost:
3263.6337
, Comm
1.0178
, Size:
-100.00
, Stock:
600466.
SH

2019-01-29
, Close,
34.7922
2019-01-29
, Open,
33.9285
2019-01-29
, 当前可用资金,
1000126.88
2019-01-29
, 当前总资产,
1000126.88
2019-01-29
, 当前持仓数量,
0.00
2019-01-29
, 当前持仓成本,
0.00
......
  • 2019-01-17 以开盘价买入标的 600466.SH,当天的开盘价为 32.6331 ,实际成交价 Price 为 32.6331 * (1+0.0001) = 32.63636331;
  • 2019-01-29 以开盘价卖出标的 600466.SH,当天的开盘价为 33.9285 ,实际成交价 Price 为  33.9285 * (1-0.0001) = 33.92510715。
固定滑点
假设设置了大小为 n 的固定滑点,如果指定的买入价为 x,那实际成交时的买入价会提高至 x + n ;同理,若指定的卖出价为 x,那实际成交时的卖出价会降低至 x - n,下面时将滑点固定为 0.001 的例子:
# 方式1:通过 BackBroker 类中的 slip_fixed 参数设置固定滑点
cerebro.broker = bt.brokers.BackBroker(slip_fixed=
0.001
)

# 方式2:通过调用 brokers 的 set_slippage_fixed 方法设置固定滑点
cerebro.broker.set_slippage_fixed(fixed=
0.001
)


# 部分输出结果

......

2019-01-17
, BUY EXECUTED, ref:
368
,Price:
32.6321
, Cost:
3263.2074
, Comm
0.9790
, Size:
100.00
, Stock:
600466.
SH

2019-01-17
, Close,
32.0779
2019-01-17
, Open,
32.6331
2019-01-17
, 当前可用资金,
996735.81
2019-01-17
, 当前总资产,
999943.60
2019-01-17
, 当前持仓数量,
100.00
2019-01-17
, 当前持仓成本,
32.6321
......

2019-01-29
, SELL EXECUTED, ref:
369
, Price:
33.9295
, Cost:
3263.2074
, Comm
1.0179
, Size:
-100.00
, Stock:
600466.
SH

2019-01-29
, Close,
34.7922
2019-01-29
, Open,
33.9285
2019-01-29
, 当前可用资金,
1000127.75
2019-01-29
, 当前总资产,
1000127.75
2019-01-29
, 当前持仓数量,
0.00
2019-01-29
, 当前持仓成本,
0.0000
  • 2019-01-17 以开盘价买入标的 600466.SH,当天的开盘价为 32.6331 ,实际成交价 Price 为 32.6331 -0.001 = 32.6321;
  • 2019-01-29 以开盘价卖出标的 600466.SH,当天的开盘价为 33.9285 ,实际成交价 Price 为 33.9285 + 0.001  = 33.9295 。
有关滑点的其他设置
除了用于设置滑点的 slip_perc 和 slip_fixed 参数外,broker 还提供了其他参数用于处理价格出现滑点后的极端情况:
  • slip_open:是否对开盘价做滑点处理,该参数在 BackBroker() 类中默认为 False,在 set_slippage_perc 和set_slippage_fixed 方法中默认为 True;
  • slip_match:是否将滑点处理后的新成交价与成交当天的价格区间 low ~ high 做匹配,如果为 True,则根据新成交价重新匹配调整价格区间,确保订单能被执行;如果为 False,则不会与价格区间做匹配,订单不会执行,但会在下一日执行一个空订单;默认取值为 True;
  • slip_out:如果新成交价高于最高价或低于最高价,是否以超出的价格成交,如果为 True,则允许以超出的价格成交;如果为 Fasle,实际成交价将被限制在价格区间内  low ~ high;默认取值为 False;
  • slip_limit:是否对限价单执行滑点,如果为 True,即使 slip_match 为Fasle,也会对价格做匹配,确保订单被执行;如果为 Fasle,则不做价格匹配;默认取值为 True。
下面是将滑点设置为固定 0.35 ,对上述参数去不同的值,标的 600466.SH 在 2019-01-17 的成交情况做对比:
# 情况1:
set_slippage_fixed(fixed=
0.35
,

                   slip_open=
False
,

                   slip_match=
True
,

                   slip_out=
False
)

# 由于 slip_open=False ,不会对开盘价做滑点处理,所以仍然以原始开盘价 32.63307367 成交
......

date
2019-01-16
 open
33.00320305
 low
32.57138544
 high
33.00320305
2019-01-17
, BUY EXECUTED, ref:
249
,Price:
32.6331
, Cost:
3263.3074
, Comm
0.9790
, Size:
100.00
, Stock:
600466.
SH

2019-01-17
, 当前持仓,
100.00
2019-01-17
, 当前持仓,
32.63
date
2019-01-17
 open
32.63307367
 low
31.83112668
 high
32.94151482
......


# 情况2:
set_slippage_fixed(fixed=
0.35
,

                   slip_open=
True
,

                   slip_match=
True
,

                   slip_out=
False
)

# 滑点调整的新成交价为 32.63307367+0.35 = 32.98307367,超出了当天最高价 32.94151482
# 由于允许做价格匹配 slip_match=True, 但不以超出价格区间的价格执行 slip_out=False
# 最终以最高价 32.9415 成交
......

date
2019-01-16
 open
33.00320305
 low
32.57138544
 high
33.00320305
2019-01-17
, BUY EXECUTED, ref:
493
,Price:
32.9415
, Cost:
3294.1515
, Comm
0.9882
, Size:
100.00
, Stock:
600466.
SH

2019-01-17
, 当前持仓,
100.00
2019-01-17
, 当前持仓,
32.94
date
2019-01-17
 open
32.63307367
 low
31.83112668
 high
32.94151482
.....


# 情况3:
set_slippage_fixed(fixed=
0.35
,

                   slip_open=
True
,

                   slip_match=
True
,

                   slip_out=
True
)

# 滑点调整的新成交价为 32.63307367+0.35 = 32.98307367,超出了当天最高价 32.94151482
# 允许做价格匹配 slip_match=True, 而且运行以超出价格区间的新成交价执行 slip_out=True
# 最终以新成交价 32.98307367 成交
......

2019-01-17
, BUY EXECUTED, ref:
640
,Price:
32.9831
, Cost:
3298.3074
, Comm
0.9895
, Size:
100.00
, Stock:
600466.
SH

2019-01-17
, 当前持仓,
100.00
2019-01-17
, 当前持仓,
32.98
date
2019-01-17
 open
32.63307367
 low
31.83112668
 high
32.94151482
......


# 情况4:
set_slippage_fixed(fixed=
0.35
,

                   slip_open=
True
,

                   slip_match=
False
,

                   slip_out=
True
)

# 滑点调整的新成交价为 32.63307367+0.35 = 32.98307367,超出了当天最高价 32.94151482
# 由于不进行价格匹配 slip_match=False,新成交价超出价格区间无法成交
# 2019-01-17 这一天订单不会执行,但会在下一日 2019-01-18 执行一个空订单
# 再往后的 2019-07-02,也未执行订单,下一日 2019-07-03 执行空订单
# 即使 2019-07-03的 open 39.96627412+0.35 < high 42.0866713 满足成交条件,也不会补充成交
......

date
2019-01-17
 open
32.63307367
 low
31.83112668
 high
32.94151482
2019-01-18
, BUY EXECUTED, ref:
597
,Price:
0.0000
, Cost:
0.0000
, Comm
0.0000
, Size:
0.00
, Stock:
600466.
SH

2019-01-18
, 当前持仓,
0.00
2019-01-18
, 当前持仓,
0.00
date
2019-01-18
 open
31.95450314
 low
31.95450314
 high
32.81813836
......

date
2019-07-01
 open
40.4803098
 low
39.90201966
 high
41.18710886
crossover ture

2019-07-02
, 当前持仓,
0.00
2019-07-02
, 当前持仓,
0.00
date
2019-07-02
 open
40.4803098
 low
39.70925628
 high
40.54456426
2019-07-03
, BUY EXECUTED, ref:
900
,Price:
0.0000
, Cost:
0.0000
, Comm
0.0000
, Size:
0.00
, Stock:
600466.
SH

2019-07-03
, 当前持仓,
0.00
2019-07-03
, 当前持仓,
0.00
date
2019-07-03
 open
39.96627412
 low
39.90201966
 high
42.0866713
上述参数在 BackBroker() 中也是以参数形式存在:
bt.brokers.BackBroker(..., slip_perc=0, slip_fixed=0,  slip_open=False, slip_match=True, slip_out=False, slip_limit=True, ...)
交易税费管理
交易时是否考虑交易费用对回测的结果影响很大,所以在回测是通常会设置交易税费,不同标的的费用收取规则也各不相同:
  • 股票:目前 A 股的交易费用分为 2 部分:佣金和印花税,其中佣金双边征收,不同证券公司收取的佣金各不相同,一般在 0.02%-0.03% 左右,单笔佣金不少于 5 元;印花税只在卖出时收取,税率为 0.1%。
  • 期货:期货交易费用包括交易所收取手续费和期货公司收取佣金 2 部分,交易所手续费较为固定,不同期货公司佣金不一致,而且不同期货品种的收取方式不相同,有的按照固定费用收取,有的按成交金额的固定百分比收取:合约现价*合约乘数*手续费费率。除了交易费用外,期货交易时还需上交一定比例的保证金 。
Backtrader 也提供了多种交易费设置方式,既可以简单的通过参数进行设置,也可以结合交易条件自定义费用函数:
  • 根据交易品种的不同,Backtrader 将交易费用分为 股票 Stock-like 模式和期货 Futures-like 种模式;
  • 根据计算方式的不同,Backtrader 将交易费用分为 PERC 百分比费用模式 和 FIXED 固定费用模式 ;
 Stock-like 模式与 PERC 百分比费用模式对应,期货 Futures-like 与 FIXED 固定费用模式对应;
  • 在设置交易费用时,最常涉及如下 3 个参数:
    • commission:手续费 / 佣金;
    • mult:乘数;
    • margin:保证金 / 保证金比率 。
    • 双边征收:买入和卖出操作都要收取相同的交易费用 。
通过 BackBroker() 设置
BackBroker 中有一个 commission 参数,用来全局设置交易手续费。如果是股票交易,可以简单的通过该方式设置交易佣金,但该方式无法满足期货交易费用的各项设置。
# 设置 0.0002 = 0.02% 的手续费
cerebro.broker = bt.brokers.BackBroker(commission= 0.0002)
通过 setcommission() 设置
如果想要完整又方便的设置交易费用,可以调用 broker 的 setcommission() 方法,该方法基本上可以满足大部分的交易费用设置需求,下面是对该方法中各个参数的解释说明:
cerebro.broker.setcommission(

# 交易手续费,根据margin取值情况区分是百分比手续费还是固定手续费
    commission=
0.0
,

# 期货保证金,决定着交易费用的类型,只有在stocklike=False时起作用
    margin=
None
,

# 乘数,盈亏会按该乘数进行放大
    mult=
1.0
,

# 交易费用计算方式,取值有:
# 1.CommInfoBase.COMM_PERC 百分比费用
# 2.CommInfoBase.COMM_FIXED 固定费用
# 3.None 根据 margin 取值来确定类型
    commtype=
None
,

# 当交易费用处于百分比模式下时,commission 是否为 % 形式
# True,表示不以 % 为单位,0.XX 形式;False,表示以 % 为单位,XX% 形式
    percabs=
True
,

# 是否为股票模式,该模式通常由margin和commtype参数决定
# margin=None或COMM_PERC模式时,就会stocklike=True,对应股票手续费;
# margin设置了取值或COMM_FIXED模式时,就会stocklike=False,对应期货手续费
    stocklike=
False
,

# 计算持有的空头头寸的年化利息
# days * price * abs(size) * (interest / 365)
    interest=
0.0
,

# 计算持有的多头头寸的年化利息
    interest_long=
False
,

# 杠杆比率,交易时按该杠杆调整所需现金
    leverage=
1.0
,

# 自动计算保证金
# 如果False,则通过margin参数确定保证金
# 如果automargin<0,通过mult*price确定保证金
# 如果automargin>0,如果automargin*price确定保证金
    automargin=
False
,

# 交易费用设置作用的数据集(也就是作用的标的)
# 如果取值为None,则默认作用于所有数据集(也就是作用于所有assets)
    name=
None
)
从上述各参数的含义和作用可知,margin 、commtype、stocklike 存在 2 种默认的配置规则:股票百分比费用、期货固定费用,具体如下:
  • 第 1 条规则:未设置 margin(即 margin 为 0 / None / False)→ commtype 会指向 COMM_PERC 百分比费用 → 底层的 _stocklike 属性会设置为 True → 对应的是“股票百分比费用”。所以如果想为股票设置交易费用,就令 margin = 0 / None / False,或者令 stocklike=True;
  • 第 2 条规则:为 margin 设置了取值 →   commtype 会指向 COMM_FIXED 固定费用 → 底层的 _stocklike 属性会设置为 False → 对应的是“期货固定费用”,因为只有期货才会涉及保证金。所以如果想为期货设置交易费用,就需要设置 margin,此外还需令 stocklike=True,margin 参数才会起作用 。
通过 addcommissioninfo() 设置
如果想要更灵活的设置交易费用,可以在继承 CommInfoBase 基础类的基础上自定义交易费用子类 ,然后通过 addcommissioninfo() 方法将实例添加进 broker。
# 在继承 CommInfoBase 基础类的基础上自定义交易费用
classMyCommission(bt.CommInfoBase):
# 对应 setcommission 中介绍的那些参数,也可以增添新的全局参数
    params = ((xxx, xxx),)

# 自定义交易费用计算方式
def_getcommission(self, size, price, pseudoexec):
pass
# 自定义佣金计算方式
defget_margin(self,price):
pass
    ...


# 实例化
mycomm = MyCommission(...)

cerebro = bt.Cerebro()

# 添加进 broker
cerebro.broker.addcommissioninfo(mycomm, name=
'xxx'
)
# name 用于指定该交易费用函数适用的标的
Backtrader 中与交易费用相关的设置都是由 CommInfoBase 类管理的,上一节介绍的 setcommission() 方法中的参数就是 CommInfoBase 类中 params 属性里包含的参数,此外还内置许多 getxxx 方法,用于计算并返回交易产生的指标,比如计算成交量 getsize(price, cash) 、计算持仓市值 getvalue(position, price)、计算佣金getcommission(size, price) 或 _getcommission(self, size, price, pseudoexec)、计算保证金 get_margin(price) ......,其中自定义时最常涉及的就是上面案例中显示的 _getcommission 和 get_margin。
自定义交易费用的例子
Backtrader 中默认配置了 2 种费用:“股票百分比费用”和“期货固定费用”,官方文档还给大家提供了“期货百分比费用”的自定义子类:
# 自定义期货百分比费用
classCommInfo_Fut_Perc_Mult(bt.CommInfoBase):
    params = (

      (
'stocklike'
,
False
),
# 指定为期货模式
      (
'commtype'
, bt.CommInfoBase.COMM_PERC),
# 使用百分比费用
      (
'percabs'
,
False
),
# commission 以 % 为单位
    )


def_getcommission(self, size, price, pseudoexec):
# 计算交易费用
return
 (abs(size) * price) * (self.p.commission/
100
) * self.p.mult

# pseudoexec 用于提示当前是否在真实统计交易费用
# 如果只是试算费用,pseudoexec=False
# 如果是真实的统计费用,pseudoexec=True

comminfo = CommInfo_Fut_Perc_Mult(

    commission=
0.1
,
# 0.1%
    mult=
10
,

    margin=
2000
)
# 实例化
cerebro.broker.addcommissioninfo(comminfo)


# 上述自定义函数,也可以通过 setcommission 来实现
cerebro.broker.setcommission(commission=
0.1
,
#0.1%
                             mult=
10
,

                             margin=
2000
,

                             percabs=
False
,

                             commtype=bt.CommInfoBase.COMM_PERC,

                             stocklike=
False
)
下面是考虑佣金和印花税的股票百分比费用:
classStockCommission(bt.CommInfoBase):
    params = (

      (
'stocklike'
,
True
),
# 指定为期货模式
      (
'commtype'
, bt.CommInfoBase.COMM_PERC),
# 使用百分比费用模式
      (
'percabs'
,
True
),
# commission 不以 % 为单位
      (
'stamp_duty'
,
0.001
),)
# 印花税默认为 0.1%

# 自定义费用计算公式
def_getcommission(self, size, price, pseudoexec):
if
 size >
0
:
# 买入时,只考虑佣金
return
 abs(size) * price * self.p.commission

elif
 size <
0
:
# 卖出时,同时考虑佣金和印花税
return
 abs(size) * price * (self.p.commission + self.p.stamp_duty)

else
:

return0
成交量限制管理
默认情况下,Broker 在撮合成交订单时,不会将订单上的购买数量与成交当天 bar 的总成交量 volume 进行对比,即使购买数量超出了当天该标的的总成交量,也会按购买数量全部撮合成交,显然这种“无限的流动性”是不现实的,这种 “不考虑成交量,默认全部成交” 的交易模式,也会使得回测结果与真实结果产生较大偏差。如果想要修改这种默认模式,可以通过 Backtrader 中的 fillers 模块来限制实际成交量,fillers 会告诉 Broker 在各个成交时间点应该成交多少量,一共有 3 种形式:
形式1:bt.broker.fillers.FixedSize(size) 
通过 FixedSize() 方法设置最大的固定成交量:size,该种模式下的成交量限制规则如下:
  • 订单实际成交量的确定规则:取(size、订单执行那天的 volume 、订单中要求的成交数量)中的最小者;
  • 订单执行那天,如果订单中要求的成交数量无法全部满足,则只成交部分数量。
设置方式如下:
# 通过 BackBroker() 类直接设置
cerebro = Cerebro()

filler = bt.broker.fillers.FixedSize(size=xxx)

newbroker = bt.broker.BrokerBack(filler=filler)

cerebro.broker = newbroker


# 通过 set_filler 方法设置
cerebro = Cerebro()

cerebro.broker.set_filler(bt.broker.fillers.FixedSize(size=xxx))
下面是部分输出案例:
  • 2019-01-17 这天执行买入订单,当天 volume 869.0 < buy(size=2000) < FixedSize(size=3000),所以当天只买入了最小的 volume 869 股,剩余未成交数量 Remsize: 1131.00 ;2019-02-22 这天情况类似;
  • 2019-03-15 这天执行买入订单,当天 buy(size=2000) < FixedSize(size=3000)< volume 3063.0,所以可以全部成交,剩余未成交数量 Remsize: 0;
  • 2019-05-20 这天执行卖出订单,当天 close 平仓时的仓位 2000.0 > volume 1686.0,无法全部平仓,所以只卖出了 1686 股,剩余未成交数量 Remsize: -314.00;随后,在 2019-06-12 再次触发卖出信号,2019-06-13 执行卖出,对剩余仓位 341 股 进行了平仓。
......

self.order = self.buy(size=
2000
)
# 每次买入 2000 股
......

cerebro.broker.set_filler(bt.broker.fillers.FixedSize(size=
3000
))
# 固定最大成交量


# 下面是部分输出结果
......

date
2019-01-16
 open
33.00320305
 volume
660.0
 当前持仓量
0
 当前持仓成本
0.0
2019-01-17
, BUY EXECUTED, ref:
2456
,Price:
32.6331
, Size:
869.00
, Remsize:
1131.00
, Cost:
28358.1410
, Stock:
600466.
SH

date
2019-01-17
 open
32.63307367
 volume
869.0
 当前持仓量
869.0
 当前持仓成本
32.63307367
date
2019-01-18
 open
31.95450314
 volume
890.0
 当前持仓量
869.0
 当前持仓成本
32.63307367
......

date
2019-02-21
 open
35.47073225
 volume
2012.0
 当前持仓量
0.0
 当前持仓成本
0.0
2019-02-22
, BUY EXECUTED, ref:
2458
,Price:
34.9155
, Size:
1627.00
, Remsize:
373.00
, Cost:
56807.5806
, Stock:
600466.
SH

date
2019-02-22
 open
34.91553818
 volume
1627.0
 当前持仓量
1627.0
 当前持仓成本
34.91553818
date
2019-02-25
 open
35.28566756
 volume
4040.0
 当前持仓量
1627.0
 当前持仓成本
34.91553818
......

date
2019-03-14
 open
41.82461994
 volume
3063.0
 当前持仓量
0.0
 当前持仓成本
0.0
2019-03-15
, BUY EXECUTED, ref:
2460
,Price:
41.2077
, Size:
2000.00
, Remsize:
0.00
, Cost:
82415.4753
, Stock:
600466.
SH

date
2019-03-15
 open
41.20773764
 volume
4078.0
 当前持仓量
2000.0
 当前持仓成本
41.20773764
......

ate
2019-05-17
 open
40.93009102
 volume
2470.0
 当前持仓量
2000.0
 当前持仓成本
46.63630188
2019-05-20
, SELL EXECUTED, ref:
2465
, Price:
39.7735
, Size:
-1686.00
, Remsize:
-314.00
, Cost:
78628.8050
, Stock:
600466.
SH

date
2019-05-20
 open
39.77351074
 volume
1686.0
 当前持仓量
314.0
 当前持仓成本
46.63630188
date
2019-05-21
 open
39.25947506
 volume
1921.0
 当前持仓量
314.0
 当前持仓成本
46.63630188
......

date
2019-06-11
 open
38.48842154
 volume
4038.0
 当前持仓量
314.0
 当前持仓成本
46.63630188
date
2019-06-12
 open
39.90201966
 volume
1785.0
 当前持仓量
314.0
 当前持仓成本
46.63630188
2019-06-13
, SELL EXECUTED, ref:
2466
, Price:
40.0948
, Size:
-314.00
, Remsize:
0.00
, Cost:
14643.7988
, Stock:
600466.
SH

date
2019-06-13
 open
40.09478304
 volume
1605.0
 当前持仓量
0.0
 当前持仓成本
0.0
......
从执行结果跟踪记录来看,存在 2 个现象:
  • 对订单执行当天未成交的剩余数量,并不会在第二天接着成交;
  • 在订单执行当天,如果遇到对于无法全部成交的情况,订单会被部分执行,然后在第二天取消该订单,并打印 notify_order :以上面的案例为例,2019-01-16 这一天触发买入信号,下达订单指令,创建订单 → 2019-01-17 订单被传递给 broker,并由 broker 接受,然后由于成交量限制,订单被部分执行 → 2019-01-18 这天,剩余订单会被取消,同时打印 notify_order 。
形式2:bt.broker.fillers.FixedBarPerc(perc)
通过 FixedBarPerc(perc) 将 订单执行当天 bar 的总成交量 volume 的 perc % 设置为最大的固定成交量,该模式的成交量限制规则如下:
  • 订单实际成交量的确定规则:取 (volume * perc /100、订单中要求的成交数量) 的 最小者;
  • 订单执行那天,如果订单中要求的成交数量无法全部满足,则只成交部分数量。
设置方式如下:
# 通过 BackBroker() 类直接设置
cerebro = Cerebro()

filler = bt.broker.fillers.FixedBarPerc(perc=xxx)

newbroker = bt.broker.BrokerBack(filler=filler)

cerebro.broker = newbroker


# 通过 set_filler 方法设置
cerebro = Cerebro()

cerebro.broker.set_filler(bt.broker.fillers.FixedBarPerc(perc=xxx))

# perc 以 % 为单位,取值范围为[0.0,100.0]
下面是部分输出案例:
  • 2019-01-17 这天执行买入订单,订单的buy(size=2000) > 当天 volume 869.0,只能部分成交,数量为 volume 869.0 * (50/100)= 434 ,订单剩余数量 Remsize: 1566.00 不会成交;2019-02-22 这天情况类似;
  • 2019-03-15 这天执行买入订单,当天 buy(size=2000) < volume 3063.0,所以可以全部成交,剩余未成交数量 Remsize: 0;
  • 2019-04-03 这天执行卖出订单,当天要 close 平仓的量仓位为 2000.0 ,虽然小于当天的 volume 3826.0,但是大于 volume 3826.0 *(50/100)= 1913.00,所以最多只成交了 1913.00,还剩 Remsize: -87.00 未成交;随后,在 2019-05-17 再次触发卖出信号,2019-05-20 剩余仓位 87 进行了平仓。
......

self.order = self.buy(size=
2000
)
# 以下一日开盘价买入2000股
......

cerebro.broker.set_filler(bt.broker.fillers.FixedBarPerc(perc=
50
))

# perc=50 表示 50%
......



# 返回的部分结果:
date
2019-01-16
 open
33.00320305
 volume
660.0
 当前持仓量
0
 当前持仓成本
0.0
2019-01-17
, BUY EXECUTED, ref:
2664
,Price:
32.6331
, Size:
434.00
, Remsize:
1566.00
, Cost:
14162.7540
, Stock:
600466.
SH

date
2019-01-17
 open
32.63307367
 volume
869.0
 当前持仓量
434.0
 当前持仓成本
32.63307367
date
2019-01-18
 open
31.95450314
 volume
890.0
 当前持仓量
434.0
 当前持仓成本
32.63307367
......

date
2019-02-21
 open
35.47073225
 volume
2012.0
 当前持仓量
0.0
 当前持仓成本
0.0
2019-02-22
, BUY EXECUTED, ref:
2666
,Price:
34.9155
, Size:
813.00
, Remsize:
1187.00
, Cost:
28386.3325
, Stock:
600466.
SH

date
2019-02-22
 open
34.91553818
 volume
1627.0
 当前持仓量
813.0
 当前持仓成本
34.91553818
......

date
2019-03-14
 open
41.82461994
 volume
3063.0
 当前持仓量
0.0
 当前持仓成本
0.0
2019-03-15
, BUY EXECUTED, ref:
2668
,Price:
41.2077
, Size:
2000.00
, Remsize:
0.00
, Cost:
82415.4753
, Stock:
600466.
SH

date
2019-03-15
 open
41.20773764
 volume
4078.0
 当前持仓量
2000.0
 当前持仓成本
41.20773764
......

date
2019-04-02
 open
48.1168194
 volume
3281.0
 当前持仓量
2000.0
 当前持仓成本
46.14279604
2019-04-03
, SELL EXECUTED, ref:
2671
, Price:
47.0681
, Size:
-1913.00
, Remsize:
-87.00
, Cost:
88271.1688
, Stock:
600466.
SH

date
2019-04-03
 open
47.06811949
 volume
3826.0
 当前持仓量
87.0
 当前持仓成本
46.14279604
date
2019-04-04
 open
48.17850763
 volume
3770.0
 当前持仓量
87.0
 当前持仓成本
46.14279604
......

date
2019-05-16
 open
40.35180088
 volume
1424.0
 当前持仓量
87.0
 当前持仓成本
46.14279604
date
2019-05-17
 open
40.93009102
 volume
2470.0
 当前持仓量
87.0
 当前持仓成本
46.14279604
2019-05-20
, SELL EXECUTED, ref:
2672
, Price:
39.7735
, Size:
-87.00
, Remsize:
0.00
, Cost:
4014.4233
, Stock:
600466.
SH

date
2019-05-20
 open
39.77351074
 volume
1686.0
 当前持仓量
0.0
 当前持仓成本
0.0
......
形式3:bt.broker.fillers.BarPointPerc(minmov=0.01,perc=100.0)
BarPointPerc() 在考虑了价格区间的基础上确定成交量,在订单执行当天,成交量确定规则为:
  • 通过 minmov 将 当天 bar 的价格区间 low ~ high 进行均匀划分,得到划分的份数:
  • part =  (high -low +minmov)  // minmov  (向下取整)
  • 再对当天 bar 的总成交量 volume 也划分成相同的份数 part ,这样就能得到每份的平均成交量:
  • volume_per = volume // part 
  • 最终,volume_per * (perc / 100)就是允许的最大成交量,实际成交时,对比订单中要求的成交量,就可以得到最终实际成交量
  • 实际成交量 = min ( volume_per * (perc / 100), 订单中要求的成交数量 )
设置方式如下:
# 通过 BackBroker() 类直接设置
cerebro = Cerebro()

filler = bt.broker.fillers.BarPointPerc(minmov=
0.01
,perc=
100.0
)

newbroker = bt.broker.BrokerBack(filler=filler)

cerebro.broker = newbroker


# 通过 set_filler 方法设置
cerebro = Cerebro()

cerebro.broker.set_filler(bt.broker.fillers.BarPointPerc(minmov=
0.01
,perc=
100.0
))

# perc 以 % 为单位,取值范围为[0.0,100.0]
可以通过下面部分输出案例做验证:

......

self.order = self.buy(size=
2000
)
# 以下一日开盘价买入2000股
......

cerebro.broker.set_filler(bt.broker.fillers.BarPointPerc(minmov=
0.1
, perc=
50
))
# 表示 50%
......


# 部分输出结果
date
2019-01-16
 open
33.00320305
 high
33.00320305
 low
32.57138544
 volume
660.0
 当前持仓量
0
 当前持仓成本
0.0
2019-01-17
, BUY EXECUTED, ref:
2560
,Price:
32.6331
, Size:
36.00
, Remsize:
964.00
, Cost:
1174.7907
, Stock:
600466.
SH

date
2019-01-17
 open
32.63307367
 high
32.94151482
 low
31.83112668
 volume
869.0
 当前持仓量
36.0
 当前持仓成本
32.63307367
date
2019-01-18
 open
31.95450314
 high
32.81813836
 low
31.95450314
 volume
890.0
 当前持仓量
36.0
 当前持仓成本
32.63307367
......


# 结果验证:
# part = (high 32.94151482 - low 31.83112668 + minmov 0.1) // minmov 0.1 = 12.0
# volume_per = volume 869.0 // 12.0 = 72.0
# 最终成交数量 = min ( volume_per 72.0 * (perc 50 / 100), 订单中要求的成交数量 2000 ) = 36.0
交易时机管理

对于交易订单生成和执行时间,Backtrader 默认是 “当日收盘后下单,次日以开盘价成交”,这种模式在回测过程中能有效避免使用未来数据。但对于一些特殊的交易场景,比如“all_in”情况下,当日所下订单中的数量是用当日收盘价计算的(总资金 / 当日收盘价),次日以开盘价执行订单时,如果开盘价比昨天的收盘价提高了,就会出现可用资金不足的情况。为了应对一些特殊交易场景,Backtrader 还提供了一些 cheating 式的交易时机模式:Cheat-On-Open 和 Cheat-On-Close。
# 下面是正常模式下的订单执行情况
# Send 对应订单指令下单时间,也就是订单发送时间
# Executed 对应订单执行时间
2019-01-16
 Send Buy, open
33.00320305
2019-01-17
 Buy Executed at price
32.63307367
2019-01-28
 Send Sell, open
33.311644199999996
2019-01-29
 Sell Executed at price
33.928526500000004
2019-02-21
 Send Buy, open
35.47073225
2019-02-22
 Buy Executed at price
34.91553818
2019-02-26
 Send Sell, open
37.07462623
2019-02-27
 Sell Executed at price
37.50644384
2019-03-14
 Send Buy, open
41.82461994
2019-03-15
 Buy Executed at price
41.20773764
2019-03-15
 Send Sell, open
41.20773764
2019-03-18
 Sell Executed at price
44.10708445
2019-03-29
 Send Buy, open
43.55189038
2019-04-01
 Buy Executed at price
46.14279604
2019-04-02
 Send Sell, open
48.1168194
2019-04-03
 Sell Executed at price
47.06811949
......
Cheat-On-Open
Cheat-On-Open 是“当日下单,当日以开盘价成交”模式,在该模式下,Strategy 中的交易逻辑不再写在 next() 方法里,而是写在特定的 next_open()、nextstart_open() 、prenext_open() 函数中,具体设置可参考如下案例:
  • 方式1:bt.Cerebro(cheat_on_open=True);
  • 方式2:cerebro.broker.set_coo(True);
  • 方式3:BackBroker(coo=True)。
classTestStrategy(bt.Strategy):
    ......

defnext_open(self):
# 取消之前未执行的订单
if
 self.order:

            self.cancel(self.order)

# 检查是否有持仓
ifnot
 self.position:

# 10日均线上穿5日均线,买入
if
 self.crossover >
0
:

                print(
'{} Send Buy, open {}'
.format(self.data.datetime.date(),self.data.open[
0
]))

                self.order = self.buy(size=
100
)
# 以下一日开盘价买入100股
# # 10日均线下穿5日均线,卖出
elif
 self.crossover <
0
:

            self.order = self.close()
# 平仓,以下一日开盘价卖出
    ......


# 实例化大脑
cerebro= bt.Cerebro(cheat_on_open=
True
)

.......

# 当日下单,当日开盘价成交
# cerebro.broker.set_coo(True)

# 部分运行结果
2019-01-17
 Send Buy, open
32.63307367
2019-01-17
 Buy Executed at price
32.63307367
2019-01-29
 Send Sell, open
33.928526500000004
2019-01-29
 Sell Executed at price
33.928526500000004
2019-02-22
 Send Buy, open
34.91553818
2019-02-22
 Buy Executed at price
34.91553818
2019-02-27
 Send Sell, open
37.50644384
2019-02-27
 Sell Executed at price
37.50644384
2019-03-15
 Send Buy, open
41.20773764
2019-03-15
 Buy Executed at price
41.20773764
2019-03-18
 Send Sell, open
44.10708445
2019-03-18
 Sell Executed at price
44.10708445
......
与常规模式返回的结果进行对可知:
  • 原本 2019-01-16 生成的下单指令,被延迟到了 2019-01-17 日才发出;
  • 2019-01-17 发出的订单,在 2019-01-17 当日就以 开盘价 执行成交了。
Cheat-On-Close
Cheat-On-Close 是“当日下单,当日以收盘价成交”模式,在该模式下,Strategy 中的交易逻辑仍写在 next() 中,具体设置如下:
  • 方式1:cerebro.broker.set_coc(True);
  • 方式3:BackBroker(coc=True)。
classTestStrategy(bt.Strategy):
    ......

defnext(self):
# 取消之前未执行的订单
if
 self.order:

            self.cancel(self.order)

# 检查是否有持仓
ifnot
 self.position:

# 10日均线上穿5日均线,买入
if
 self.crossover >
0
:

                print(
'{} Send Buy, open {}'
.format(self.data.datetime.date(),self.data.open[
0
]))

                self.order = self.buy(size=
100
)
# 以下一日开盘价买入100股
# # 10日均线下穿5日均线,卖出
elif
 self.crossover <
0
:

            self.order = self.close()
# 平仓,以下一日开盘价卖出
    ......


# 实例化大脑
cerebro= bt.Cerebro()

.......

# 当日下单,当日收盘价成交
cerebro.broker.set_coc(
True
)



# 部分运行结果
2019-01-16
 Send Buy, close
32.63307367
2019-01-16
 Buy Executed at price
32.63307367
2019-01-28
 Send Sell, close
33.86683827
2019-01-28
 Sell Executed at price
33.86683827
2019-02-21
 Send Buy, close
34.85384995
2019-02-21
 Buy Executed at price
34.85384995
2019-02-26
 Send Sell, close
37.75319676
2019-02-26
 Sell Executed at price
37.75319676
2019-03-14
 Send Buy, close
41.20773764
2019-03-14
 Buy Executed at price
41.20773764
2019-03-15
 Send Sell, close
42.62656693
2019-03-15
 Sell Executed at price
42.626566929999996
......
与常规模式返回的结果进行对可知:
  • 2019-01-16 生成的下单指令,当天就被发送,而且当天就以 收盘价 执行了;并未在指令发出的下一日执行。
未完待续
下一篇《Backtrader 交易篇(下)》将会围绕 Order 给大家讲述剩下的“交易函数”、“交易订单”、“交易执行”、“交易结算”等相关内容。敬请期待!
量化投资与机器学习微信公众号,是业内垂直于量化投资、对冲基金、Fintech、人工智能、大数据领域的主流自媒体公众号拥有来自公募、私募、券商、期货、银行、保险、高校等行业20W+关注者,连续2年被腾讯云+社区评选为“年度最佳作者”。
继续阅读
阅读原文