Backtrader系列教程④:交易篇(上)

预定系列

  • Backtrader 来了
  • Backtrader 数据篇
  • Backtrader 指标篇
  • Backtrader 交易篇(上)
  • Backtrader 交易篇(下)
  • Backtrader 策略篇
  • Backtrader 可视化篇(重构)
  • Backtrader 常见问题汇总
  • Backtrader 常见案例汇总
  • ······

前言

无论是实盘,还是模拟或回测,都会涉及到 “交易”,区区 “交易” 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)

# self.broker.getcash,self.broker.getvalue 等后面要加括号self.broker.getcash(), 否则会报错:TypeError: must be real number, not method

# 注: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

......

  • 在计算当前可用资金时,除了考虑扣除购买标的时的费用外,还需要考虑扣除交易费用 。

滑点管理

在实际交易中,由于市场波动、网络延迟等原因,交易指令中指定的交易价格与实际成交价格会存在较大差别,出现滑点。为了让回测结果更真实,在交易前可以通过 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

......

固定滑点

假设设置了大小为 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

有关滑点的其他设置

除了用于设置滑点的 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。

# 情况1:

set_slippage_fixed(fixed= 0.35,

slip_open= False,

slip_match= True,

slip_out= False)

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

......

date 2019-01-16open 33.00320305low 32.57138544high 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-17open 32.63307367low 31.83112668high 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-16open 33.00320305low 32.57138544high 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-17open 32.63307367low 31.83112668high 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-17open 32.63307367low 31.83112668high 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-17open 32.63307367low 31.83112668high 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-18open 31.95450314low 31.95450314high 32.81813836

......

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

crossover ture

2019-07-02, 当前持仓, 0.00

2019-07-02, 当前持仓, 0.00

date 2019-07-02open 40.4803098low 39.70925628high 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-03open 39.96627412low 39.90201966high 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 种费用:“股票百分比费用”和“期货固定费用”,官方文档还给大家提供了“期货百分比费用”的自定义子类:

# 自定义期货百分比费用

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-16open 33.00320305volume 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-17open 32.63307367volume 869.0当前持仓量 869.0当前持仓成本 32.63307367

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

......

date 2019-02-21open 35.47073225volume 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-22open 34.91553818volume 1627.0当前持仓量 1627.0当前持仓成本 34.91553818

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

......

date 2019-03-14open 41.82461994volume 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-15open 41.20773764volume 4078.0当前持仓量 2000.0当前持仓成本 41.20773764

......

ate 2019-05-17open 40.93009102volume 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-20open 39.77351074volume 1686.0当前持仓量 314.0当前持仓成本 46.63630188

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

......

date 2019-06-11open 38.48842154volume 4038.0当前持仓量 314.0当前持仓成本 46.63630188

date 2019-06-12open 39.90201966volume 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-13open 40.09478304volume 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-16open 33.00320305volume 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-17open 32.63307367volume 869.0当前持仓量 434.0当前持仓成本 32.63307367

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

......

date 2019-02-21open 35.47073225volume 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-22open 34.91553818volume 1627.0当前持仓量 813.0当前持仓成本 34.91553818

......

date 2019-03-14open 41.82461994volume 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-15open 41.20773764volume 4078.0当前持仓量 2000.0当前持仓成本 41.20773764

......

date 2019-04-02open 48.1168194volume 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-03open 47.06811949volume 3826.0当前持仓量 87.0当前持仓成本 46.14279604

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

......

date 2019-05-16open 40.35180088volume 1424.0当前持仓量 87.0当前持仓成本 46.14279604

date 2019-05-17open 40.93009102volume 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-20open 39.77351074volume 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-16open 33.00320305high 33.00320305low 32.57138544volume 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-17open 32.63307367high 32.94151482low 31.83112668volume 869.0当前持仓量 36.0当前持仓成本 32.63307367

date 2019-01-18open 31.95450314high 32.81813836low 31.95450314volume 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-16Send Buy, open 33.00320305

2019-01-17Buy Executed at price 32.63307367

2019-01-28Send Sell, open 33.311644199999996

2019-01-29Sell Executed at price 33.928526500000004

2019-02-21Send Buy, open 35.47073225

2019-02-22Buy Executed at price 34.91553818

2019-02-26Send Sell, open 37.07462623

2019-02-27Sell Executed at price 37.50644384

2019-03-14Send Buy, open 41.82461994

