【Backtrader】Guidance 详探(二)

927 阅读8分钟

前言

篇幅关系,详探系列会分成几篇来写,前篇《【Backtrader】Guidance 详探(一)》(以下简称「详探一」)写到了【Our First Strategy】这个 Demo,本篇继续。

正文

Demo 的代码就不往上贴了,所以以下内容请对照 Quickstart 文档 中的代码同步阅读。

Adding some Logic to the Strategy

书接上文,《详探一》中说过,策略当中最重要的就是那个 next 方法,几乎所有策略逻辑都会写在这。果然,此 Demo 在 TestStrategy 的 next 方法中加入了以下代码:

if self.dataclose[0] < self.dataclose[-1]:
    # current close less than previous close

    if self.dataclose[-1] < self.dataclose[-2]:
        # previous close less than the previous close

        # BUY, BUY, BUY!!! (with all possible default parameters)
        self.log('BUY CREATE, %.2f' % self.dataclose[0])
        self.buy()

代码逻辑比较好理解,如果收盘价满足:今天 < 昨天,昨天 < 前天,也就是「3 连跌」,那么就执行买入操作,并打印 log。关键点就在这个 self.buy() 上了,从最终打印的 cash 结果来看,每次 buy 肯定是扣了钱了。单价比较好猜,应该就是收盘价了,但是如何做到实时交易呢?如何控制买入多少股呢?我们查看一下文档,可以查到 buy/sell 有很多参数。

buy(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)

比如,如果想买入 10 股,那么就写 self.buy(size=10) 即可。这里实际上有个 order 的概念,每一个买卖都是一个 order。真正操作过股票交易的同学应该知道,可以下定价单、实时交易单等各种类型的 order,所以这么多参数都是为了实现这些订单功能设计的,更多信息我们下文继续说。

Do not only buy … but SELL

这里面提到了,在调用 buy 和 sell 时,只是「创建了一个 order」,并没有执行。只有状态是 in the market 的时候,买单和卖单才会被执行。有此知识背景,我们来看下新增的代码:

    def __init__(self):
        # Keep a reference to the "close" line in the data[0] dataseries
        self.dataclose = self.datas[0].close

        # To keep track of pending orders
        self.order = None

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            # Buy/Sell order submitted/accepted to/by broker - Nothing to do
            return

        # Check if an order has been completed
        # Attention: broker could reject order if not enough cash
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log('BUY EXECUTED, %.2f' % order.executed.price)
            elif order.issell():
                self.log('SELL EXECUTED, %.2f' % order.executed.price)

            self.bar_executed = len(self)

        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log('Order Canceled/Margin/Rejected')
            
        # Write down: no pending order
        self.order = None
        
    def next(self):
        # Simply log the closing price of the series from the reference
        self.log('Close, %.2f' % self.dataclose[0])
        # Check if an order is pending ... if yes, we cannot send a 2nd one
        if self.order:
            return
        # Check if we are in the market
        if not self.position:
            # Not yet ... we MIGHT BUY if ...
            if self.dataclose[0] < self.dataclose[-1]:
                    # current close less than previous close
                    if self.dataclose[-1] < self.dataclose[-2]:
                        # previous close less than the previous close
                        # BUY, BUY, BUY!!! (with default parameters)
                        self.log('BUY CREATE, %.2f' % self.dataclose[0])
                        # Keep track of the created order to avoid a 2nd order
                        self.order = self.buy()
        else:
            # Already in the market ... we might sell
            if len(self) >= (self.bar_executed + 5):
                # SELL, SELL, SELL!!! (with all possible default parameters)
                self.log('SELL CREATE, %.2f' % self.dataclose[0])
                # Keep track of the created order to avoid a 2nd order
                self.order = self.sell()

__init__ 中新增了一行 self.order = None,会在 notify_ordernext 中用到,我们先来重点解析一下 notify_order

notify_order

我们发现这个方法在代码中没有被主动调用,从 文档 中可以知道,当 order 的状态变化时,该函数会自动触发,可以理解为 order 生命周期的钩子函数。order 的状态如下,就不翻译了:

StatusExplain
Createdset when the Order instance is created. Never to be seen by end-users unless order instances are manually created rather than through buysell and close
Submittedset when the order instance has been transmitted to the broker. This simply means it has been sent. In backtesting mode this will be an immediate action, but it may take actual time with a real broker, which may receive the order and only first notify when it has been forwarded to an exchange
Acceptedthe broker has taken the order and it is in the system (or already in a exchange) awaiting execution according to the set parameters like execution type, size, price and validity
Partialthe order has been partially executed. order.executed contains the current filled size and average price. order.executed.exbits contains a complete list of ExecutionBits detailing the partial fillings
Completethe order has been completely filled average price.
Rejectedthe broker has rejected the order. A parameter (like for example valid to determine its lifetime) may not be accepted by the broker and the order cannot be accepted.
Marginthe order execution would imply a margin call and the previously accepted order has been taken off the system
Cancelledconfirmation of the user requested cancellation. It must be taken into account that a request to cancel an order via the cancel method of the strategy is no guarantee of cancellation. The order may have been already executed but such execution may not have yet notified by the broker and/or the notification may not have yet been delivered to the strategy
Expireda previously accepted order which had a time validity has expired and been taken off the system

代码中还有 isbuyissell 方法,它们都返回一个 boolean 值,来标明当前是买单还是卖单,另外看文档还有一个 alive,是判断 order 是否处于 PartialAccepted 状态。

然后就是 order.executed.xxx 属性,也就是成交时的各种数据,比如 price 等,这个查看文档中的 OrderData 说明即可。

最后,比较难理解的是 self.bar_executed = len(self) 这句,实际上就是记录一下当前一共已经遍历了几个 line,每次成交就记录一下,目的就是为了实现「买完之后过 5 个 line(天)卖」。不过更细节一点的,在这个 Demo 中,理论上应该只有在 buy 的时候才需要记录,经过笔者测试也的确是这样,把这行代码移到 isbuy 的判断分支里,结果与当前代码相同。

最后一句 self.order = None 会在 order 状态不是 SubmittedAccepted 的时候触发,所以算是一个「开关」,大体上就是防止 order 在处于「未完成」状态时多次进行重复的 buy 和 sell 的操作。

notify_order 解析的差不多了,里面绝大多数就是 if-else 的逻辑,最主要还是打印 log。另外就是改变一些后续运算中需要用到的变量值,比如 self.bar_executedself.order,这些变量也是很重要的,会在下面的 next 中起到很关键的作用。

next

最开始的 if self.order: 这个判断,备注写的已经比较清楚了,再结合上文的分析,只有 order 在完成时,self.order 才会被置为 None。这句应该算是模板语句了,无论些什么策略都应该加上。那么 order 在什么时候不是 None 呢?我们接着往下看。

下面是另一个判断 if not self.position:,这个逻辑很有意思。position 是「持仓」的意思,也就是说如果没有持仓会走买入判断,有持仓才会走卖出判断。笔者尝试 print 了一下 self.position,效果如下:

--- Position Begin
- Size: 1
- Price: 24.74
- Price orig: 0.0
- Closed: 0
- Opened: 1
- Adjbase: 25.85
--- Position End

猜测 if 判断的应该是其 size 属性。再结合 if-else 中的代码逻辑,简单解释就是:如果没有持仓且连续 3 天跌,就买入;如果有持仓且已经持有了 5 个 line(天),就卖出。

不得不说这逻辑还真是简单粗暴啊,难道不应该连跌连买吗?后来想了一下,如果是这样的话,那「持仓 5 天」这个逻辑就不好计算了,光用一个整型的 self.bar_executed 记录买入时机是不够的,起码需要一个数组。总之 Demo 嘛,简单最重要。

然后在关注一下 if-else 分支的中的最后一句,无论是买还是卖,都是要给 self.order 赋值的,这就回答了上面遗留的问题。当已经触发了 buy 或 sell,且 order 的状态为「未完成」时,self.order 不为 None,就会在最开始走 return 的逻辑。嗯~很合理。

小结

这个 Demo 算是有那么点意思了,如果细研究其实还是有很多内容的,所以写个小结。它涉及到了 orderposition 等概念,尤其是 order 的生命周期,也就是各种状态;还有就是 notify_order 这个自动执行的生命周期钩子函数,还是有一定理解难度的,它当中的函数体注定写满了 if-else 逻辑。私以为充分的理解 order 的生命周期,是使用好它的前提,也是使用好 backtrader 的前提。

The broker says: Show me the money!

这个 Demo 主要是计入佣金的,内容比较好理解,这里重点列一下值得讲的代码:

    def __init__(self):
        # Keep a reference to the "close" line in the data[0] dataseries
        self.dataclose = self.datas[0].close

        # To keep track of pending orders and buy price/commission
        self.order = None
        self.buyprice = None
        self.buycomm = None


    def notify_order(self, order):
# other code...
        # Check if an order has been completed
        # Attention: broker could reject order if not enough cash
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(
                    'BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                    (order.executed.price,
                     order.executed.value,
                     order.executed.comm))

                self.buyprice = order.executed.price
                self.buycomm = order.executed.comm
            else:  # Sell
                self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                         (order.executed.price,
                          order.executed.value,
                          order.executed.comm))

    def notify_trade(self, trade):
        if not trade.isclosed:
            return

        self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' %
                 (trade.pnl, trade.pnlcomm))


if __name__ == '__main__':
# other code...
    # Set the commission - 0.1% ... divide by 100 to remove the %
    cerebro.broker.setcommission(commission=0.001)
# other code...

先看简单的变化:

  • cerebro.broker.setcommission(commission=0.001) 这句显然实在设置佣金比例;
  • 然后再看 __init__ 中多了 buypricebuycomm 两个变量,显然是用来记录计算佣金相关的数据的;
  • 最后是 notify_order 中的逻辑,除了 log 内容更多了之外,就是在买单逻辑里设置了 buypricebuycomm。但是这两个变量在这个 Demo 中没有在用到的地方了,似乎去掉也没关系。经过笔者的测试,也的确是这样的,甚至整个 Guidance 的 Demo 都有这几行冗余的代码。不过问题不大,大家心里清楚即可,而且相信这几行代码在真实环境中是非常常用的。

下面来看一个新方法:notify_trade。有了 notify_order 的铺垫,这个方法就不难理解了,就是针对 trade 的生命周期的一个钩子函数,不过 trade 的状态就简单很多了,只有 Created, Open, Closed 3 种,而且看起来通常只会用到后两种,还会直接使用 isclosedisopen 方法来判断状态。除了状态,它还有很多其他属性,比如代码中的 pnlpnlcomm 分别代表未去/已去佣金的损益值,原文是:

  • pnl: current profit and loss of the trade (gross pnl);
  • pnlcomm: current profit and loss of the trade minus commission (net pnl)

这里就不多展开了,更多属性请见 文档

总结

本篇内容已经有了一定复杂度了,尤其是 notify_order 这种钩子函数,以及 ordertrade 的概念。笔者认为,最关键的就是要理解 order 的概念,它是整个 backtrader 的核心,各位同学可以多看看文档,多查查资料,多理解理解。

篇幅原因,本文就先到这里,不出意外的话,还有一篇整个系列就能结束了,过了今天这关,后面就没有什么难点了。

TO BE CONTINUED……

一个人应该:活泼而守纪律,天真而不幼稚,勇敢而不鲁莽,倔强而有原则,热情而不冲动,乐观而不盲目。——卡尔·海因里希·马克思