本文将继续对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
此时,开盘价大于等于触发价格,那么订单就被触发:
order.triggered = True
随后订单将以plimit为限制价格,按照Limit订单的逻辑被执行:
self._try_exec_limit(order, popen, phigh, plow, plimit)
来看两个例子:
例1:将图表显示时间范围调整为2019年8月,输出图形如下:
上图中,红色的曲线表示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月,输出图形如下:
部分输出结果为:
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,那么订单就被触发:
order.triggered = True
随后订单将以plimit价格成交:
self._execute(order, ago=0, price=plimit)
来看一个例子:
例3:将图表显示时间范围调整为2018年3月,输出图形如下:
部分输出结果为:
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月,输出图形如下:
部分输出结果为:
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订单。
为了便于相互交流学习,新建了微信群,感兴趣的读者请加微信。