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

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文件的觀察者。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM