1. 程式人生 > 實用技巧 >Backtrader中文筆記之Observers and Statistics

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 from Indicator)

  • 繼承與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 methods getcash and getvalue

  • 如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