Backtrader来啦:交易篇(上)

论坛 期权论坛 期权     
期权匿名问答   2022-1-2 02:02   15428   2
量化投资与机器学习公众号 独家撰写



公众号为全网读者带来Backtrader系列自推出第一期以来,受到了众多读者的喜爱与点赞,QIML也会继续把这个系列做好。

让那些割韭菜的课程都随风而去吧!!!

公众号将为大家多维度、多策略、多场景来讲述Backtrader在量化投资领域的实践应用。同时,我们对每段代码都做了解读说明,愿你在Quant的道路上学有所获!
预定系列


  • Backtrader 来了
  • Backtrader 数据篇
  • Backtrader 指标篇
  • Backtrader 交易篇(上)
  • Backtrader 交易篇(下)
  • Backtrader 策略篇
  • Backtrader 可视化篇(重构)
  • Backtrader 常见问题汇总
  • Backtrader 常见案例汇总
  • ······
此系列将由浅入深,每期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 中进行持仓查询操作:
class TestStrategy(bt.Strategy):
    def next(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 基础类的基础上自定义交易费用
class MyCommission(bt.CommInfoBase):
    # 对应 setcommission 中介绍的那些参数,也可以增添新的全局参数
    params = ((xxx, xxx),)
    # 自定义交易费用计算方式
    def _getcommission(self, size, price, pseudoexec):
        pass
    # 自定义佣金计算方式
    def get_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 种费用:“股票百分比费用”和“期货固定费用”,官方文档还给大家提供了“期货百分比费用”的自定义子类:
# 自定义期货百分比费用
class CommInfo_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)下面是考虑佣金和印花税的股票百分比费用:
class StockCommission(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:
            return 0成交量限制管理

默认情况下,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)。
class TestStrategy(bt.Strategy):
    ......
    def next_open(self):
        # 取消之前未执行的订单
        if self.order:
            self.cancel(self.order)
        # 检查是否有持仓
        if not 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)。
class TestStrategy(bt.Strategy):
    ......
    def next(self):
        # 取消之前未执行的订单
        if self.order:
            self.cancel(self.order)
        # 检查是否有持仓
        if not 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年被腾讯云+社区评选为“年度最佳作者”。
分享到 :
0 人收藏

2 个回复

倒序浏览
2#
期权匿名回答  16级独孤 | 2022-1-2 02:03:09 发帖IP地址来自 北京
先收藏,慢慢看。 数字货币搞起。
3#
期权匿名回答  16级独孤 | 2022-1-2 02:03:37 发帖IP地址来自 北京
刚想睡觉就有人送枕头。。[飙泪笑]
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:400157
帖子:80032
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP