Skip to content

观察员和统计数字

原文: https://www.backtrader.com/docu/observers-and-statistics/observers-and-statistics/

backtrader内部运行的策略主要处理数据源指标

数据源被添加到大脑实例,并最终成为策略输入的一部分(解析并作为实例的属性),而指标则由策略本身声明和管理。

到目前为止,所有backtrader样本图表都绘制了 3 件事情,这些事情似乎被认为是理所当然的,因为它们没有在任何地方声明:

  • 现金和价值(经纪人的钱怎么了)

  • 贸易(又名经营)

  • 买卖订单

它们是Observers并且存在于子模块backtrader.observers中。它们之所以存在,是因为大脑支持一个参数自动将它们添加(或不添加)到策略中:

  • stdstats(默认为True

如果遵守默认值,则大脑执行以下等效用户代码:

import backtrader as bt

...

cerebro = bt.Cerebro()  # default kwarg: stdstats=True

cerebro.addobserver(bt.observers.Broker)
cerebro.addobserver(bt.observers.Trades)
cerebro.addobserver(bt.observers.BuySell) 

让我们看看这 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=False)
    cerebro.addstrategy(bt.Strategy)

    data = bt.feeds.BacktraderCSVData(dataname='../../datas/2006-day-001.txt')
    cerebro.adddata(data)

    cerebro.run()
    cerebro.plot() 

!image

现在我们在创建大脑实例时将stdstats的值改为False(调用run时也可以这样做):

cerebro = bt.Cerebro(stdstats=False) 

现在的图表不同了。

!image

接纳观察员

在默认情况下,如上所示的观察者已经存在,并收集可用于统计目的的信息,这就是为什么可以通过策略的一个属性访问观察者,该属性名为:

  • stats

它只是一个占位符。如果我们回顾一下,如上文所述,增加了一名默认观察员

...
cerebro.addobserver(backtrader.observers.Broker)
... 

显而易见的问题是如何访问Broker观察者。这里举例说明如何通过策略的next方法实现:

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 ....!!!') 

Broker观察者就像一个数据、一个指标,策略本身也是一个Lines对象。在这种情况下,Broker有两行:

  • cash

  • value

观察员执行

执行情况与指标非常相似:

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() 

步骤:

  • 源自Observer(而非源自Indicator

  • 根据需要声明行和参数(Broker有 2 行但没有参数)

  • 将有一个自动属性_owner,它是持有观察者的策略

观察员开始行动:

  • 所有指标计算完毕后

  • 策略next方法执行后

  • 这意味着:在周期结束时,他们观察发生了什么

Broker案例中,它只是盲目地记录每个时间点的经纪人现金和投资组合价值。

为战略增加观察员

如上所述,大脑正在使用stdstats参数来决定是否添加 3 个默认观察者,从而减轻最终用户的工作。

可以在混合中添加其他观察者,无论是沿着stdstats还是移除这些观察者。

让我们采用通常的策略,当close价格高于SimpleMovingAverage时买入,如果相反情况成立,则卖出。

加上一个“加法”:

  • 下降backtrader生态系统中已经存在的观察者
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):
        # 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):
        # Access -1, because drawdown[0] will be calculated after "next"
        self.log('DrawDown: %.2f' % self.stats.drawdown.drawdown[-1])
        self.log('MaxDrawDown: %.2f' % self.stats.drawdown.maxdrawdown[-1])

        # 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:
            self.log('BUY CREATE, %.2f' % self.data.close[0])
            self.buy()

def runstrat():
    cerebro = bt.Cerebro()

    data = bt.feeds.BacktraderCSVData(dataname='../../datas/2006-day-001.txt')
    cerebro.adddata(data)

    cerebro.addobserver(bt.observers.DrawDown)

    cerebro.addstrategy(MyStrategy)
    cerebro.run()

    cerebro.plot()

if __name__ == '__main__':
    runstrat() 

可视化输出显示了缩编的演变

!image

和部分文本输出:

...
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观察者实际上有两行:

  • drawdown

  • maxdrawdown

选择不是绘制maxdrawdown线,而是使其仍然可供用户使用。

实际上,maxdrawdown的最后一个值在名为maxdd的直接属性(非行)中也可用

发展中的观察员

Broker观察器的实现如上图所示。为了产生有意义的观察者,实现可以使用以下信息:

  • self._owner当前是否正在执行策略

    因此,观察者可以使用策略中的任何内容

  • 策略中可用的默认内部内容可能有用:

    • broker->属性,用于访问策略创建订单的代理实例

    Broker所示,通过调用getcashgetvalue方法收集现金和投资组合价值

    • _orderspending->列出策略创建的订单,并且经纪人已向策略通知了该订单的事件。

    BuySell观察者遍历列表,寻找已执行(全部或部分)的订单,以创建给定时间点(索引 0)的平均执行价格

    • _tradespending->根据买入/卖出订单编制的交易清单(一套完整的买入/卖出或卖出/买入对)

观察者显然可以通过self._owner.stats路径访问其他观察者。

定制订单观察者

标准的BuySell观察者只关心已经执行的操作。我们可以创建一个观察者,它可以显示订单创建的时间和地点,以及订单是否过期。

为了可见性的缘故,显示屏不会沿价格标绘,而是在单独的轴上标绘。

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 

定制观察者只关心购买订单,因为这是一种只为盈利而购买的策略。销售订单是市场订单,将立即执行。

关闭 SMA 交叉策略更改为:

  • 创建限价订单,价格低于信号发出时收盘价的 1.0%

  • 有效期为 7(日历)天

生成的图表。

!image

从新的分图表(红色方块)中可以看出,有几份订单已经过期,我们还可以理解,在“创建”和“执行”之间有几天的时间。

最后是应用新的观察者的该策略代码

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() 

保存/保存统计数据

到目前为止backtrader还没有实现任何机制来跟踪将其存储到文件中的观察者的值。最好的方法是:

  • 按策略的start方法打开一个文件

  • 在策略的next方法中写下值

考虑到DrawDown观察者,可以这样做

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') 

要保存索引 0 的值,处理完所有观察者后,可以将写入文件的自定义观察者添加为系统的最后一个观察者,以将值保存到 csv 文件。

笔记

Writer 功能可以自动执行此任务。


我们一直在努力

apachecn/AiLearning

【布客】中文翻译组