2019-03-15Buy Executed at price 41.20773764

2019-03-15Send Sell, open 41.20773764

2019-03-18Sell Executed at price 44.10708445

2019-03-29Send Buy, open 43.55189038

2019-04-01Buy Executed at price 46.14279604

2019-04-02Send Sell, open 48.1168194

2019-04-03Sell 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 notself.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 # 平仓,以下一日开盘价卖出

classTestStrategy(bt.Strategy):

......

defnext_open(self):

# 取消之前未执行的订单

ifself.order:

self.cancel(self.order)

# 检查是否有持仓

ifnotself.position:

# 10日均线上穿5日均线,买入

ifself.crossover > 0:

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

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

# # 10日均线下穿5日均线,卖出

elifself.crossover < 0:

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

......

# 实例化大脑

cerebro= bt.Cerebro(cheat_on_open= True)

.......

# 当日下单,当日开盘价成交

# cerebro.broker.set_coo(True)

# 部分运行结果

2019-01-17Send Buy, open 32.63307367

2019-01-17Buy Executed at price 32.63307367

2019-01-29Send Sell, open 33.928526500000004

2019-01-29Sell Executed at price 33.928526500000004

2019-02-22Send Buy, open 34.91553818

2019-02-22Buy Executed at price 34.91553818

2019-02-27Send Sell, open 37.50644384

2019-02-27Sell Executed at price 37.50644384

2019-03-15Send Buy, open 41.20773764

2019-03-15Buy Executed at price 41.20773764

2019-03-18Send Sell, open 44.10708445

2019-03-18Sell 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)。

def next(self):

    # 取消之前未执行的订单

    if self.order:

        self.cancel(self.order)

    # 检查是否有持仓

    if notself.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-16Send Buy, close 32.63307367

2019-01-16Buy Executed at price 32.63307367

2019-01-28Send Sell, close 33.86683827

2019-01-28Sell Executed at price 33.86683827

2019-02-21Send Buy, close 34.85384995

2019-02-21Buy Executed at price 34.85384995

2019-02-26Send Sell, close 37.75319676

2019-02-26Sell Executed at price 37.75319676

2019-03-14Send Buy, close 41.20773764

2019-03-14Buy Executed at price 41.20773764

2019-03-15Send Sell, close 42.62656693

2019-03-15Sell Executed at price 42.626566929999996

......

与常规模式返回的结果进行对比可知:

  • 2019-01-16 生成的下单指令,当天就被发送,而且当天就以 收盘价 执行了;并未在指令发出的下一日执行。

未完待续

下一篇《Backtrader 交易篇(下)》将会围绕 Order 给大家讲述剩下的“交易函数”、“交易订单”、“交易执行”、“交易结算”等相关内容。敬请期待!

  • 8
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Backtrader AutoDateLocator是一个用于自动确定日期刻度位置的工具。它是Backtrader库中的一部分,Backtrader是一个用于开发和回测交易策略的Python框架。 AutoDateLocator的主要功能是根据给定的日期范围和绘图区域的大小,自动确定合适的日期刻度位置。它可以根据需要在图表上显示适当的日期刻度,以便更好地展示时间序列数据。 使用AutoDateLocator非常简单。您只需将其与Matplotlib的日期刻度定位器(DateLocator)一起使用即可。以下是一个简单示例: ``` import backtrader as bt import matplotlib.pyplot as plt from matplotlib.dates import AutoDateLocator # 创建一个Backtrader策略 class MyStrategy(bt.Strategy): def __init__(self): # 初始化策略 def next(self): # 策略逻辑 # 创建Cerebro引擎 cerebro = bt.Cerebro() # 添加策略 cerebro.addstrategy(MyStrategy) # 运行回测 results = cerebro.run() # 绘制图表 cerebro.plot(style='bar') # 获取当前图表对象 fig = plt.gcf() # 使用AutoDateLocator设置日期刻度 locator = AutoDateLocator() fig.autofmt_xdate() fig.gca().xaxis.set_major_locator(locator) # 显示图表 plt.show() ``` 在上面的示例中,我们创建了一个Backtrader策略,并使用Cerebro引擎运行回测。然后,我们使用`cerebro.plot()`方法绘制图表,并获取当前图表对象。最后,我们使用AutoDateLocator设置日期刻度,并显示图表。 希望这可以帮助您了解Backtrader AutoDateLocator的使用!如果您有任何其他问题,请随时提问。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值