天天看點

Python量化交易學習筆記(30)——backtrader的StopLimit訂單

本文将繼續對backtrader的order進行介紹,具體介紹StopLimit訂單的使用。

選取平安銀行(000001)2018年1月1日至2019年12月31日的日線資料進行回測。為了便于分析,回測過程中設定傭金為0,交易機關大小為100。

這篇文章寫作過程有些坎坷,因為發現backtrader在StopLimit訂單實作過程中可能存在bug,已經在backtrader的官方社群發帖提問,目前尚未收到回複。本文将結合目前StopLimit訂單的實作代碼對其功能進行說明。

執行規則

在StopLimit訂單建立時,會設定price、plimit和valid時間,如果超過valid時間訂單仍未滿足執行條件,訂單就會過期被取消。在valid時間内,訂單會按照下面描述的價格比對規則判斷訂單是否會成交。

價格比對

StopLimit訂單使用K線4個價格點(Open/High/Low/Close)以及price和plimit,來判斷訂單是否會成交。

  • 首先以price作為觸發價格,使用Stop訂單價格比對規則判斷訂單是否會被觸發,參閱筆記(29)。
  • 對于已觸發訂單,以plimit作為限制價格,使用Limit訂單價格比對規則判斷訂單是否會成交,參閱筆記(28)。

示例

政策:

  • 買入條件:收盤價高于15日均線,價格在10日内,較收盤價向上突破0.5%作為觸發價格,較收盤價上漲0.2%作為限制價格。
  • 賣出條件:收盤價低于15日均線,價格在10日内,較收盤價向下跌破0.5%作為觸發價格,較收盤價下跌0.2%作為限制價格。
# 檢查是否持倉
        if self.position:
            # 檢查是否達到賣出條件
            if self.buysell < 0:
                if self.p.valid:
                    valid = self.data.datetime.date(0) + \
                            datetime.timedelta(days=self.p.valid)
                else:
                    valid = None
                ...
                elif self.p.exectype == 'StopLimit':
                    price = self.data.close * (1.0 - self.p.perc1 / 100.0)
                    plimit = self.data.close * (1.0 - self.p.perc2 / 100.0)
                    self.sell(exectype=bt.Order.StopLimit, price=price, valid=valid, plimit = plimit)
                    if self.p.valid:
                        txt = 'SELL CREATE, exectype StopLimit, close %.2f, price %.2f, pricelimit %.2f, valid: %s'
                        self.log(txt % (self.data.close[0], price, plimit, valid.strftime('%Y-%m-%d')))
                    else:
                        txt = 'SELL CREATE, exectype StopLimit, close %.2f, price %.2f, pricelimit %2.f'
                        self.log(txt % (self.data.close[0], price, plimit))

        # 不在場内且出現買入信号        
        elif self.buysell > 0: 
            if self.p.valid:
                valid = self.data.datetime.date(0) + \
                        datetime.timedelta(days=self.p.valid)
            else:
                valid = None
						...
            elif self.p.exectype == 'StopLimit':
                price = self.data.close * (1.0 + self.p.perc1 / 100.0)
                plimit = self.data.close * (1.0 + self.p.perc2 / 100.0)
                self.buy(exectype=bt.Order.StopLimit, price=price, valid=valid,
                         plimit=plimit)
                if self.p.valid:
                    txt = ('BUY CREATE, exectype StopLimit, close %.2f, price %.2f,'
                           ' pricelimit %.2f, valid: %s')
                    self.log(txt % (self.data.close[0], price, plimit, valid.strftime('%Y-%m-%d')))
                else:
                    txt = ('BUY CREATE, exectype StopLimit, close %.2f, price %.2f,'
                           ' pricelimit: %.2f')
                    self.log(txt % (self.data.close[0], price, plimit))
           

為了說明StopLimit訂單可能出現的成交情況,在代碼中使用p.perc1 = 0.5,p.perc2 = 0.2,來控制觸發及限制價格,使用p.valid = 10,來控制訂單的有效期。

StopLimit訂單執行邏輯的源代碼位于backtrader包中的bbroker.py,這裡以買單的代碼為例:

def _try_exec_stoplimit(self, order,
                            popen, phigh, plow, pclose,
                            pcreated, plimit):
        if order.isbuy():
            if popen >= pcreated:
                order.triggered = True
                self._try_exec_limit(order, popen, phigh, plow, plimit)

            elif phigh >= pcreated:
                # price penetrated upwards during the session
                order.triggered = True
                # can calculate execution for a few cases - datetime is fixed
                if popen > pclose:
                    if plimit >= pcreated:  # limit above stop trigger
                        p = self._slip_up(phigh, pcreated, lim=True)
                        self._execute(order, ago=0, price=p)
                    elif plimit >= pclose:
                        self._execute(order, ago=0, price=plimit)
                else:  # popen < pclose
                    if plimit >= pcreated:
                        p = self._slip_up(phigh, pcreated, lim=True)
                        self._execute(order, ago=0, price=p)
           

在這段源代碼中展示了StopLimit訂單價格比對的規則。在價格比對之前,會判斷訂單是否過期,相關代碼也在bbroker.py中,這裡就不做展示了。代碼中,popen、phigh、plow、pclose依次對應K線的open、high、low、close值,pcreated對應于buy方法參數中的觸發價格price值,plimit對應buy方法參數中的限制價格plimit值。

1. 對于第1種條件分支情況:popen >= pcreated

此時,開盤價大于等于觸發價格,那麼訂單就被觸發:

随後訂單将以plimit為限制價格,按照Limit訂單的邏輯被執行:

來看兩個例子:

例1:将圖表顯示時間範圍調整為2019年8月,輸出圖形如下:

Python量化交易學習筆記(30)——backtrader的StopLimit訂單

上圖中,紅色的曲線表示15日均線。

部分輸出結果為:

2019-08-08, BUY CREATE, exectype StopLimit, close 14.38, price 14.45, pricelimit 14.41, valid: 2019-08-18
2019-08-09, ORDER SUBMITTED
2019-08-09, ORDER ACCEPTED
2019-08-09, Open: 14.55, High: 14.85, Low: 14.43, Close: 14.52
2019-08-12, Open: 14.61, High: 15.12, Low: 14.60, Close: 15.12
2019-08-13, Open: 15.00, High: 15.08, Low: 14.74, Close: 14.89
2019-08-14, Open: 15.14, High: 15.22, Low: 14.80, Close: 14.97
2019-08-15, Open: 14.64, High: 14.96, Low: 14.60, Close: 14.94
2019-08-16, Open: 15.09, High: 15.14, Low: 14.78, Close: 14.90
2019-08-19, Open: 14.91, High: 14.94, Low: 14.52, Close: 14.92
2019-08-19, BUY EXPIRED
           

下面結合輸出列印結果及圖形進行分析。

(1) 8月8日,收盤價高于15日均線,達到了買入條件,建立StopLimit買單,當日收盤價為14.38,設定較收盤價上漲突破0.5%後的觸發價格為14.45,較收盤價上漲0.2%後的限制價格為14.41,p.valid = 10,有效期valid至8月18日。若在截止日期前能夠滿足StopLimit的價格比對條件,訂單就會成交,否則訂單就會過期。

(2) 8月9日,收到買單送出通知。(8日建立的訂單,在9日收到訂單狀态通知,是因為notify_order方法會在Strategy的next方法前被調用,即在8日的next方法中建立了買單,在9日的notify_order方法中通知訂單被送出、接受。)

(3) 8月9日,收到買單接受通知。

(4) 8月9日,開盤價popen值14.55,高于觸發價格pcreated值14.45,訂單将以plimit = 14.41為限制價格,按照Limit訂單的邏輯被執行。

(5) 8月9日至8月19日(8月18日為周末,未開市),每日的最低點均高于限制價格14.41,未達到Limit訂單的買入條件,訂單未成交。

(6) 8月19日,收到訂單過期通知。8月18日為StopLimit訂單設定的截止日期valid,由于18日為周末,是以即使19日滿足了Limit訂單的價格比對條件,訂單也不會成交,會因為逾時而過期。

在例1中,訂單達到了觸發價格,随之被按照Limit訂單的邏輯進行價格比對,最終由于到截止日仍未滿足Limit訂單的成交條件而逾時。

