前言
篇幅关系,详探系列会分成几篇来写,前篇《【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_order
和 next
中用到,我们先来重点解析一下 notify_order
。
notify_order
我们发现这个方法在代码中没有被主动调用,从 文档 中可以知道,当 order 的状态变化时,该函数会自动触发,可以理解为 order 生命周期的钩子函数。order 的状态如下,就不翻译了:
Status | Explain |
---|---|
Created | set when the Order instance is created. Never to be seen by end-users unless order instances are manually created rather than through buy , sell and close |
Submitted | set 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 |
Accepted | the 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 |
Partial | the 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 |
Complete | the order has been completely filled average price. |
Rejected | the 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. |
Margin | the order execution would imply a margin call and the previously accepted order has been taken off the system |
Cancelled | confirmation 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 |
Expired | a previously accepted order which had a time validity has expired and been taken off the system |
代码中还有 isbuy
和 issell
方法,它们都返回一个 boolean 值,来标明当前是买单还是卖单,另外看文档还有一个 alive
,是判断 order 是否处于 Partial
或 Accepted
状态。
然后就是 order.executed.xxx
属性,也就是成交时的各种数据,比如 price 等,这个查看文档中的 OrderData
说明即可。
最后,比较难理解的是 self.bar_executed = len(self)
这句,实际上就是记录一下当前一共已经遍历了几个 line
,每次成交就记录一下,目的就是为了实现「买完之后过 5 个 line(天)卖」。不过更细节一点的,在这个 Demo 中,理论上应该只有在 buy
的时候才需要记录,经过笔者测试也的确是这样,把这行代码移到 isbuy
的判断分支里,结果与当前代码相同。
最后一句 self.order = None
会在 order 状态不是 Submitted
和 Accepted
的时候触发,所以算是一个「开关」,大体上就是防止 order 在处于「未完成」状态时多次进行重复的 buy 和 sell 的操作。
notify_order 解析的差不多了,里面绝大多数就是 if-else
的逻辑,最主要还是打印 log。另外就是改变一些后续运算中需要用到的变量值,比如 self.bar_executed
和 self.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 算是有那么点意思了,如果细研究其实还是有很多内容的,所以写个小结。它涉及到了 order
、position
等概念,尤其是 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__
中多了buyprice
和buycomm
两个变量,显然是用来记录计算佣金相关的数据的; - 最后是
notify_order
中的逻辑,除了 log 内容更多了之外,就是在买单逻辑里设置了buyprice
和buycomm
。但是这两个变量在这个 Demo 中没有在用到的地方了,似乎去掉也没关系。经过笔者的测试,也的确是这样的,甚至整个 Guidance 的 Demo 都有这几行冗余的代码。不过问题不大,大家心里清楚即可,而且相信这几行代码在真实环境中是非常常用的。
下面来看一个新方法:notify_trade
。有了 notify_order
的铺垫,这个方法就不难理解了,就是针对 trade
的生命周期的一个钩子函数,不过 trade 的状态就简单很多了,只有 Created, Open, Closed 3 种,而且看起来通常只会用到后两种,还会直接使用 isclosed
和 isopen
方法来判断状态。除了状态,它还有很多其他属性,比如代码中的 pnl
和 pnlcomm
分别代表未去/已去佣金的损益值,原文是:
- pnl: current profit and loss of the trade (gross pnl);
- pnlcomm: current profit and loss of the trade minus commission (net pnl)
这里就不多展开了,更多属性请见 文档。
总结
本篇内容已经有了一定复杂度了,尤其是 notify_order
这种钩子函数,以及 order
、trade
的概念。笔者认为,最关键的就是要理解 order
的概念,它是整个 backtrader 的核心,各位同学可以多看看文档,多查查资料,多理解理解。
篇幅原因,本文就先到这里,不出意外的话,还有一篇整个系列就能结束了,过了今天这关,后面就没有什么难点了。
TO BE CONTINUED……
一个人应该:活泼而守纪律,天真而不幼稚,勇敢而不鲁莽,倔强而有原则,热情而不冲动,乐观而不盲目。——卡尔·海因里希·马克思