Backtrader中文笔记之Observers and Statistics
官方链接:https://www.backtrader.com/blog/posts/2015-08-12-observers-and-statistics/observers-and-statistics/
Strateties running inside the backtrader do mostly deal with datas and indicators.
策略在backtrader中运行,主要靠datas与indicators
Datas are added to Cerebro instances and end up being part of the input of strategies (parsed and served as attributes of the instance) whereas Indicators are declared and managed by the Strategy itself.
数据被添加到cerebro实例中,最终会成为策略输入的一部分(被解析作为实例的属性),而指标由策略本身申明与管理。
All backtrader sample charts have so far had 3 things which seem to be taken for granted because they are not declared anywhere:
到目前为止,所有backtrader的样本图表都有3件事情似乎是理所当然的,因为它们没有在任何地方声明:
-
Cash and Value (what’s happening with the money in the broker)
-
Trades (aka Operations)
-
Buy/Sell Orders
They are Observers
and exist within the submodule backtrader.observers
. They are there because Cerebro supports a parameter to automatically add (or not) them to the Strategy
它们是观察者,存在于backtrader.observer子模块中。它们存在是因为Cerebro支持一个参数来自动将它们添加(或不添加)到策略中:
import backtrader as bt ... cerebro = bt.Cerebro() # default kwarg: stdstats=True cerebro.addobserver(backtrader.observers.Broker) cerebro.addobserver(backtrader.observers.Trades) cerebro.addobserver(backtrader.observers.BuySell)
Let’s see the usual chart with those 3 default observers (even if no order is issued and therefore no trade happens and there is no change to the cash and portfolio value)
让我们看看这3个默认观察者的图表(即使没有发出指令,因此没有交易发生,现金和投资组合价值也没有变化)
from __future__ import (absolute_import, division, print_function, unicode_literals) import backtrader as bt import backtrader.feeds as btfeeds if __name__ == '__main__': cerebro = bt.Cerebro(stdstats=True) cerebro.addstrategy(bt.Strategy) data = bt.feeds.BacktraderCSVData(dataname='../datas/2006-day-001.txt') cerebro.adddata(data) cerebro.run() cerebro.plot()
Now let’s change the value of stdstats
to False
when creating the Cerebro instance (can also be done when invoking run
):
现在让我们在创建Cerebro实例时将stdstats的值更改为False(也可以在调用run时完成):
cerebro = bt.Cerebro(stdstats=False)
Accesing the Observers
The Observers as seen above are already there in the default case and collecting information which can be used for statistical purposes and that’s why acess to the observers can be done through an attribute of the strategy called:
上面所看到的观察者在默认情况下已经在那里了,并且收集了可以用于统计目的的信息,这就是为什么对观察者的访问可以通过策略的一个属性来完成:
stats
It is simply a placeholder. If we recall the addition of one of the default Observers as laid out above:
它只是一个占位符。如果我们回想一下上面添加的一个默认观察者:
... cerebro.addobserver(backtrader.observers.Broker) ...
The obvious question would be how to access the Broker
observer. Here for example how it’s done from the next
method of a strategy:
显而易见的问题是如何访问代理观察者。这里举个例子,如何从策略的下一个方法着手:
class MyStrategy(bt.Strategy): def next(self): if self.stats.broker.value[0] < 1000.0: print('WHITE FLAG ... I LOST TOO MUCH') elif self.stats.broker.value[0] > 10000000.0: print('TIME FOR THE VIRGIN ISLANDS ....!!!')
The Broker
observer just like a Data, an Indicator and the Strategy itself is also a Lines
objects. In this case the Broker
has 2 lines:
Broker
观察者就像一个数据、一个指标,而策略本身也是一个lines对象。在本例中,Broker
有两个lines:
cash value
Observer Implementation
The implementation is very similar to that of an Indicator:
实现非常类似于一个指标:
class Broker(Observer): alias = ('CashValue',) lines = ('cash', 'value') plotinfo = dict(plot=True, subplot=True) def next(self): self.lines.cash[0] = self._owner.broker.getcash() self.lines.value[0] = value = self._owner.broker.getvalue()
Steps:
-
Derive from
Observer
(and not fromIndicator
) - 继承与
Observer
(不是Indicator
) -
Declare lines and params as needed (
Broker
has 2 lines but no params) - 根据需要声明行和参数(
Broker有两个lines,但没有params
) -
There will be an automatic attribute
_owner
which is the strategy holding the observer - 将有一个自动属性_owner,它是容纳观察者的策略
- 这个真的有意思,observer在strategy.stats里面,但strategy却在observer_owner里面
Observers come in action:
Observers行动起来:
-
After all Indicators have been calculated
- 在所有的指标计算完成
-
After the Strategy
next
method has been executed - 然后策略的next方法开始被执行
-
That means: at the end of the cycle … they observe what has happened
- 这意味在一个循环的最后,观察者发生了什么
In the Broker
case it’s simply blindly recording the broker cash and portfolio values at each point in time.
在Broker
的情况下,它只是盲目地记录经纪人在每个时间点的现金和投资组合价值。
Adding Observers to the Strategy
向策略添加观察者
As already pointed out above, Cerebro is using the stdstats
parameter to decide whether to add 3 default Observers, alleviating the work of the end user.
正如上面已经指出的,Cerebro使用stdstats参数来决定是否添加3个默认观察者,减轻最终用户的工作。
Adding other Observers to the mix is possible, be it along the stdstats
or removing those.
可以添加其他观察器,可以沿着stdstats添加,也可以删除它们。
Let’s go for the usual strategy which buys when the close
price goes above a SimpleMovingAverage
and sells if the opposite is true.
让我们采用通常的策略,当收盘价高于平均线时买入,反之则卖出。
With one “addition”:
- DrawDown which is an already existing observer in the
backtrader
ecosystem - 在回溯交易生态系统中已经存在的一个观察者
from __future__ import (absolute_import, division, print_function, unicode_literals) import argparse import datetime import os.path import time import sys import backtrader as bt import backtrader.feeds as btfeeds import backtrader.indicators as btind class MyStrategy(bt.Strategy): params = (('smaperiod', 15),) def log(self, txt, dt=None): ''' Logging function fot this strategy''' dt = dt or self.data.datetime[0] if isinstance(dt, float): dt = bt.num2date(dt) print('%s, %s' % (dt.isoformat(), txt)) def __init__(self):
The visual output shows the evolution of the drawdown
视觉输出显示了递减的演变过程
And part of the text output:
... 2006-12-14T23:59:59+00:00, MaxDrawDown: 2.62 2006-12-15T23:59:59+00:00, DrawDown: 0.22 2006-12-15T23:59:59+00:00, MaxDrawDown: 2.62 2006-12-18T23:59:59+00:00, DrawDown: 0.00 2006-12-18T23:59:59+00:00, MaxDrawDown: 2.62 2006-12-19T23:59:59+00:00, DrawDown: 0.00 2006-12-19T23:59:59+00:00, MaxDrawDown: 2.62 2006-12-20T23:59:59+00:00, DrawDown: 0.10 2006-12-20T23:59:59+00:00, MaxDrawDown: 2.62 2006-12-21T23:59:59+00:00, DrawDown: 0.39 2006-12-21T23:59:59+00:00, MaxDrawDown: 2.62 2006-12-22T23:59:59+00:00, DrawDown: 0.21 2006-12-22T23:59:59+00:00, MaxDrawDown: 2.62 2006-12-27T23:59:59+00:00, DrawDown: 0.28 2006-12-27T23:59:59+00:00, MaxDrawDown: 2.62 2006-12-28T23:59:59+00:00, DrawDown: 0.65 2006-12-28T23:59:59+00:00, MaxDrawDown: 2.62 2006-12-29T23:59:59+00:00, DrawDown: 0.06 2006-12-29T23:59:59+00:00, MaxDrawDown: 2.62
下面是DrawDown观察者的源码
class DrawDown(Observer): '''This observer keeps track of the current drawdown level (plotted) and the maxdrawdown (not plotted) levels Params: - ``fund`` (default: ``None``) If ``None`` the actual mode of the broker (fundmode - True/False) will be autodetected to decide if the returns are based on the total net asset value or on the fund value. See ``set_fundmode`` in the broker documentation Set it to ``True`` or ``False`` for a specific behavior ''' _stclock = True params = ( ('fund', None), ) lines = ('drawdown', 'maxdrawdown',) plotinfo = dict(plot=True, subplot=True) plotlines = dict(maxdrawdown=dict(_plotskip=True,)) def __init__(self): kwargs = self.p._getkwargs() self._dd = self._owner._addanalyzer_slave(bt.analyzers.DrawDown, **kwargs) def next(self): self.lines.drawdown[0] = self._dd.rets.drawdown # update drawdown self.lines.maxdrawdown[0] = self._dd.rets.max.drawdown # update max
Note
请注意
As seen in the text output and in the code, the DrawDown
observer has actually 2 lines:
从文本输出的代码中可以看到,DrawDown observer实际上有两行:
-
drawdown
-
maxdrawdown
The choice is not to plot the maxdrawdown
line, but make it is still available to the user.
选择maxdrawdown
并不是想画出来,而是让他对用户仍然可用
Actually the last value of maxdrawdown
is also available in a direct attribute (not a line) with the name of maxdd
实际上,maxdrawdown的最后一个值也可以在名为maxdd的直接属性(而不是一行)中使用
Developing Observers
The implementation of the Broker
observer was shown above. To produce a meaningful observer, the implementation can use the following information:
上面显示了Broker
observer的实现。为了产生有意义的观察者,实现可以使用以下信息:
-
self._owner
is the currently strategy being executed - self._owner是当前正在执行的策略
-
As such anything within the strategy is available to the observer
- 因此,策略中的任何东西都可供观察者使用
-
Default internal things available in the strategy which may be useful:
- 策略中可用的默认内部信息可能有用
-
broker
-> attribute giving access to the broker instance the strategy creates order against- 这个策略创造了broker属性,通过broker能够访问beoker实例
As seen in
Broker
, cash and portfolio values are collected by invoking the methodsgetcash
andgetvalue
- 如Broker中所示,现金和投资组合价值是通过调用getcash和getvalue方法来收集的
_orderspending
-> list orders created by the strategy and for which the broker has notified an event to the strategy.- _orderspending->列出由策略创建的、broker已将事件通知策略的订单。
The
BuySell
observer traverses the list looking for orders which have executed (totally or partially) to create an average execution price for the given point in time (index 0) -
BuySell
observer遍历列表,寻找已执行(全部或部分)的订单,以创建给定时间点(索引0)的平均执行价格_tradespending
-> list of trades (a set of completed buy/sell or sell/buy pairs) which is compiled from the buy/sell orders_tradespending
->交易列表(一组完整的买入/卖出或卖出/买入对),由买入/卖出指令编译而成
An Observer can obviously access other observers over the self._owner.stats
path.
一个Observer显然能够通过自身访问其他观察者self._owner.stats
的方法
Custom OrderObserver
自定义OrderObserver
The standard BuySell
observer does only care about operations which have executed. We can create an observer which shows when orders where created and if they expired.
标准的BuySell观察者只关心已经执行的操作。我们可以创建一个观察者来显示订单何时何地创建以及是否过期。
For the sake of visibility the display will not be plotted along the price but on a separate axis.
为了便于查看,显示不会沿价格绘制,而是在单独的轴上绘制。
from __future__ import (absolute_import, division, print_function, unicode_literals) import math import backtrader as bt class OrderObserver(bt.observer.Observer): lines = ('created', 'expired',) plotinfo = dict(plot=True, subplot=True, plotlinelabels=True) plotlines = dict( created=dict(marker='*', markersize=8.0, color='lime', fillstyle='full'), expired=dict(marker='s', markersize=8.0, color='red', fillstyle='full') ) def next(self): for order in self._owner._orderspending: if order.data is not self.data: continue if not order.isbuy(): continue # Only interested in "buy" orders, because the sell orders # in the strategy are Market orders and will be immediately # executed if order.status in [bt.Order.Accepted, bt.Order.Submitted]: self.lines.created[0] = order.created.price elif order.status in [bt.Order.Expired]: self.lines.expired[0] = order.created.price
The custom observer only cares about buy orders, because this is a stratey which only buys to try to make a profit. Sell orders are Market orders and will be executed immediately.
这个自定义观察者只关心购买订单,因为这是一个只为获取利润而购买的策略。卖出执行是按照市场价,立即执行的
The Close-SMA CrossOver strategy is changed to:
Close-SMA交叉策略改为:
-
Create a Limit order with a price below 1.0% the close price at the moment of the signal
- 创建一个价格低于信号时刻收盘价1.0%的限价单
-
A validity for the order of 7 (calendar) days
- 订单有效期为7(日历)天
The resulting chart.
Several orders have expired as can be seen in the new subchart (red squares) and we can also appreciate that between “creation” and “execution” several days happen to be.
从新的子图(红色方块)中可以看出,有几个订单已经过期,我们也可以看出,“创建”和“执行”之间正好有几天时间。
Note
Starting with commit 1560fa8802 in the development
branch if price is unset at the time of order creation, the closing price will be used as the reference price.
从开发分支中的commit 1560fa8802开始,如果在创建订单时价格未设置,则收盘价将用作参考价格。
This has no impact in Market orders but keeps order.create.price
usable at all times and eases up the usage of buy
这对市场订单没有影响,但是订单创建价格随时可用,简化了buy的使用
Finally the code for this strategy which applies the new observer
from __future__ import (absolute_import, division, print_function, unicode_literals) import datetime import backtrader as bt import backtrader.feeds as btfeeds import backtrader.indicators as btind from orderobserver import OrderObserver class MyStrategy(bt.Strategy): params = ( ('smaperiod', 15), ('limitperc', 1.0), ('valid', 7), ) def log(self, txt, dt=None): ''' Logging function fot this strategy''' dt = dt or self.data.datetime[0] if isinstance(dt, float): dt = bt.num2date(dt) print('%s, %s' % (dt.isoformat(), txt)) def notify_order(self, order): if order.status in [order.Submitted, order.Accepted]: # Buy/Sell order submitted/accepted to/by broker - Nothing to do self.log('ORDER ACCEPTED/SUBMITTED', dt=order.created.dt) self.order = order return if order.status in [order.Expired]: self.log('BUY EXPIRED') elif 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)) else: # Sell self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' % (order.executed.price, order.executed.value, order.executed.comm)) # Sentinel to None: new orders allowed self.order = None def __init__(self): # SimpleMovingAverage on main data # Equivalent to -> sma = btind.SMA(self.data, period=self.p.smaperiod) sma = btind.SMA(period=self.p.smaperiod) # CrossOver (1: up, -1: down) close / sma self.buysell = btind.CrossOver(self.data.close, sma, plot=True) # Sentinel to None: new ordersa allowed self.order = None def next(self): if self.order: # pending order ... do nothing return # Check if we are in the market if self.position: if self.buysell < 0: self.log('SELL CREATE, %.2f' % self.data.close[0]) self.sell() elif self.buysell > 0: plimit = self.data.close[0] * (1.0 - self.p.limitperc / 100.0) valid = self.data.datetime.date(0) + \ datetime.timedelta(days=self.p.valid) self.log('BUY CREATE, %.2f' % plimit) self.buy(exectype=bt.Order.Limit, price=plimit, valid=valid) def runstrat(): cerebro = bt.Cerebro() data = bt.feeds.BacktraderCSVData(dataname='../datas/2006-day-001.txt') cerebro.adddata(data) cerebro.addobserver(OrderObserver) cerebro.addstrategy(MyStrategy) cerebro.run() cerebro.plot() if __name__ == '__main__': runstrat()
Saving/Keeping the statistics
保存统计数据
As of now backtrader
has not implemented any mechanism to track the values of observers storing them into files. The best way to do it:
到目前为止,backtrader还没有实现任何机制来跟踪将它们存储到文件中的观察者的值。最好的方法是:
-
Open a file in the
start
method of the strategy - 在策略的start方法中打开一个文件
-
Write the values down in the
next
method of the strategy - 在策略的next方法中写下这些值
Considering the DrawDown
observer, it could be done like this:
考虑到DrawDown
observer,可以这样做:
class MyStrategy(bt.Strategy): def start(self): self.mystats = open('mystats.csv', 'wb') self.mystats.write('datetime,drawdown, maxdrawdown\n') def next(self): self.mystats.write(self.data.datetime.date(0).strftime('%Y-%m-%d')) self.mystats.write(',%.2f' % self.stats.drawdown.drawdown[-1]) self.mystats.write(',%.2f' % self.stats.drawdown.maxdrawdown-1]) self.mystats.write('\n')
To save the values of index 0, once all observers have been processed a custom observer which writes to a file could be added as the last observer to the system to save values to a csv file.
为了保存索引0的值,一旦所有观察者都处理完毕,可以添加一个写入文件的自定义观察者,作为系统最后一个将值保存到csv文件的观察者。