量化投资之工具篇一:Backtrader从入门到精通(8)-交易系统代码详解

本文将介绍Backtrader的交易系统,包括Order、Broker、Trade和Sizer等和交易相关关键类。

Order(订单)

这个有翻译为订单,也有翻译为委托单的,后续统一为订单。

如之前文章所述,Cerebro是Backtrader的关键控制中心,是大脑。而Strategy是大脑的神经,基于数据和分析做出最终的决策,那么这个决策如何由系统的其他部件去成交呢?订单就承担这样的责任,将Strategy的做出的决策转换为由券商(Broker)执行操作的消息,通过如下三个方式完成:

  • 创建

这个在咱们之前的代码中可以看出,通过Strategy的buy,sell和close,返回的是一个Order实例。

  • 取消

可以通过Strategy的Cancel函数取消,参数必须指定操作的Order

另外,Order也可以反向给使用者Strategy回馈信息,通过通知的方式告知订单的成交情况:

  • 通知

使用Strategy的notify_order函数返回订单实例。

以上咱们之前的示例中均有涉及,下面重点从代码的角度看看咱们是如何使用Order。

Order的创建

在介绍Order操作之前,咱们先看看Order类:

class Order(OrderBase):
class OrderBase(with_metaclass(MetaParams, object)):

从代码可以看出Order类继承自OrderBase,而OrderBase直接继承了所有类的最顶层object和元类MetaParams。从继承关系可以看出,Order是一个极普通的类,没有复杂的继承关系,咱们就不画图了。另外,Order也继承了MetaParams,也就是Order的创建会受到元类的控制,MetaParams之前咱们讲过,主要是针对参数的处理。

先看订单的创建,有多单和空单以及对应的平仓订单,通常,订单是在Strategy的next中根据策略来创建。主要体现就是3个函数:buy、sell和close。比如我们之前示例中,最简单的使用方式如下:

self.order = self.buy()
self.order = self.sell()

顺着这个线索,我们看看Backtrader是如何处理的。

def buy(self, data=None,
            size=None, price=None, plimit=None,
            exectype=None, valid=None, tradeid=0, oco=None,
            trailamount=None, trailpercent=None,
            parent=None, transmit=True,
            **kwargs):
        
        if isinstance(data, string_types):
            data = self.getdatabyname(data)

        data = data if data is not None else self.datas[0]
        size = size if size is not None else self.getsizing(data, isbuy=True)

        if size:
            return self.broker.buy(
                self, data,
                size=abs(size), price=price, plimit=plimit,
                exectype=exectype, valid=valid, tradeid=tradeid, oco=oco,
                trailamount=trailamount, trailpercent=trailpercent,
                parent=parent, transmit=transmit,
                **kwargs)

        return None

首先看函数的关键参数,咱们之前在讲解Strategy的时候详细介绍过,不过为了方便理解Order,咱们还是在这里再提供下:

参数 缺省值 含义
data None 指定本次操作归属的data。每个data记录的是每个标的(股票、期货等等)的数据(open/close…),买卖操作就是基于这些数据。在多个资产(或者证券,包括股票期货等等)的情况下,你可能需要针对不同的数据创建订单。缺省情况就是针对第一个数据(data0)。
size None 本单买卖的数量。比如说股票,本次你要买卖多少股。有些地方可能有最小限制,比如国内最小一手100股。这个可以通过addsizer的stake指定。size也是很重要的策略,这一单下多少?还是很有学问,后面我们还要单独研讨。
price None 指定价格。这个参数在市价订单单(Market,通常是下一个开市价格)或者收市订单(close价格)的时候,不需要设置(也就是None)。因为价格由市场来决定,在Backtrader中使用的开市订单
对于限价委托(Limit)单、止损订单(Stop)和止损限价订单(StopLimit),这个price就是订单的触发价格。几种单子的情况下文还要详细描述。
plimit None 止损限价。这个只有止损限价订单的有效。因为这种类型的订单需要两个价格,具体参见下文描述。
exectype None 订单成交类型:
None:这个就是市价委托,在backtrader中,采取的下一个bar的开市(open)价格创建订单。
Close:采取下一个bar的收盘价(close)创建订单。
Limit:限价订单。这种在向broker发出买卖某种股票的指令时,对买卖的价格作出限定,对于多单(买),限定一个最高价,只允许broker按其规定的最高价或低于最高价的价格成交,对于空单(卖),限定一个最低价。限价委托的最大特点是,股票的买卖可按照投资人希望的价格或者更好的价格成交,有利于投资人实现预期投资计划。
Stop: 止损订单。对于多单:低于指定价格卖出,防止亏损扩大。对于空单,高于指定价格卖出。这个价格采用的是市价(也就是下一个开市价open),也成为止损市价订单。还有一种止盈订单,和上述策略相反
StopLimit:止损限价订单,就是以限价委托的止损单。止损限价指令避免了止损指令成交价格不确定的不足,在止损价委托中,投资者要注明两个价格:止损价(对应参数price)和限价(对应参数plimit),一旦市场价格达到或超过止损价格,止损限价委托自动形成一个限价委托。
国内后两种券商都不支持,据说期货支持,没玩过。不过现在很多券商会提供一些条件单功能,基本上也可以达成相同的效果。因此,我们在做好策略回测之后,对于验证好的策略,可以通过券商的条件单设置自动完成交易。
还有追踪止损(StopTrail)、追踪踪止损限价(StopTrailLimit)等,订单的成交方式是策略的重要手段,后面专门讨论。
valid None 有效期。有如下取值:
None:无有限期,这种情况下,改订单一致存在直到订单满足条件被成交或者被取消。现实中,通常会有时间限制,但是我们这里还是当做无期限。
datetime.datetime 或者datetime.date 实例:也就是指定时间或者日期。也就是订单截止时间。
Order.DAY 或者0 或者 timedelta():也就是指定订单的持续时间。
数值:使用数值指定的截止时间,这个主要用于matplotlib(Backtrader用于画图)的时间编码方式。
tradeid 0 这是一个内部标识。如果多个交易(trade)使用的相同的资产,那么通过整个标识区分不同的交易。在后续通知的处理中,tradeid会返回给Strategy进行区分处理
**kwargs 还要一些broker的实现会支持更多的参数,那么通过**kwargs传递。

此外,还有几个参数用于一些特殊的订单,后续专门说明。

回到代码,Strategy中执行buy函数,其实就是调用broker的buy函数。策略的broker来自哪里呢?可以回头看看Strategy的代码解读,来自Cerebro。

那么在broker函数中会做啥呢?

def buy(self, owner, data,
            size, price=None, plimit=None,
            exectype=None, valid=None, tradeid=0, oco=None,
            trailamount=None, trailpercent=None,
            parent=None, transmit=True,
            histnotify=False, _checksubmit=True,
            **kwargs):

        order = BuyOrder(owner=owner, data=data,
                         size=size, price=price, pricelimit=plimit,
                         exectype=exectype, valid=valid, tradeid=tradeid,
                         trailamount=trailamount, trailpercent=trailpercent,
                         parent=parent, transmit=transmit,
                         histnotify=histnotify)

        order.addinfo(**kwargs)
        self._ocoize(order, oco)

        return self.submit(order, check=_checksubmit)

看第9行,就是创建了一个BuyOrder(就是Order的子类),这里会实例化和初始化一个Order实例,并返回。这个代码后续的处理咱们在Broker部分再讲。至此,一个Order就创建成功了。

对于sell也类似,也就是在broker中实例化和初始化一个SellOrder。

close就是平仓操作,什么是平仓?翻译成专业术语就是执行和现有持仓未平仓头寸完全相反的证券交易,也就是关闭证券的多头头寸需要卖出,而关闭证券的空头头寸则需要买回。

def close(self, data=None, size=None, **kwargs):
       
        if isinstance(data, string_types):
            data = self.getdatabyname(data)
        elif data is None:
            data = self.data

        possize = self.getposition(data, self.broker).size
        size = abs(size if size is not None else possize)

        if possize > 0:
            return self.sell(data=data, size=size, **kwargs)
        elif possize < 0:
            return self.buy(data=data, size=size, **kwargs)

        return None

关键点在于:

  • 可以指定平仓对应的数据,没有指定,就是缺省第一个数据。
  • 然后获取该数据的持仓情况。如果是多头持仓,就创建卖单。如果是空头持仓,就创建卖单。
  • 当然还可以指定size,也就是可以用于减仓,不是完全平掉。

至于cancel,就是将订单置为Canceled状态,后续不再成交就行了,具体就不讲了。

创建订单的示例

Backtrader官网提供了一些示例,可供我们代码的时候参考:

# 这是最简单的使用方法,创建买单,使用缺省的规模(size),使用市价成交。
order = self.buy()

# 市价单,指定有效期,这个有效期对于事件单是无效的,因为市价单是下一天成交。
order = self.buy(valid=datetime.datetime.now() + datetime.timedelta(days=3))

# 市价单,指定成交价格,这个价格也会无效,因为市价单使用open价格成交。
order = self.buy(price=self.data.close[0] * 1.02)

# 市价单,手动指定规模。
order = self.buy(size=25)

# 限价单,设定价格和有效期
order = self.buy(exectype=Order.Limit,
                 price=self.data.close[0] * 1.02,
                 valid=datetime.datetime.now() + datetime.timedelta(days=3)))

# 止损限价单,设定价格和限定价。
order = self.buy(exectype=Order.StopLimit,
                 price=self.data.close[0] * 1.02,
                 plimit=self.data.close[0] * 1.07)

# 所有订单全部取消,是否能取消成功,取决于订单当前的实际状态。
self.broker.cancel(order)

order通知

通常我们会在定制Strategy中定义notify_order函数用于跟踪订单的状态信息,如下示例:

def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            # 提交和接受委托单不做任何处理
            return
            # 订单完成,记录。
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(
                    '买单成交 成交价格: %.2f, 成交金额: %.2f, 佣金 %.2f' %
                    (order.executed.price,
                     order.executed.value,
                     order.executed.comm))
                self.buyprice = order.executed.price
                self.buycomm = order.executed.comm
            else:  # Sell
                self.log('卖单成交 成交价格: %.2f, 成交金额: %.2f, 佣金 %.2f' %
                         (order.executed.price,
                          order.executed.value,
                          order.executed.comm))
            self.bar_executed = len(self)
        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log('Order 取消/金额不足/拒绝')
        self.order = None

在该函数中,我们可以或者定制的状态信息,以及成交价格、金额等信息,也可以将相关信息计入日志供后续分析。

如下几点需要关注:

  • notify是在next函数中触发的

  • 在一次next中,对相同的订单也可能触发多个通知(相同状态或者不同状态)。

  • 订单提交给broker之后,可能会在下个next中之前就会立即成交,这种情况下通常是如下三种状态的订单:

    1. Order.Submitted(提交):因为订单提交给broker,这个在代码中就是立即完成的。
    2. Order.Accepted(接受):订单立即被broker接受,在回测系统中,broker没理由拒绝订单。
    3. Order.Completed(成交):这种情况下,就是订单条件立即满足,所以实时成交。
  • 在相同状态下,例如Order.Partial(部分成交),Backtrader也可能产生多个通知。这个在回测系统中不会出现,你就是买1个亿的股票,还是会立即成交。但是在实际交易中可不行。所以说规模是量化交易基金的天敌,你看有些量化基金规模小业绩很好,一上规模就不行了,就是很多交易机会在大资金量下无法成交。

  • 交易数据会保存在order.executed中,这个数据在订单的整个生命周期会一直存在。

订单的状态咱们之前也介绍过,这里再说明下:

  1. 创建(Create):实例化后就是这个状态,这个状态对用户没啥意义,不可见。
  2. 提交(Submitted):订单发送给broker。在回测的时候,这个就是一个及时的动作(前面broker的buy函数中,立即调用了submit),但是在实际券商中,可能会需要一定的时间才能完成。实际券商可能会收到订单,并且只有在订单被转发到交易所时才通知。
  3. 接受(Accepted):经纪人已接受订单,并且根据设置的参数(如成交类型、大小、价格和时效)在系统中(或已在交易所中)等待成交。
  4. 部分成交(Partial):订单只有部分被成交,比如你要买100股,实际成交50股。order.executed包含当前完成的数量以及平均价格。order.executed.exbits则包含了所有部分订单的信息。
  5. 完全成交(Completed):订单全部成功成交,记录成交的平均价格。
  6. 取消(Canceled):订单被用户取消。这里需要考虑到,通过Strategy的取消方法取消订单的请求并不保证取消。实际中订单可能已经成交。
  7. 超时(Expired):订单因为超时被取消。订单如果超过设定的时间,就会被系统取消。
  8. 金额不足(Margin):订单因为现金金额不足被取消。回测中不会出现。
  9. 拒绝(Rejected):订单被broker拒绝。券商有可能会因为各种原因拒绝订单,当然回测的时候咱们是模拟系统,就不会有这个拒绝。拒绝原因将通过Strategy的notify_store方法通知。为啥不在notify_order中通知呢?原因是现实中券商 拒绝一个订单可能订单直接相关,也可能与订单无关。所以统一通过notify_store通知。

一些关键order类的说明

在Backtrader中,Order相关的关键类包括Order,OrderData和OrderExecutionBit。

Order类

order类主要用于记录订单相关信息,例如类型、状态,成交方式等。

状态以及成交类型前文已叙及,下面提供下几个关键属性:

  • ref: 参考号,订单的唯一编号。
  • created: OrderData对象, 用于存储创建时的数据。
  • executed: OrderData对象,用于存储成交时的数据。
  • info: 在Order中,可以通过addinfo(**kwargs)函数添加自定义的数据,就保存在这个属性里面,保存形式是字典。这样的话,如果进行一些策略的定制开发,可以在订单中传递一些信息。

还有几个方法经常用到:

  • isbuy(): 如果是买单,返回True.
  • issell(): 如果是卖单,返回True.
  • alive(): 如果订单在创建状态(Create)、提交状态(Submitted)和部分成交状态(Partial)和接受状态(Accepted),返回True。

OrderData

这个类在Order中使用,用于记录订单创建和成交时的数据,可以用于对比创建和实际成交的差别,主要包含以下关键属性:

  • exbits :记录一组OrderExecutionBits(或者说迭代器),用于记录所有成交(包括部分成交)的信息。
  • dt:创建/成交的时间
  • size:请求和成交的规模,就是这一单的下注金额。
  • price:成交的价格 注意: 如果没有设定price和pricelimite参数, 当前的收盘价或者订单创建时的收盘价会作为参考。
  • pricelimit:为 StopLimit类型的订单保存保存 pricelimit价格。
  • trailamount:追踪止损单的价格绝对差值。
  • trailpercent:追踪止损单的价格百分比差值。
  • value:当前订单头寸下的市值。
  • comm:订单本次成交的佣金。
  • pnl:全称profit and loss,订单的利润/损失,只有订单平仓,一次交易完成才有pnl。
  • margin:订单导致的保证金,这个在支持保证金(融资)交易的券商有作用。
  • psize:当前开仓的规模。
  • pprice:当前开仓的价格。

OrderExecutionBit

这个类在OrderData中使用,用于记录订单的成交情况,这个类并不能指示订单是完全成交还是部分成交,仅仅指示记录信息。

  • dt:订单成交时间。
  • size:本次成交的规模,可能只有部分成交。
  • price:成交价格
  • closed:已成交部分有多少是关闭现有持仓。
  • opened:已成交部分有多少是新开持仓。这个怎么理解?比如你有10股股票,创建个卖单卖掉11股,其中10股是关闭现有持仓,1股是创建新的空头持仓。
  • openedvalue:新开仓头寸的市值。
  • closedvalue:已关闭头寸的市值。
  • closedcomm:已关闭头寸的佣金。
  • openedcomm:新开仓头寸的佣金。
  • value:整个头寸的市值。
  • comm:整个头寸的佣金。
  • pnl:订单的利润/损失。
  • psize:新开仓的规模。
  • pprice:新开仓价格。

目标订单

Backtrader还提供了一种智能化的下单方式:目标订单。咱们前面所述的buy,sell和close都需要指定size(订单的规模,例如1手100股),这个通过Sizer类来完成。但是我们在进行资产配置的时候,希望对资产组合中的进行调整,设定规模就不太方便,因为你很难知道最后的效果,这时,可以通过设定目标,由系统自动根据目标规模下单。

对于规模的设定,可以有3种方式:

  • 目标规模(size):可以设定资产组合中特定资产的规模大小。
  • 目标市值(value):可以设定资产组合中特定资产的目标市值。
  • 目标百分比(percent):设定资产组合中特定资产的所占比例。

一个组合中,特定资产(例如股票)如何指定?通过data来指定,不同的data对应具体的资产。比如,你要建一个组合,其中包含10个股票,那么需要增加10个股票对应的数据,各种操作针对具体的数据,前面参数表格中已有描述。

在Backtrader中,你设定具体的资产目标规模/市值/目标百分比,系统根据当前持仓情况,和目标进行对比,来决定是buy还是sell,抑或是close。

以目标规模为例,如果目标规模大于当前持仓,就会进行买入操作,规模为:目标规模-当前持仓。例如

  • 当前持仓:0,目标规模:7,那么就会buy(size=7)
  • 当前持仓:-3(负值表示空头持仓),目标规模:7,那么就会buy(size=10)
  • 当前持仓:-3(负值表示空头持仓),目标规模:-2,那么就会buy(size=1)

如果目标规模小于当前持仓,就会进行卖出操作,规模为:当前持仓-目标规模。例如:

  • 当前持仓0,目标规模:-7,那么就会sell(size=7)
  • 当前持仓3,目标规模:-7,那么就会sell(size=10)
  • 当前持仓3,目标规模:2,那么就会sell(size=1)

在Backtrader中,分别提供了如下3个函数(定义在Strategy类中,因此直接在Strategy中调用)完成目标订单的设定:

order_target_size

下面我们通过源代码看看系统是如何实现目标订单的设置。

def order_target_size(self, data=None, target=0, **kwargs):     
        if isinstance(data, string_types):
            data = self.getdatabyname(data)
        elif data is None:
            data = self.data

        possize = self.getposition(data, self.broker).size
        if not target and possize:
            return self.close(data=data, size=possize, **kwargs)

        elif target > possize:
            return self.buy(data=data, size=target - possize, **kwargs)

        elif target < possize:
            return self.sell(data=data, size=possize - target, **kwargs)

        return None  # no execution target == possize

关键代码解读如下:

  • 2-5行获取指定的资产(data),如果没有指定,那么就是用缺省第一个数据。这里提醒下,可以看出,使用这个函数,如果要指定特定资产,那就必须要使用名称,也就是调用Cerebro,adddata的时候推荐一定设置一个名称,比如你这个股票的编号和名称,也方便后续的操作。
  • 7-9行获取目标资产当前的持仓头寸,如果没有设置目标规模(也就是0)而且当前有头寸,直接清仓(close),清仓咱们前面讲过,如果有多头头寸(大于0),卖出资产。如果有空头尺寸(小于0),买入资产。
  • 11-12行,就是咱们举例说明的如果目标规模大于当前持仓,就会进行买入操作,规模为目标规模-当前持仓。
  • 14-15行,目标规模小于当前持仓,就会进行卖出操作

order_target_value

代码如下:

def order_target_value(self, data=None, target=0.0, price=None, **kwargs): 
        if isinstance(data, string_types):
            data = self.getdatabyname(data)
        elif data is None:
            data = self.data

        possize = self.getposition(data, self.broker).size
        if not target and possize:  # closing a position
            return self.close(data=data, size=possize, price=price, **kwargs)

        else:
            value = self.broker.getvalue(datas=[data])
            comminfo = self.broker.getcommissioninfo(data)

            # Make sure a price is there
            price = price if price is not None else data.close[0]

            if target > value:
                size = comminfo.getsize(price, target - value)
                return self.buy(data=data, size=size, price=price, **kwargs)

            elif target < value:
                size = comminfo.getsize(price, value - target)
                return self.sell(data=data, size=size, price=price, **kwargs)

        return None  # no execution size == possize

和目标规模相比,这里的主要差别如下:

  • 12-13行:获取目标资产的当前市值以及对应的comminfo信息,comminfo后面再讲,主要保存应用于当前资产的佣金信息。
  • 16行:因为根据金额计算规模需要价格信息,所以没有如果输入price,那么就是用当前的收盘价。
  • 18-20行:如果目标市值大于当前市值,那么就根据差额计算买入订单的规模(考虑价格和佣金等因素)。这里要注意的是,由于是使用当前收盘价计算的规模,可能和实际成交有差异,这里需要预留一部分资金。
  • 22-24行:相反地,就会买入一定规模的资产以满足目标市值。

order_target_percent

代码如下:

def order_target_percent(self, data=None, target=0.0, **kwargs):
        if isinstance(data, string_types):
            data = self.getdatabyname(data)
        elif data is None:
            data = self.data

        possize = self.getposition(data, self.broker).size
        target *= self.broker.getvalue()

        return self.order_target_value(data=data, target=target, **kwargs)

这段代码关键就是获取当前资产组合所有市值(注意,这里getvalue没有指定data,就是返回所有市值包括现金),然后乘以目标百分比(第8行)作为当前资产的目标市值,然后使用order_target_value函数完成买卖操作。

通过以上三个函数,可以方便地对投资组合进行调整。投资组合的管理是量化交易系统非常重要的部分,因此需要重点关注。

OCO订单

什么是OCO订单,全称是One Cancel Others,翻译的意思就是成交一个取消其它。OCO订单是一组订单(通常是两个),规则是如果一个订单成交,则其它订单将自动取消。OCO 订单通常结合了自动交易平台上的止损订单和限价订单。当达到止损或限价并成交订单时,另一个订单将自动取消。我们可以使用 OCO 订单来降低风险。要注意,这个需要交易平台支持,目前Backtrader只能是回测实现。

假设投资者拥有 1,000 股价格为 10 元的股票。投资者预计这只股票在短期内将在大范围内交易,目标价为 13元。为了减轻风险,他们不希望每股损失超过 2 元。因此,投资者可以发出 OCO 指令,其中包括以 8 元卖出 1,000 股的止损指令,以及以 13 元卖出 1,000 股的同时限价指令,以先到者为准。如果股票的交易价格高达 13元,则成交卖出限价单,投资者持有的 1,000 股股票以 13元的价格卖出。同时,8元的止损单被交易平台自动取消。如果投资者独立下达这些订单,他们可能会忘记取消止损订单,如果股票随后交易价格下跌至 8元,这可能会导致1,000 股的空头头寸。

在Backtrader中如何使用呢?可以参考如下官方示例:

def next(self):
    ...
    o1 = self.buy(...)
    ...
    o2 = self.buy(..., oco=o1)
    ...
    o3 = self.buy(..., oco=o1)  # 也可以oco=o2, 因为o2已经在o1组中

首先,设定一个普通订单O1,可以当做一个组的领头羊。

然后创建另外订单(例如O2/O3)的时候,通过参数指定oco为o1,则这些订单就形成了一个OCO组合,如果该组中的任何订单被成交、取消或到期,其他订单将被取消。

Bracket 订单

Bracket订单,不好翻译,括号订单?你可以理解会将3个订单放到一个括号内,类似这样(低价单,主订单,高价单),逻辑上也等于一组订单,这一组订单和OCO订单有差别,OCO订单地位平等,Bracket订单有主次之分,下面详细说明。

Bracket订单区分为买单(做多)和卖单(做空)。

对于买单,包括3个订单:

  • 一个买单(主订单):通常为限价单(Limit)或者限价止损(StopLimit)单。
  • 一个卖单(低价单):设置为止损单(Stop),用来限定亏损(止损),价格低于买入订单。
  • 一个卖单(高价单),设置为限价单(Limit),用来锁定利润(止盈),价格高于买入订单。

相应地,对于卖单,也包括3个订单:

  • 一个卖单(主订单):通常为限价单(Limit)或者限价止损(StopLimit)单。
  • 一个买单(高价单):设置为止损单(Stop),用来限定亏损(止损),价格高于买入订单。
  • 一个买单(低价单),设置为限价单(Limit),用来锁定利润(止盈),价格低于买入订单。

这些单子的规则如下:

  • 3个订单一起提交,避免分别被触发成交。
  • 低价单和高价单作为主订单的附属。
  • 主订单成交之后,附属订单才会激活,附属订单之一成交/取消,另外一个也会自动取消。
  • 主订单取消,附属订单也会取消。

从以上规则可以看出,这种结构的订单能够保护自己的潜在损失,同时在价格上涨(多单)/下跌(空单)时让自己获利。括号之内订单的价格距离(底价单,主订单,高价单)代表交易的潜在盈亏范围。

Braket订单的作何用途呢?以多单为例。假设投资者以 50 元的价格下达 100 股某股票 的买单,下达 55 元的限价卖单和 45 元的止损卖单。如果价格上涨至 55 元或下跌至 45 元,则卖出该头寸。交易者要么通过卖出限价获利 5 元,要么通过止损订单将损失限制在 5 元。

Braket有两大好处,一个是可以在交易成交之前设置括号内的买单,这为投资者提供了灵活性。另外可以提供纪律约束,投资者使用Bracket买单来实现交易计划。下单后,投资者无需采取任何进一步行动,只需等待止损或限价卖单成交即可。

Backtrader提供了两种方式使用Bracketd订单。

自动创建3个订单

Backtrader同样通过Strategy类提供了两个函数(buy_bracket 和sell_bracket),分别对应买入和卖出的Bracket订单。

一个简单的例子:

brackets = self.buy_bracket(limitprice=14.00, price=13.50, stopprice=13.00)

这个函数返回一个列表,包含3个点单(主订单和两个附属订单),返回格式为[main, stop, limit],具体参见前文描述。比如示例的这一行代码就创建3个订单,13.50买入,超过14块或者低于13块卖出止盈止损。

关于返回的stop和limit,针对多单和空单含义不同:

  • stop:用于止损。对于多单(buy),就是低价卖出单,对于空单(sell),就是高价买入单。
  • limit:用于止盈。对于多单(buy),就是高价卖出单,对于空单(sell),就是低价买入单。

注意,这里高价、低价相对的是主订单的买入/卖出价格。

手动创建3个订单

Backtrader还提供了创建3个订单的方法,方式是通过transmit和parent来指定,规则是:

  • 首先创建主订单,参数 transmit设置为False。
  • 然后创建高价单和低价单,注意,两个订单的参数parent设为第一步创建的主订单。
  • 第一个高价/底价单,参数参数 transmit设置为False。
  • 最后一个订单(高价或者低价单),参数transmit=True,指示Bracket订单完成。

以下为官网示例代码:

mainside = self.buy(price=13.50, exectype=bt.Order.Limit, transmit=False)
lowside  = self.sell(price=13.00, size=mainside.size, exectype=bt.Order.Stop,
                     transmit=False, parent=mainside)
highside = self.sell(price=14.00, size=mainside.size, exectype=bt.Order.Limit,
                     transmit=True, parent=mainside)

几个关键点要注意:

  • 主订单需要通过transmit为False,parent为空指明自己是一个Bracket订单的主订单。
  • 最后一个订单(示例中为highside)通过transmit指示Bracket创建完毕。
  • 附属订单的规模(size)必须和主订单一致,而且要通过参数size明确指定

总体上,我们可以看出,Order可以理解为一个复杂消息体,主要进行订单信息的传递,真正执行操作的是broker,下面我们介绍broker。

Backbroker(模拟券商)

Broker是啥,前面咱们多次说过,在咱们中国,就是券商,例如华泰、中信等等,但是呢?这些券商对个人用户不开放接口,所以咱们也用不上,至于需要啥条件才能用上,这个目前不太清楚,据说也有券商能提供,后面我了解下。由于没有真实(live)券商,咱们只能用模拟Broker进行回测,Backtrader提供了BackBroker类用来模拟券商的行为。如果要实时交易,需要手动挂单,有些券商提供很好的条件单功能。

BackBroker类的继承关系

Backtrader的继承关系比较简单,就先不提供图了。

class BackBroker(bt.BrokerBase)
class BrokerBase(with_metaclass(MetaBroker, object))
class MetaBroker(MetaParams)

从类定义来看,BackBroker也继承了元类,所以其实例化也会受元类控制,下面在实例化和初始化过程进行说明。

BackBroker的实例化和初始化

回头看看咱们系列文章3中Cerebro代码,可以看出Backtrader是Cerebro在初始化函数中调用:

        self._broker = BackBroker()
        self._broker.cerebro = self

当然你也可以自己写个Broker(比如针对特定券商提供的不同的接口),按照如下方法重新设定:

broker = MyBroker()
cerebro.broker = broker

在BackBroker中,可以通过参数控制Broker的行为,参数如下:

    params = (
        ('cash', 10000.0),
        ('checksubmit', True),
        ('eosbar', False),
        ('filler', None),
        # slippage options
        ('slip_perc', 0.0),
        ('slip_fixed', 0.0),
        ('slip_open', False),
        ('slip_match', True),
        ('slip_limit', True),
        ('slip_out', False),
        ('coc', False),
        ('coo', False),
        ('int2pnl', True),
        ('shortcash', True),
        ('fundstartval', 100.0),
        ('fundmode', False),
    )

还有从父类BrokerBase继承的如下参数:

    params = (
        ('commission', CommInfoBase(percabs=True)),
    )

这些关键参数的解释如下表所述:

参数 缺省值 说明
cash 10000 初始现金。在买卖操作的时候,cash会改变。
commission CommInfoBase 这个参数就是一个CommInfoBase类,用于设置佣金方案,这个后面专门说下。
checksubmit True 在下单前是否检查现金和保证金(Margin,香港叫孖展,主要用于融资,也就是杠杆交易,例如根据你持股/现金,评估后提供一定额度的融资)。
eosbar False 代码中该参数并未有实际用途,暂时忽略。
filler None 回调函数,定义为callable(order, price, ago),返回值是数字(含义为size),也就是在订单成交的时候可以通过这个回调函数进行成交量的订单匹配。
slip_xxx - 这几个用于滑点功能,后面专题讨论。
coc False Cheat-On-Close,对于市价单,使用该订单发出时的收盘价成交该订单。这个只用于回测才有意义,实际上无法实施,因此称为cheat。很容易理解,我们知道收盘价了,当天(或者当前Bar)已经结束了咋成交?只能下一天(或bar)才能成交。为啥有这种需求呢?主要是用于回测的时候控制成交时间和价格。比如,我想测试一个股票一个月的收益,那最后一天提交订单,得第二天才能成交,这个很难控制具体回测周期
coo False Cheat-On-Open,含义同上,使用该订单发出时的开盘价成交该订单。
int2pnl True 每一次减仓(不管是多头还是空头)都会分别计算pnl。有些情况下没有必要,因为组合中,不同策略实施的操作,主要看最终效果,每一次的操作意义不大。
shortcash True 设置为True的话,股票类资产做空的时候,现金将会增加,但是资产的市值将会标记为负值,也就是将空头资产记为负债(本来就是借入的资产),市值越低越好。
设置为False的话则相反,现金会扣减作为本次做空的成本扣除,同时市值对应增加。建议使用缺省值,容易理解。
fundstartval 100 此参数控制以类似基金的方式衡量组合绩效的起始值,即:增加股票可以增加(做多)和减少现金(做空)。业绩不是用组合的资产净值来衡量的,而是用基金的价值来衡量的。
fundmode False 如果将其设置为True,则分析器(例如TimeReturn)可以根据价值而不是总资产净值自动计算收益。

Note:Backtrader提供 set_xxx 函数来设置参数 (cerebro.broker.set_xxx),其中 xxx 需要设置的参数的名称。

如前所述,BackBroker继承了元类,受MetaBase元类的控制。首先到MetaBase的__call__走一圈,也就是一整套动作(doprenew、donew、dopreinit、doinit和dopostinit),其机制前面几篇文章讲的比较多,这里就简要说下重点:

  • 在donew中,首先会走到MataParams中donew中进行参数到属性的映射,同时调用父类MetaBase的donew进行BackBroker的实例化。
  • 在doinit中,MetaBase直接就会调用BackBroker的__init__函数。这一块函数比较简单,就是
def __init__(self):
        super(BackBroker, self).__init__()
        self._userhist = []
        self._fundhist = []
        # share_value, net asset value
        self._fhistlast = [float('NaN'), float('NaN')]

首先调用的是父类的__init__函数,然后初始化内部使用的容器,父类就是BrokerBase,其父类__init__函数是:

def __init__(self):
        self.comminfo = dict()
        self.init()

主要就是调用init函数,这个self是谁,就是咱们之前初始化的BackBroker,然后又走到init函数:

def init(self):
        super(BackBroker, self).init()
        self.startingcash = self.cash = self.p.cash
        self._value = self.cash
        self._valuemkt = 0.0  # no open position

        self._valuelever = 0.0  # no open position
        self._valuemktlever = 0.0  # no open position

        self._leverage = 1.0  # initially nothing is open
        self._unrealized = 0.0  # no open position

        self.orders = list()  # will only be appending
        self.pending = collections.deque()  # popleft and append(right)
        self._toactivate = collections.deque()  # to activate in next cycle

        self.positions = collections.defaultdict(Position)
        self.d_credit = collections.defaultdict(float)  # credit per data
        self.notifs = collections.deque()

        self.submitted = collections.deque()

        # to keep dependent orders if needed
        self._pchildren = collections.defaultdict(collections.deque)

        self._ocos = dict()
        self._ocol = collections.defaultdict(list)

        self._fundval = self.p.fundstartval
        self._fundshares = self.p.cash / self._fundval
        self._cash_addition = collections.deque()

首先就是调用父类MetaBase的init函数,其主要作用就是记录佣金方案到comminfo中。

def init(self):
        # called from init and from start
        if None not in self.comminfo:
            self.comminfo = dict({None: self.p.commission})

初始化简单,主要就是初始化各种订单类型的队列(使用的是deque类,前面介绍过,这个是类似于list的容器,可以在队列头部和尾部添加、删除元素,可以快速高效地进行数据的操作)以及一些内部变量,计算现金、市值啥的。

BackBroker的启动

如前所述,BackBrocker是由Cerebro创建的(实例化),启动也是在runstrategies中。启动之前,还进行一些参数的设置:

def runstrategies(self, iterstrat, predata=False):
    ...
    if self.p.cheat_on_open and self.p.broker_coo:
        # try to activate in broker
        if hasattr(self._broker, 'set_coo'):
            self._broker.set_coo(True)

    if self._fhistory is not None:
        self._broker.set_fund_history(self._fhistory)

    for orders, onotify in self._ohistory:
        self._broker.add_order_history(orders, onotify)

    self._broker.start()

首先进行coo参数的设置,然后记录历史订单和历史资金信息,然后调用start函数:

def start(self):
        self.init()

大家有点奇怪吧,前面不是在创建的时候初始化了吗?为啥还要初始化?别忘了,runstrategies是针对一个策略调用一次(回头看看Cerebro代码解读),如果有多个策略,每次调用前都会初始化,保证每个策略不受前一策略的影响。

BackBroker中订单的执行过程。

在1.2节中,我们描述了创建订单的时候,Strategy进行买卖操作的时候,均调用的Broker提供的函数。Broker就是创建(实例化和初始化)一个Order(BuyOrder或者SellOrder)实例,并调用submit函数提交订单,下面看看后续对订单的处理:

    def submit(self, order, check=True):
        pref = self._take_children(order)
        if pref is None:  # order has not been taken
            return order

        pc = self._pchildren[pref]
        pc.append(order)  # store in parent/children queue

        if order.transmit:  # if single order, sent and queue cleared
            # if parent-child, the parent will be sent, the other kept
            rets = [self.transmit(x, check=check) for x in pc]
            return rets[-1]  # last one is the one triggering transmission

        return order

要点如下:

  • 2-4行:首先看看订单之前处理过没有,如果已经在处理中(在记录的队列中),直接返回。
  • 6-7行:是新订单,加入队列。
  • 9-12行:这里要判断是否独立的话单。transmit参数前面讲了,是Bracket订单使用,这种情况下,需要3个订单一起提交,避免分别触发执行。所以如果为False,那么函数直接跳出,不调用transmit函数提交了,而是等待最后一个订单(transmit=True)一起提交。对于普通订单,transmit的却设置是True,所以是立刻提交。 下面看transmit函数:
def transmit(self, order, check=True):
        if check and self.p.checksubmit:
            order.submit()
            self.submitted.append(order)
            self.orders.append(order)
            self.notify(order)
        else:
            self.submit_accept(order)

        return order

这里有个关键参数checksubmit,控制在下单前是否检查现金和保证金。:

  • 如果要检查,需要调用Order的submit函数,这个函数比较简单,就是修改订单状态为submitted,记录broker信息。

  • 然后,对应的队列中条件该订单,然后最关键的是要调用borker的notify函数,克隆该订单,并加入到notifs队列,后续next会检查这个队列。

    def notify(self, order):
            self.notifs.append(order.clone())
    
  • 如果不检查,那么调用submit_accept函数:

     order.pannotated = None
            order.submit()
            order.accept()
            self.pending.append(order)
            self.notify(order)
    

    这个和前面的处理差别就是提交之后,直接accept了(函数中设置订单的状态为Accepted),不加入提交队列。最后还是调用notify,也就是将订单加入到notifs队列。

继续下一步流程之前,你一定理解系统运行当前的位置。从前述可以知道,订单的操作是在Strategy的next中,也就是逐天(bar)进行数据处理过程中,可以回头再看看Strategy代码解读。那么BackBroker将订单加入到notifs队列中,当前处理就结束了,马上进入下一个next,具体就在Cerebro的_runonce/_runnext函数中,都会调用self._brokernotify函数(可以再看看Cerebro代码解读):

    def _brokernotify(self):
        '''
        Internal method which kicks the broker and delivers any broker
        notification to the strategy
        '''
        self._broker.next()
        while True:
            order = self._broker.get_notification()
            if order is None:
                break

            owner = order.owner
            if owner is None:
                owner = self.runningstrats[0]  # default

            owner._addnotification(order, quicknotify=self.p.quicknotify)

这里最关键的是6行broker的next函数,先看它:

def next(self):
    while self._toactivate:
        self._toactivate.popleft().activate()

    if self.p.checksubmit:
        self.check_submitted()
        ...

2,3行是对需要激活的订单进行处理,现在还没有,暂时忽略。如果checksubmit参数为True,就要调用check_submitted进行资金检查:

def check_submitted(self):
    cash = self.cash
    positions = dict()

    while self.submitted:
        order = self.submitted.popleft()

        if self._take_children(order) is None:  # children not taken
            continue

        comminfo = self.getcommissioninfo(order.data)

        position = positions.setdefault(
            order.data, self.positions[order.data].clone())

        # pseudo-execute the order to get the remaining cash after exec
        cash = self._execute(order, cash=cash, position=position)

        if cash >= 0.0:
            self.submit_accept(order)
            continue

        order.margin()
        self.notify(order)
        self._ococheck(order)
        self._bracketize(order, cancel=True)

这个函数关键就是取出提交的订单,然后获取对应的佣金方案,调用_execute函数进行伪执行。这个函数代码过于复杂,就不拿出来讲,其关键就是根据佣金,价格以及规模信息进行计算,看看执行完成后还能剩余多少现金。如果大于0的话,那么就调用submit_accept函数接受该订单,和checksubmit为False的场景站到同一起跑线了。

再回头到next函数,忽略一些细节代码,看看如下部分:

def next(self):
        ...
            else:
                self._try_exec(order)
                if order.alive():
                    self.pending.append(order)

                elif order.status == Order.Completed:
                    # a bracket parent order may have been executed
                    self._bracketize(order)
		...

这里最关键的就是_try_exec执行这个订单了:

def _try_exec(self, order):
        data = order.data
        popen = getattr(data, 'tick_open', None)
        if popen is None:
            popen = data.open[0]
        phigh = getattr(data, 'tick_high', None)
        if phigh is None:
            phigh = data.high[0]
        plow = getattr(data, 'tick_low', None)
        if plow is None:
            plow = data.low[0]
        pclose = getattr(data, 'tick_close', None)
        if pclose is None:
            pclose = data.close[0]

        pcreated = order.created.price
        plimit = order.created.pricelimit

        if order.exectype == Order.Market:
            self._try_exec_market(order, popen, phigh, plow)

        elif order.exectype == Order.Close:
            self._try_exec_close(order, pclose)

        elif order.exectype == Order.Limit:
            self._try_exec_limit(order, popen, phigh, plow, pcreated)

        elif (order.triggered and
              order.exectype in [Order.StopLimit, Order.StopTrailLimit]):
            self._try_exec_limit(order, popen, phigh, plow, plimit)

        elif order.exectype in [Order.Stop, Order.StopTrail]:
            self._try_exec_stop(order, popen, phigh, plow, pcreated, pclose)

        elif order.exectype in [Order.StopLimit, Order.StopTrailLimit]:
            self._try_exec_stoplimit(order,
                                     popen, phigh, plow, pclose,
                                     pcreated, plimit)

        elif order.exectype == Order.Historical:
            self._try_exec_historical(order)

可以看出,这里根据不同的执行类型对订单进行执行,以Market为例:

def _try_exec_market(self, order, popen, phigh, plow):
        ago = 0
        if self.p.coc and order.info.get('coc', True):
            dtcoc = order.created.dt
            exprice = order.created.pclose
        else:
            if not self.p.coo and order.data.datetime[0] <= order.created.dt:
                return    # can only execute after creation time

            dtcoc = None
            exprice = popen

        if order.isbuy():
            p = self._slip_up(phigh, exprice, doslip=self.p.slip_open)
        else:
            p = self._slip_down(plow, exprice, doslip=self.p.slip_open)

        self._execute(order, ago=0, price=p, dtcoc=dtcoc)

我们先忽略coc以及滑点(后面专题讲)的处理,可以看出,这里直接使用open价格去执行成交。Market(市价单)的逻辑是使用第二天(bar)的open价格匹配成交。_execute中主要就是基于价格以及佣金进行各种计算,太细节了,就不讲了。

后续还有一些特殊处理,均忽略,记住一点,订单的执行都是在下一个bar的处理过程,如果当前bar无法成交,则继续到下一个bar。对我们而言,比较重要的各种类型订单的成交机制,影响我们选择对应的策略,所以下面重点讲各种类型订单的作用以及在Backtrader中的成交机制。

BackBroker的订单类型

不同类型的订单,执行的方式是不一样的,我们需要弄清楚其中的机制,对我们后续策略的设计以及自动执行也有重要的作用。

市价单(Market)

什么是市价单,就是投资者不指定价格,而是愿意按市场的价格支付当前的订单。在BackBroker中,会取下一天(bar)的open价格匹配成交(这里要注意,如果是期货,相同时间内有更小的tick,这个也必须要求时间改变了才认为是下一个bar),代码前面讲过了。

市价单是市场上最常见和最直接的交易。它的目的是以当前要价尽快执行,并且在大多数情况下是买卖双方共同协商的选择。通常作为券商的默认方式。

市价单对于大盘股都是安全的选择,因为它们的流动性很高。也就是说,在交易日的任何特定时刻,他们的股票都会有大量换手,交易可立即完成。除非当时市场非常不稳定,波动剧烈,否则当我们点击“买入”或“卖出”时显示的价格与最终成交的价格几乎相同。

但是,当交易流动性较低的投资时,这些股票换手率低,买卖价差往往较大。因此,市价订单可能很难成交,或者成交价格和预期相差很大。由于这样的问题,通常会使用限价单。

收盘价单(Close)

这个也是市价单的一种,采用的是下订单的下一天(bar)的收盘价,而市价单的开盘价。其它和市价单的描述一致,不单独说明。

限价单(Limit)

限价单是一种以指定价格或更高价格购买或出售证券的订单。对于买入限价订单,订单将仅以限价或更低的价格执行,而对于卖出限价订单,订单将仅以限价或更高的价格执行。这种订单让让交易者更好地控制交易的价格。看下代码,这个 成交策略非常明了:

def _try_exec_limit(self, order, popen, phigh, plow, plimit):
        if order.isbuy():
            if plimit >= popen:
                # open smaller/equal than requested - buy cheaper
                pmax = min(phigh, plimit)
                p = self._slip_up(pmax, popen, doslip=self.p.slip_open,
                                  lim=True)
                self._execute(order, ago=0, price=p)
            elif plimit >= plow:
                # day low below req price ... match limit price
                self._execute(order, ago=0, price=plimit)

        else:  # Sell
            if plimit <= popen:
                # open greater/equal than requested - sell more expensive
                pmin = max(plow, plimit)
                p = self._slip_down(plimit, popen, doslip=self.p.slip_open,
                                    lim=True)
                self._execute(order, ago=0, price=p)
            elif plimit <= phigh:
                # day high above req price ... match limit price
                self._execute(order, ago=0, price=plimit)

简要说明如下:

  • 限价单对于做多(buy)和做空(Sell)处理有差异,先看对于做多(2-11行),如果限价高于开盘价,就直接取开盘价成交(忽略滑点函数,_slip_up函数对于不适用滑点情况直接返回open),如果限价大于最低价,那么就取限价成交(为啥不去最低价?因为实际中你无法抓到这个最低价,但是肯定可以以不高于limit价格成交)。这个处理的要点是确保以不高于限价的价格成交。
  • 对于做空(13-22行),如果限价高于开盘价,就直接取开盘价成交。如果限价低于开盘价,直接使用开盘价成交。如果限价低于最高价,则以限价成交(一样的道理,你无法扑捉到最高价)。这个处理的要点是确保以不低于限价的价格成交。

这种订单可以保证投资者支付该价格或更少。价格有保证,但是订单有可能不成交,除非目标证券的价格符合订单条件,否则限价单不会被执行。如果未达到指定价格,投资者可能错失交易机会。

这种订单和市场订单的差异是市场订单以现行市场价格执行交易,没有指定任何价格限制。

很多情况下建议使用限价单,例如当股票快速上涨或下跌时,交易者担心从市价单得到一个不希望的价格。此外,如果交易者不关注股票并且愿意以特定价格购买或出售该证券的,限价单也会有作用。限价单也可以在指定日期,在到期日保持开放状态,只要满足价格条件就成交。

通常限价单和止损单配合使用,防止出现超出预期的亏损。

止损单(STOP)

止损单是在证券价格超过特定价格的时候买入或卖出证券的订单,以确保尽可能了达到预定的进场或出场价格,限制投资者的损失或锁定利润。一旦价格超过预定义的进入或退出点,止损单就变成了市价单,按照市价单的逻辑成交。

同样的,止损单也分为做多和做空,处理逻辑相反:

    def _try_exec_stop(self, order, popen, phigh, plow, pcreated, pclose):
        if order.isbuy():
            if popen >= pcreated:
                # price penetrated with an open gap - use open
                p = self._slip_up(phigh, popen, doslip=self.p.slip_open)
                self._execute(order, ago=0, price=p)
            elif phigh >= pcreated:
                # price penetrated during the session - use trigger price
                p = self._slip_up(phigh, pcreated)
                self._execute(order, ago=0, price=p)

        else:  # Sell
            if popen <= pcreated:
                # price penetrated with an open gap - use open
                p = self._slip_down(plow, popen, doslip=self.p.slip_open)
                self._execute(order, ago=0, price=p)
            elif plow <= pcreated:
                # price penetrated during the session - use trigger price
                p = self._slip_down(plow, pcreated)
                self._execute(order, ago=0, price=p)

        # not (completely) executed and trailing stop
        if order.alive() and order.exectype == Order.StopTrail:
            order.trailadjust(pclose)

要点:

  • 对于做多(buy),如果当前开盘价大于订单创建时设定的价格,那么,使用开盘价成交。如果当前bar的最高价大于设定价格,使用设定价格成交。这里有点绕,为啥价格高了还买?因为这是止损单,buy操作通常对应的是空单,对于空单而言,价格越高损失越大,因此,在超过指定价格之后,立即以最可能成交的方式成交,完成对空单的平仓。当然也可以仅仅作为多头仓位,自己的心理价位作为限价,超过心理价位就不成交。
  • 对于做空(sell),和上述逻辑相反。对应的是多单,价格越低损失越大,因此,低于指定价格之后,立即以可能成交的方式成交,完成对多单的平仓。
  • 对于跟踪止损单(StopTrail),后面专门再讨论。

实际上,还有一种止盈单,就是对于多单,价格如果超过设定价格就sell,锁定一定的利润。对于空单,价格如果低于设定价格,就买入平仓,也达到锁定利润的效果。但是看代码,这个函数无法完成止盈操作,可以考虑使用限价单和止损单一起使用。

使用技术分析的交易者会在主要移动平均线、趋势线、摆动高点、摆动低点或其他关键支撑位或阻力位下方放置止损单,在趋势形成的时候进行止损操作。

止损单也有风险,比如市场的一个波动,可能意外触及止损点并激活订单时,可能会导致本可以盈利或更多盈利的交易出现亏损,也就是所谓的卖飞了。

止损限价单(StopLimit)

这种订单是止损单和限价单的综合,其要点是:

  1. 到达止损点的时候触发。
  2. 触发后按照限价单的逻辑成交。

代码就不贴了,根据上述要点,结合咨询单和限价单的代码很容易理解。

在使用止损限价单的时候,需要设定两个参数:

止损:交易指定目标止损价格,对应Backtrader中订单的price参数

限价:交易指定限价对应Backtrader中订单的plimit参数。

还必须设置一个时间点,也就是Backtrader中订单的valid参数。

止损限价单的主要好处是交易者可以精确控制何时应执行订单。

止损限价单也有不利之处,所有限价单一样,如果证券在指定时间段内未达到止损价格,则不能保证交易会执行。

止损限价单将在达到给定止损价格后以指定价格或更好的价格执行。一旦达到止损价,止损限价单就成为限价单,以限价或更好的价格买入或卖出。这种类型的订单使用比较广泛。

这里给个例子说明:例如,假设 某股票的交易价格为 155 元,并且一旦该股票开始显示出某种明显的上涨势头,投资者就想购买该股票,就可以下达止损限价单,止损价为 160 元,限价为 165 元。如果 股票的价格高于 160 元的止损价,则订单被激活并变为限价订单。只要订单可以在 165 元(即限价)以下成交,交易就会被成交。如果价格高于 165 元,则订单将不会被执行。

跟踪止损单(StopTrial)

这种订单也是对止损单的一种优化,也需要券商的支持。目前盈透证券支持OCO以及StopTrail。据说还有一个StopTrailLimit,越来越复杂,可以看出,订单的执行方式对于交易系统非常重要。

但是呢?咱们Backtrader的模拟中没有实现,只在真实的盈透证券(ibbroker)中使用,咱们用不上。

        # not (completely) executed and trailing stop
        if order.alive() and order.exectype == Order.StopTrail:
            order.trailadjust(pclose)
    
    def trailadjust(self, price):
        pass  # generic interface

虽然咱们用不上,但是了解下跟踪止损单也是有好处的。只需要简单了解下,具体用法就不讲了。

前面说了,追踪止损是普通止损单的优化,可以将止损价格设置为当前市场价格上下浮动范围,浮动范围可以设置为百分比或者绝对值。对于多头头寸,我们可以将追踪止损设置在当前市场价格下方。对于空头头寸,可以将追踪止损设置在当前市场价格之上。可以看出,追踪止损比固定止损更灵活,因为它会自动跟踪股票的价格方向,并且不需要像固定止损那样手动重置。

下面给出一个例子说明这种订单的使用方法:

假设您以 1,000 元的价格 购买了某股票。通过查看股票的先前上涨,您会发现价格通常会在再次走高之前经历 5% 到 8%的回调。这些先前的变动可以帮助建立用于追踪止损的百分比水平。

选择 3%,甚至 5%,可能太小了,一个很小的回调就会把你振出局。

选择 20% 的追踪止损是又太大了。根据最近的趋势,平均回调约为 6%,较大的回调接近 8%。更好的追踪止损是 10% 到 12%。这给了交易者一定的回调空间,但如果价格下跌超过 12%,交易者也可以迅速退出。12% 的跌幅大于典型的回调,这意味着可能会发生 趋势逆转,而不仅仅是回调。

假设 10% 的追踪止损,如果价格下跌 10% 低于购买价格,券商将执行卖单,也就是 900 元。如果购买后价格从未超过 1,000 元,那么止损将保持在 900 元。如果价格达到 1,010 元,止损将升至 909 元,比 1,010 元低 10%。如果股票上涨至 1250 元,如果价格跌至 1,125 元,券商就执行卖出指令。如果价格从 1,250 元开始下跌并且没有回升,追踪止损订单将保持在 1,125 元。总结一点就是追踪止损价格跟随价格上涨,但如果价格开始下跌,则保持不变,以潜在地获得利润。

上面的例子是对于多单,如果是空单,正好相反。

虽然国内券商没有这种订单类型(据我所知),但是有些券商支持条件单可以达到相同效果,例如回落卖出,就是上涨途中,下调一定比例(或者绝对值)卖出。

滑点(Slippage)

滑点是指交易的预期价格与交易实际执行价格之间的差异。滑点可能随时发生,在使用市价单时市场波动较大的时期最为普遍。当执行大订单但所选价格没有足够的交易量来维持当前的买卖差价时,也会发生这种情况。

由于滑点在真实市场普遍存在,如果Backtrader不考虑这个这个情况,那么回测和实际情况可能相差很大(在量化交易的成本模型中,滑点也作为一个重要的成本进行考虑),基于回测的策略可能就不适用于真实市场,为了尽可能地保证模拟现实情况,Backtrader提供了滑点功能。

BackBroker滑点包含如下可以设置的参数(设置方法参见2.2节参数说明):

参数 缺省值 说明
slip_perc 0.0 设定允许变动(滑动)范围,以百分比实际值表示,例如:0.01指的是1%,同样的0.001指的是0.1%。
slip_fixed 0.0 同上,以百分比值(单位是%)表示,例如0.01指的是0.01%。注意,slip_perc优先级高。
slip_open False 是否对以下一bar的open价格执行的订单使用滑点,例如市价单。还包括其他所有需要以开盘价匹配成交的情况。
slip_match True 设置为True,BackBroker会保证滑点后的价格在high和low之间,不能超出。
设置为False,BackBroker不会进行检查,直接使用当前的价格,并在下一个循环成交。
slip_limit True 对于限价单,即便slip_match设置为false,还是会按照为True处理。
slip_out False 即便价格不在high-low之间。

关于代码,大家可以参见之前各种类型订单的执行逻辑代码中,都有对于滑点的处理。老实说,这个对于咱们个人交易者,作用实在不大,咱们那点钱,还不至于能影响市场,滑点可能性比较小。代码就不详细描述了。

如果要使用的话,也可以考虑一点滑点,设置方法如下,其他参数缺省就好:

cerebro = bt.Cerebro()
cerebro.broker = bt.brokers.BackBroker(slip_perc=0.005)  # 0.5%

一些辅助交易类

头寸(Position)

在交易过程中,我们的Strategy通常要获知头寸情况,Backtrader提供了Position类要记录相关信息。这个类比较简单,代码就不讲了。

几个关键点:

  1. 针对每一个Data(也就是资产/证券)有不同的Position记录持仓情况。
  2. 策略中可以直接访问position(装饰器)或者getposition(data=None, broker=None)函数获取,不指定data和broker的话,缺省就返回第一个数据(data0)在缺省broker中的头寸情况。
  3. Position两个重要的信息:size,头寸的规模。price,头寸的平均价格。
  4. Position通常用于一种状态,用来决定是否发起订单。比如,对于多头仓位只有在没有头寸的情况下才进入。参见我们之前代码执行买入操作之前会判断self.position.

交易(Trade)

交易应该是人类最古老的行为了。什么才能叫交易?一买一卖成交了才能称为交易。从投资的角度来看,交易有两个重要的状态:

  1. 未平仓状态(open),或者叫开仓状态:你拥有的某证券的头寸从0到X,这种情况就处于未平仓状态。如果x是正数,对应多头头寸,小于0就是空头头寸。你拥有的这些头寸,都处于未平仓状态。要注意,你的这些未平仓头寸代表你投资的市场敞口,风险会一直存在,直到头寸清仓。根据投资者风格和目标,未平仓头寸可以持有数分钟(短线投资)到数年(长线投资)。
  2. 平仓状态(closed):就是你拥有的头寸从X变为0,交易就处于平仓状态。平仓可以是买入平仓(对应空头头寸)和卖出平仓(对应多头头寸)。

当然还可能存在如下两种情况:

  1. 头寸从正数变为负数。
  2. 头寸从负数变为正数。

这两种情况可以看做分为了两部:

  1. 先平仓一个交易(头寸从X变为0)
  2. 然后开仓一个交易(头寸从0变为Y)

还记得咱们咋使用Trade的?在Strategy代码解读中,咱们说过,可以每次交易后记录交易的相关信息。由于Trade类也比较简单,不准备进行代价解读了,我们重点看看Trade提供的我们需要了解的信息:

