官方鏈接: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文件的觀察者。