例2:将圖表顯示時間範圍調整為2019年11月,輸出圖形如下:

Python量化交易學習筆記(30)——backtrader的StopLimit訂單

部分輸出結果為:

2019-11-01, BUY CREATE, exectype StopLimit, close 16.86, price 16.94, pricelimit 16.89, valid: 2019-11-11
2019-11-04, ORDER SUBMITTED
2019-11-04, ORDER ACCEPTED
2019-11-04, Open: 16.98, High: 17.25, Low: 16.77, Close: 16.92
2019-11-04, BUY EXECUTED, Price: 16.89, Cost: 1689.37.
           

下面結合輸出列印結果及圖形進行分析。

(1) 11月1日,收盤價高于15日均線,達到了買入條件,建立StopLimit買單,當日收盤價為16.86,設定較收盤價上漲突破0.5%後的觸發價格為16.94,較收盤價上漲0.2%後的限制價格為16.89,p.valid = 10,有效期valid至11月11日。若在截止日期前能夠滿足StopLimit的價格比對條件,訂單就會成交,否則訂單就會過期。

(2) 11月4日(2日和3日為周末,未開市),收到買單送出通知。

(3) 11月4日,收到買單接受通知。

(4) 11月4日,開盤價popen值16.98,高于觸發價格pcreated值16.94,訂單将以plimit = 16.89為限制價格,按照Limit訂單的邏輯被執行。11月4日,開盤價popen值16.98,大于plimit值16.89,最低點plow值16.77,小于plimit值16.89,按照Limit訂單價格比對規則,将以plimit值16.89成交。

(5) 11月4日,訂單已plimit值16.89成交。

在例2中,訂單達到了觸發價格,随之被按照Limit訂單的邏輯進行價格比對,最終滿足Limit訂單的價格比對規則而成交。

2. 對于第2種條件分支情況:popen < pcreated & phigh >= pcreated & popen > pclose & plimit < pcreated & plimit >= pclose

首先,判斷phigh >= pcreated,那麼訂單就被觸發:

随後訂單将以plimit價格成交:

來看一個例子:

例3:将圖表顯示時間範圍調整為2018年3月,輸出圖形如下:

Python量化交易學習筆記(30)——backtrader的StopLimit訂單

部分輸出結果為:

2018-03-08, BUY CREATE, exectype StopLimit, close 11.92, price 11.98, pricelimit 11.94, valid: 2018-03-18
2018-03-09, ORDER SUBMITTED
2018-03-09, ORDER ACCEPTED
2018-03-09, Open: 11.96, High: 12.01, Low: 11.79, Close: 11.90
2018-03-09, BUY EXECUTED, Price: 11.94, Cost: 1194.18.
           

下面結合輸出列印結果及圖形進行分析。

(1) 3月8日,收盤價高于15日均線,達到了買入條件,建立StopLimit買單,當日收盤價為11.92,設定較收盤價上漲突破0.5%後的觸發價格為11.98,較收盤價上漲0.2%後的限制價格為11.94,p.valid = 10,有效期valid至3月18日。若在截止日期前能夠滿足StopLimit的價格比對條件,訂單就會成交,否則訂單就會過期。

(2) 3月9日,收到買單送出通知。

(3) 3月9日,收到買單接受通知。

(4) 3月9日,開盤價popen值11.96,低于觸發價格pcreated值11.98,但最高點phigh值12.01,高于觸發價格pcreated值11.98,訂單被觸發,進入後續價格判斷。限制價格plimit值11.94小于觸發價格pcreated值11.98,并且限制價格plimit值11.94大于收盤價pclose值11.90,訂單将以plimit值11.94成交。

(5) 3月9日,訂單已plimit值11.94成交。

在例3中,訂單由于當日最高點高于觸發價格而被觸發,随後根據源代碼中的價格判斷規則,被以plimit立即成交。

3. 對于第3種條件分支情況:popen < pcreated & phigh >= pcreated & popen > pclose & plimit < pcreated & plimit >= pclose

根據源代碼可以看出,這種分支情況是不在所列的分支邏輯中的,來看一個例子。

例4:将圖表顯示時間範圍調整為2019年5月,輸出圖形如下:

Python量化交易學習筆記(30)——backtrader的StopLimit訂單

部分輸出結果為:

2019-05-28, BUY CREATE, exectype StopLimit, close 12.49, price 12.55, pricelimit 12.51, valid: 2019-06-07
2019-05-29, ORDER SUBMITTED
2019-05-29, ORDER ACCEPTED
2019-05-29, Open: 12.36, High: 12.59, Low: 12.26, Close: 12.40
2019-05-30, Open: 12.32, High: 12.38, Low: 12.11, Close: 12.22
2019-05-30, BUY EXECUTED, Price: 12.32, Cost: 1232.00.
           

下面結合輸出列印結果及圖形進行分析。

(1) 5月28日,收盤價高于15日均線,達到了買入條件,建立StopLimit買單,當日收盤價為12.49,設定較收盤價上漲突破0.5%後的觸發價格為12.55,較收盤價上漲0.2%後的限制價格為12.51,p.valid = 10,有效期valid至6月7日。若在截止日期前能夠滿足StopLimit的價格比對條件,訂單就會成交,否則訂單就會過期。

(2) 5月29日,收到買單送出通知。

(3) 5月29日,收到買單接受通知。

(4) 5月29日,開盤價popen值12.36,低于觸發價格pcreated值12.55,但最高點phigh值12.59,高于觸發價格pcreated值12.55,訂單被觸發,進入後續價格判斷。由于popen(12.36) < pcreated(12.55) & phigh(12.59) >= pcreated(12.55) & popen(12.36) < pclose(12.40),此時,将進入源代碼中的最後一個分支:

else:  # popen < pclose
                    if plimit >= pcreated:
                        p = self._slip_up(phigh, pcreated, lim=True)
                        self._execute(order, ago=0, price=p)
           

但是此時plimit(12.51) < pcreated(12.55),代碼中沒有分支來處理這個邏輯,是以将進入下一輪循環(下一日K線判斷)。

(5)5月30日,我們先轉到bbroker.py來看一下源代碼:

def _try_exec(self, order):
        ...
        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)
           

通過源代碼可以看到,在5月30日處理價格比對邏輯時,并不會進入最後一個分支執行_try_exec_stoplimit方法,由于在5月29日時,已經将order.trigered置為True,是以将會以5月30日的K線來執行_try_exec_limit方法,也就是按照Limit訂單的邏輯來處理訂單。此時,開盤價Open值12.32小于限制價格plimit值12.51,按照Limit訂單的邏輯将以開盤價12.32成交。

(6) 5月30日,訂單已開盤價12.32成交。

在例4中,5月28日滿足了買單條件,5月29日訂單被送出并觸發,5月30日訂單成交。

再來回顧一下5月29日的情況,最高點12.59,高于觸發價格12.55,買單被觸發。按照StopLimit的設計邏輯,訂單被觸發後,将會以Limit訂單的邏輯進行執行。那麼,如果在最高點之後,出現小于等于限制價格plimit的話,那麼訂單應該就會成交。5月29日,最高點12.59,收盤價12.40,而限制價格plimit值為12.51,在最高點與收盤價之間,也就是說,plimit會出現在5月29日最高點之後,收盤之前,那麼訂單理應在5月29日成交。但是如輸出結果所示,由于源代碼邏輯中未包含相應的條件分支,訂單在5月30日才成交。

由于上述問題的存在,可以看到訂單成交推遲了一天,但是不會對回測造成過大影響。該問題已經在backtrader官方社群發帖詢問,推測是backtrader的bug,或者是未明确說明的交易邏輯考慮,目前尚未收到回複。待收到回複後,再對文章做出相應調整。

上面展示了StopLimit訂單做買入的情況,賣出的情況可以反過來對應展開,這裡就不做贅述了。如有疑問,可以參考Limit訂單和Stop訂單賣出的情況。

StopLimit訂單的用法可以總結為:突破關鍵點後以更優的價格開倉,跌破關鍵點後以更優的價格平倉。與Limit訂單相同,平倉時慎用StopLimit訂單。

為了便于互相交流學習,建立了微信群,感興趣的讀者請加微信。

Python量化交易學習筆記(30)——backtrader的StopLimit訂單