属性 含义
status 当前状态,一个三种:Created(这个类初始化的状态)、open 和closed。
tradeid Order创建时传递的ID,用于识别不同的交易。
size 当前交易的规模
price 当前交易的价格
value 当前交易的金额
commission 累积的佣金
pnl 毛利
pnlcomm 减去佣金的净利润
isclosed 是否平仓.
isopen 是否处于未平仓状态
justopened 是否刚开仓
baropen 在哪个bar开仓
dtopen 开仓的时间
barclose 哪个bar平仓
dtclose 平仓时间
barlen 未平仓状态持续了多少bar
historyon 是否记录历史信息
history 保存了历史更新信息。最开始是开仓,然后中途会有不断的更新(仓位变动),最后是平仓。

佣金(CommInfoBase)

佣金方案的基本信息

佣金是交易系统中成本模型的一部分。不过,这种成本是固定且可预计的,因此较为简单,只要根据和券商的协议填写清楚即可,由于这个类过程并不复杂,不解读代码了,看几个关键点。

看过之前Cerebro代码就会知道,咱们在实例化Cerebro之后,就可以直接设置佣金方案了,如下:

    cerebro.broker.setcommission(commission=0.001)#设定交易费用(买卖都收)

Cerebro获取broker(broker的实例化和初始化在2.1节描述),然后调用setcommission设置佣金:

def setcommission(self,
                      commission=0.0, margin=None, mult=1.0,
                      commtype=None, percabs=True, stocklike=False,
                      interest=0.0, interest_long=False, leverage=1.0,
                      automargin=False,
                      name=None):

        comm = CommInfoBase(commission=commission, margin=margin, mult=mult,
                            commtype=commtype, stocklike=stocklike,
                            percabs=percabs,
                            interest=interest, interest_long=interest_long,
                            leverage=leverage, automargin=automargin)
        self.comminfo[name] = comm

Broker首先会实例化一个CommInfoBase,然后记录到commoninfo字典中,这里要注意,一个Broker可以拥有多个CommInfoBase,通过name来区分,name和Data类创建时候使用的名字一致,也就是不同的数据源(对应不同的资产)佣金方案可以不同。在相同的券商中,针对不同的资产不同的佣金,例如对于股票和ETF佣金不同,这里就可以设置不同佣金方案。

CommInfoBase就是设置相关参数,关键参数具体解释如下表:

参数 缺省值 含义
commission 0.0 就是基本佣金数据,按照百分比或者绝对值。百分比就是按照交易的金额(size*price)的百分比提取佣金。绝对值,就是针对size提取固定佣金。比如一笔交易(或者一个合约)收取2元钱。至于具体是采取百分比还是固定值,参见下面参数说明。
commtype None 就是用来指定佣金数据是百分比还是固定值。
ommInfoBase.COMM_PERC:按照百分比解释。
CommInfoBase.COMM_FIXED:按照固定值解释。
None:这种情况下系统根据资产情况自动判决,判决方法参见参数margin。为啥搞得这么复杂,主要是为了兼容原来老的类(CommissionInfo),原来是通过margin来判断是否百分比。
percabs False 当 commtype 设置为 COMM_PERC 时,指定百分比数字的填写方法。比如说,你要设置0.1%,这个值设置为True,那么commission参数填写为0.001,为False的话,只要填写为0.1.为啥搞得怎么麻烦?还是兼容性问题。因为老版本中commission填写值不包括百分号,还记得咱们之前的例子要求“0.1% ,需要除以100然后去掉%”,这个就是老的处理方式,在新版本中,如果保持老的处理方式,percaps需要填写为TRUE。
stocklike False 这里用来指示股票类资产还是期权类资产,在老的CommissionInfo,是通过margin来确定是否期权。在新的类中,当commtype为None的时候,可以通过这个参数来指示是否股票类资产。
margin None 保证金,港股称为孖展。Margin为0或者None,那么commission就是按照百分比解释。否则按照固定值解释。主要是因为Margin主要针对的期货/期权产品,通常按合约计价。这个是之前类的使用方法,新版本不再采用。
mult 1.0 杠杆比,这个主要应用于期权等可以加杠杆的资产,Backtrader据此计算盈利和亏损。
name 前面已叙及。
interest 0.0 利息。有些情况下,需要对所持资产计算利息,例如借入证券卖空,由于这些对于我们个人交易者使用不多,咱们先忽略。
interest_long False 是否对多头仓位收取利息,和上面一样,是否对做多的资产收取利息,先忽略。

系统佣金方案还有缺点,从代码来看,有些常用的场景就无法支持,比如常用的一笔交易最小5块,这个咋设置,还有除了佣金费用之外,港股/美股还需要平台费,也没法模拟。这就需要我们进行定制。

佣金方案的开发

如前所述,在不同国家,甚至不同券商,都可能有不同的佣金方案,Backtrader提供了定制佣金方案,而且由于采用了面向对象的机制,所以定制方案也很很简单。

如何定制呢?首先,继承CommInfoBase类。然后根据你的需求修改参数和重载函数。

先说简单的自定义方法,修改参数。比如,对于股票和期权,参数不一样,那么,我们可以分别定义参数重写针对股票和期权的佣金方案。

比如,对于期权,我们定义固定值的佣金方案:

class CommInfo_Futures_Fixed(CommInfoBase):
    params = (
        ('stocklike', False),
        ('commtype', CommInfoBase.COMM_FIXED),
    )

这样的话,使用的时候只需要输入commision就行了,其他参数就不要关注了。

同样的,也可以定义百分比的股票佣金方案:

class CommInfo_Stocks_Perc(CommInfoBase):
    params = (
        ('stocklike', True),
        ('commtype', CommInfoBase.COMM_PERC),
    )

假如你希望按照老方法输入0.001表示0.1%,那么还可以这样定义:

class CommInfo_Stocks_PercAbs(CommInfoBase):
    params = (
        ('stocklike', True),
        ('commtype', CommInfoBase.COMM_PERC),
        ('percabs', True),
    )

如果你还需要修改佣金的计算方法,那么你可以重载_getcommission 函数,定义如下,这个后面会给你一个详细的实例。

def _getcommission(self, size, price, pseudoexec):
   '''根据规模以及价格计算佣金
      '''

定义好佣金方案之后,下一步就是通过Cerebro调用broker.addcommissioninfo添加这个方案。注意不是使用setcommission,因为这个函数只是针对老把能的CommissionInfo对象。如下示例:

...

comminfo = CommInfo_Stocks_PercAbs(commission=0.001)  # 0.1%
cerebro.broker.addcommissioninfo(comminfo)

addcommissioninfo函数定义如下:

  def addcommissioninfo(self, comminfo, name=None):
        '''Adds a ``CommissionInfo`` object that will be the default for all assets if
        ``name`` is ``None``'''
        self.comminfo[name] = comminfo

注意,可以设定佣金方案对应的资产,通过data的name来指定。

好了,来一个实战的案例,比如在咱们国内,佣金方案可能如下:

  1. 佣金按照百分比。
  2. 每一笔交易有一个最低值,比如5块,当然有些券商可能会免5.
  3. 卖出股票还需要收印花税。
  4. 可能有的平台还需要收平台费。

按照这个需求,可以定义佣金方案如下:

class MyStockCommissionScheme(bt.CommInfoBase):
    '''
    1.佣金按照百分比。
    2.每一笔交易有一个最低值,比如5块,当然有些券商可能会免5.
    3.卖出股票还需要收印花税。
    4.可能有的平台还需要收平台费。  
    '''
    params = (
        ('stampduty', 0.005),  # 印花税率
        ('commission', 0.005),  # 佣金率
        ('stocklike', True),#股票类资产,不考虑保证金
        ('commtype', bt.CommInfoBase.COMM_PERC),#按百分比
        ('minCommission', 5),#最小佣金
        ('platFee', 0),#平台费用
    )

    def _getcommission(self, size, price, pseudoexec):
        '''
        size>0,买入操作。
        size<0,卖出操作。
        '''
        if size > 0:  # 买入,不考虑印花税,需要考虑最低收费
            return max(size * price * self.p.commission,self.p.minCommission)+platFee
        elif size < 0:  # 卖出,考虑印花税。
            return max(abs(size) * price * self.p.commission,self.p.minCommission)+abs(size) * price * self.p.stampduty+platFee
        else:
            return 0  # 防止特殊情况下size为0.

注释很清楚,就不解释了。

有的同学眼尖,_getcommission看到还有一个参数pseudoexec,这个干嘛的? 这个参数用于指示当前的调用是否用于真正的订单执行过程。那么怎么还会有不是订单的执行过程呢?大家还记得前面订单有一个参数checksubmit,这个参数就是要求在提交订单前检查现金是否足够,这个检查过程中也会调用_getcommission计算佣金,那么,在一次买/卖交易中,可能多次计算佣金。为了区分哪一次调用是真正的为了订单实际执行的调用,就需要使用这个参数。

区分订单实际执行有啥作用呢?比如这样一个场景,你和券商商量好了,如果交易量(规模)超过5000,就给我佣金50%的折扣。这交易量的计算就得使用这个参数来区分,否则的话,系统将一些并未执行的佣金计算也计入的话,券商也不认啊。

下面给个例子(官网)来看看。

import backtrader as bt

class CommInfo_Fut_Discount(bt.CommInfoBase):
    params = (
      ('stocklike', False),  # 期权类资产
      ('commtype', bt.CommInfoBase.COMM_FIXED),  # 固定佣金

      # 优惠方案参数
      ('discount_volume', 5000),  # 优惠所需最小合约数量
      ('discount_perc', 50.0),  # 50.0% 的折扣
    )

    negotiated_volume = 0  # 跟踪实际交易合约规模的参数

    def _getcommission(self, size, price, pseudoexec):
        if self.negotiated_volume > self.p.discount_volume:#超过最小次数,这设置折扣50%
           actual_discount = self.p.discount_perc / 100.0
        else:
           actual_discount = 0.0#没有超过,那么没有折扣

        commission = self.p.commission * (1.0 - actual_discount)#打折后的实际佣金
        commvalue = size * price * commission#计算折扣后的佣金。这里的size应该是abs(size),不然有负数。

        if not pseudoexec:
           # 跟踪实际交易合约规模
           self.negotiated_volume += size #如果实际执行(非伪执行),这需要累加本次实际交易的规模。

        return commvalue

通过上述例子,应该就很容易理解pseudoexec参数的使用了。

规模(sizer)

还有一个交易的类是sizer,就是一次买卖操作的规模,也可称之为赌注大小。比如你看多一个股票,那么你准备下多大赌注?在股票交易中,通常的单位是手。一手多少股呢?国内通常是100股,港股也有几十的。一手大概是多少钱?不同股票不同,比如伯克希尔哈撒韦,一手就得几百万吧。但是注意Backtrader中,单位是股。

赌注的大小在量化交易中非常重要,如何决定赌注的大小也有很多技巧,咱们后续在量化系统中再详细研究,咱们先看Backtrader中如何使用sizer。

首先看Backtrader的Sizer如何使用。

Strategy中使用

在咱们Strategy代码解读中,Strategy预初始化中,会创建一个缺省的FixedSize:

    def dopreinit(cls, _obj, *args, **kwargs):
        _obj, args, kwargs = \
            super(MetaStrategy, cls).dopreinit(_obj, *args, **kwargs)
        _obj.broker = _obj.env.broker
        _obj._sizer = bt.sizers.FixedSize()

缺省的FixedSize是啥样呢?

class FixedSize(bt.Sizer):
     params = (('stake', 1),
              ('tranches', 1))

缺省就是1股。tranches这个请忽略,是复杂资产的分片,咱们用不上。

在strategy中我们也可以通过setSizer函数替换这个缺省的值

 def setsizer(self, sizer):
        '''
        Replace the default (fixed stake) sizer
        '''
        self._sizer = sizer
        sizer.set(self, self.broker)
        return sizer

当然也可以通过getsizer函数获取当前使用的sizer实例。可以看出,在Strategy中,sizer 是可以直接获取/设置的属性。

另外,我们在定制Strategy的时候,可以通过参数设置sizer,参考如下官网代码:

class MyStrategy(bt.Strategy):
    params = (('sizer', None),)

    def __init__(self):
        if self.p.sizer is not None:
            self.sizer = self.p.sizer

这样的话,我们可以调用 Cerebro 的同时创建一个 Sizer,并将其作为参数传递给系统中的所有策略,也就是其他的策略可以共享这个sizer。

Cerebro中使用

同样的,我们也可以在Cerebro中设置sizer,我们之前的实例都是采取这种方法。

在Cerebro中,可以通过addsizer和addsizer_byidx函数添加sizer。

addsizer

这个函数添加一个缺省的sizer,适用于任何添加到当前Cerebro的Strategy,如下代码所示:

cerebro = bt.Cerebro()
cerebro.addsizer(bt.sizers.SizerFix, stake=20)  # 应用于所有策略的缺省sizer

addsizer_byidx

这个函数可以针对特定strategy添加sizer,Strategy通过idx来指定。idx是addstrategy的返回值。如下代码所示:

cerebro = bt.Cerebro()
cerebro.addsizer(bt.sizers.SizerFix, stake=20)  # 应用于所有策略的缺省sizer

idx = cerebro.addstrategy(MyStrategy, myparam=myvalue)
cerebro.addsizer_byidx(idx, bt.sizers.SizerFix, stake=5)

cerebro.addstrategy(MyOtherStrategy)

这一段代码中,增加了一个缺省的sizer,规模为20.同时为MyStrategy策略增加了一个规模为5的sizer。对于MyOtherStrategy,没有指定特殊的sizer,就使用缺省的sizer。

sizer的开发

前面咱们讲过,sizer在量化交易中非常重要,可能需要针对不同情况决定不同的sizer,所以,咱们还需要介绍如何定制开发sizer。说明下,以下示例来自官网。

感谢面向对象的机制,自定义sizer并不复杂,步骤如下:

首先继承backtrader.Sizer。通过这个类,可以访问执行买卖操作的到strategy和broker,然后可以获取相应信息决定size大小。通过broker我们可以获取如下信息:

  • 数据(也就是对应的资产)的头寸:self.strategy.getposition(data)。
  • 投资组合的市值:self.broker.getvalue(),也可以通过self.strategy.broker.getvalue()获取。

还有一些信息通过如下函数的接口获取。

然后重写_getsizing(self, comminfo, cash, data, isbuy)函数,这个函数接口入参信息如下:

  • comminfo:就是佣金的实例,包含有关数据(资产)佣金的信息,并允许计算头寸价值、操作(买卖)成本、操作佣金等。
  • cash:当前broker中的现金。
  • data:操作的目标数据(对应资产)。
  • isbuy:是买入操作(Ture)还是卖出操作(False)。

该方法返回操作的规模信息。注意返回的正负号无意义,即:如果操作是卖出操作(isbuy 为 False),则该方法可能返回 5 或 -5。卖出操作只会使用绝对值5。

大家可能不太理解,一个size计算需要这么多信息干啥?实际上,在量化自动化交易中,每一个下注(买卖操作)的大小直接影响收益。比如我们策略,收到第一个做多信号,买入20%,收到第二个,再买40%,收到第三个信号,全部买入。我们就可以在Strategy中记录做多信号次数,然后在自定义sizer中访问strategy的这个变量(做多信号次数),根据现金金额的比例计算出购买的规模,每一次返回的规模不一样,这样我们将这种下注的方式自定义sizer,后续专门用于采用这种下注方式的策略。

下面我们看看Backrtrader的FixedSize代码实例:

import backtrader as bt

class FixedSize(bt.Sizer):
    params = (('stake', 1),)

    def _getsizing(self, comminfo, cash, data, isbuy):
        return self.params.stake

这个固定规模就比较简单,没有任何计算,直接返回参数设置的数量。

下面看一个复杂点的FixedRerverser。

class FixedRerverser(bt.FixedSize):

    def _getsizing(self, comminfo, cash, data, isbuy):
        position = self.broker.getposition(data)
        size = self.p.stake * (1 + (position.size != 0))
        return size

这个类继承FixedSize的参数,并且完成:

  • 从broker获取当前资产的头寸。
  • 根据头寸的size决定是否将固定大小的stake翻倍。

这个用于一些特殊的处理,比如可以直接将多头头寸改为空头头寸。

下面看一个实际应用案例(官网提供)。

一个sizer的实际案例

在不改变strategy算法的情况下,我们可以通过修改sizer来实现只做多交易,替代多空交易策略。

比如如下一个策略:

class CloseSMA(bt.Strategy):
    params = (('period', 15),)

    def __init__(self):
        sma = bt.indicators.SMA(self.data, period=self.p.period)
        self.crossover = bt.indicators.CrossOver(self.data, sma)

    def next(self):
        if self.crossover > 0:
            self.buy()

        elif self.crossover < 0:
            self.sell()

这个就是我们之前多次使用的均线策略,这个策略中,只会根据crossover信号进行多空操作,而不管当前的头寸情况。而通过sizer就可以改变这个策略。

class LongOnly(bt.Sizer):
    params = (('stake', 1),)

    def _getsizing(self, comminfo, cash, data, isbuy):
      if isbuy:
          return self.p.stake

      # Sell situation
      position = self.broker.getposition(data)
      if not position.size:
          return 0  # do not sell if nothing is open

      return self.p.stake

在这个sizer中,只有在持有头寸的情况下,才会卖出。当然这里没有判断持有头寸的大小,我们可以优化,如果stake大于持有头寸,只卖出当前头寸。

通过这个示例,我们可以看出,通过sizer可以改变策略。其实大家已可以看出,咱们也可以在strategy中直接计算,比如前面1.5节目标订单中,在strategy中可以自己计算出size完成策略的需要。至于在哪里做比较合适,还是看大家的喜好。

几个常用的sizer

Backtrader提供了几个常用的sizer。

FixedSize

这个是最常用的sizer,前面咱们也讲过,它只会返回一个规定的大小

FixedReverser

这个代码前面讲过,就是返回需要一个固定大小,反转一个头寸或固定大小开一个。

PercentSizer

这个代码看下:

class PercentSizer(bt.Sizer):
    params = (
        ('percents', 20),
        ('retint', False),  #返回整数,而不是浮点数。
    )

    def __init__(self):
        pass

    def _getsizing(self, comminfo, cash, data, isbuy):
        position = self.broker.getposition(data)
        if not position:
            size = cash / data.close[0] * (self.params.percents / 100)
        else:
            size = position.size

        if self.p.retint:
            size = int(size)

        return size

看_getsizing函数:

  • 首先获取当前资产的头寸,如果没有的话,就拿参数中百分比的现金计算可以购买(以当前的收盘价,其实也就是估算)规模。如果有头寸的话,直接返回持有头寸的规模。注意,通常在买入操作下,没有头寸。已有头寸,针对的是卖出。
  • 然后还可以根据参数决定是返回整数还是浮点数。

AllInSizer

赌桌上,“all in",这个气势!但是投资的时候 all in是一种病,得治。记住永远留有后手。

回到Backtrader中,AllInSizer是PercentSizer子类,参数percents为100.

还提供了两个返回整数的对应类:PercentSizerIntAllInSizerInt

总结

本文重点描述了Backtrader中交易相关的类,这些类将来会作为咱们量化系统交易模型的建模基础。

后续咱们再继续介绍可视化模块、评估模块以及自动运行相关代码。

你可能感兴趣的:(backtrader,量化投资,python,开发语言,后端)