Backtrader教程:观察者 - 统计

  |  

在内部backtrader 运行的策略主要处理 data feeds指针

Data feeds 被添加到Cerebro 实例中,并最终成为策略输入的一部分(解析并用作实例的属性),而指针则由策略本身声明和管理。

到目前为止,所有backtrader 示例图表都绘制了3个东西,这些东西似乎被认为是理所当然的,因为它们没有在任何地方声明:

  • 现金和价值(经纪人中的钱发生了什么)

  • 交易(又名运营)

  • 买入/卖出订单

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

  • stdstats (默认值: True

如果遵循默认值Cerebro将运行以下等效的用户代码:

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个默认 observers (即使没有发出订单,因此没有交易发生,现金和投资组合价值也没有变化)

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

现在,让我们在创建Cerebro实例时将 False 的值stdstats更改为 (也可以在调用run时完成):

cerebro = bt.Cerebro(stdstats=False)

现在的图表不同了。

访问 Observers

如上所示, Observers 在默认情况下已经存在,并且收集可用于统计目的的信息,这就是为什么可以通过策略的属性 observers (称为:

  • stats

它只是一个占比特。如果我们还记得上面列出的缺省Observers 之一:

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

显而易见的问题是如何访问Brokerobserver。例如,如何从策略的方法完成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 observer就像数据一样,指针和策略本身也是一个Lines对象。在本例中,Broker有2个lines

  • cash

  • value

Observer 实施

实现与指针非常相似:

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

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

  • 将有一个自动属性_owner ,它是持有 observer

Observers 行动:

  • 在计算完所有指针后

  • 在运行策略next 方法后

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

在这种情况下,Broker 它只是盲目地记录经纪人在每个时间点的现金和投资组合价值。

向战略添加Observers

如上所述,Cerebro 正在使用参数 stdstats 来决定是否添加3个缺省 Observers,减轻最终用户的工作。

在混合物中添加其他 Observers 是可能的,无论是沿着stdstats 还是删除它们。

让我们采用通常的策略,当价格高于aSimpleMovingAverageclose买入,如果相反true,则卖出。

通过一个「添加」:

  • DrawDown是生态系统中backtrader已经存在observer
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()

视觉输出显示回撤的演变

与部分文本输出:

...
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

注意

如文本输出和代码所示,DrawDownobserver实际上有 2 个lines

  • drawdown

  • maxdrawdown

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

实际上,的 lastmaxdrawdown 也可以在直接属性(不是 line)中使用,其名称为 maxdd

开发 Observers

observerBroker实施情况如上所示。为了生成有意义的observer,实现可以使用以下信息:

  • self._owner 是当前正在运行的策略

    因此,策略中的任何内容都可供 observer

  • 原则中可用的缺省内部内容可能很有用:

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

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

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

    BuySell observer遍历清单,查找已运行(全部或部分)的订单,以创建给定时间点(索引 0)的平均运行价格

    • _tradespending ->交易清单(一组已完成的买入/卖出或卖出/买入对),该列表是根据买入/卖出订单编译的

Observer显然可以通过路径访问其他observersself._owner.stats

自订订单观察器

标准BuySellobserver只关心已运行的操作。我们可以创建一个observer显示订单的创建时间和到期时间。

为了提高可见性,显示不会沿价格绘制,而是在单独的轴上绘制。

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

自定义 observer 只关心买入 订单,因为这是一种仅为了获利而买入的策略。卖出订单是市价订单,将立即运行。

Close-SMA 交叉策略更改为:

  • 创建一个限价订单,其价格低于信号时刻 close 价格的1.0%

  • 订单有效期为 7(日历)天

生成的图表。

在新子图表(红色方块)中可以看到,有几个订单已经过期,我们也可以理解,在“创建”和“运行”之间恰好有几天。

最后,此策略的代码应用新的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()

保存/保留统计信息

截至目前backtrader ,尚未实施任何机制来跟踪 observers 的值将其存储到档中。最好的方法:

  • 在策略的方法中startOpen

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

考虑到DrawDownobserver,它可以像这样完成

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 的值,一旦处理完所有 observers ,就可以将写入档的自定义 observer 作为 last observer 添加到系统中,以将值保存到 csv 档。

注意

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

推荐阅读

相关文章

Backtrader对逐笔报价数据重新采样

backtrader 已经可以从分钟数据中重新采样。接受价格变动数据不是问题,只需将 4 个常用字段(open、 high、 low、 close)设置为价格变动值。 但是传递要重新采样的逐笔报价数据再次生成相同的数据。作为或版本 1.1.11.88,情况已不再如此。

Backtrader教程:安装

要求和版本 backtrader 是独立的,没有外部依赖关系(除非要绘图) 基本要求是: Python 2.7 Python 3.2 / 3.3/ 3.4 / 3.5 pypy/pypy3 如果需要绘图,则其他要求: Matplotlib >= 1.4.

Backtrader教程:日志记录 - 编写器

将以下内容写出到流中: csv 流,

Backtrader教程:Cerebro - 优化 - 改进

backtrader版本1.8.12.99改进了在多处理过程中管理data feeds和结果的方式。

Backtrader交叉回溯测试陷阱

在backtrader 社区中 ,倾向于重复的事情是,用户解释了拷贝在例如 TradingView 中获得的回溯测试结果的意愿,这些天非常流行,或者其他一些回溯测试平台。

Backtrader教程:日期时间 - 管理

在 1.5.0 版之前, backtrader 使用直接的方法来进行时间管理,因为数据源计算的任何日期时间都只是按面值使用。 对于任何用户输入也是如此,例如可以提供给任何数据源的参数fromdate (或 sessionstart)的情况 考虑到直接控制冻结的数据源以进行回溯测试,这种方法很好。

Backtrader 多数据范例

社区中的几个主题似乎以如何跟踪订单为导向,特别是当几个data feeds在起作用时,还包括当多个订单一起工作时,

Backtrader现实咬合

上一篇文章设法拷贝了该BTFD 策略,发现真正的收益 16x 而不是 31x。 但正如拷贝期间所指出的: 不收取佣金 使用2x 杠杆不收取利息 这就提出了一个显而易见的问题: 当收取佣金和利息时,这16x中有多少会存在? 幸运的是,前面的示例足够灵活,可以对其进行试验。

Backtrader教程:分析仪 - PyFolio

注意 从(至少)2017-07-25pyfolio 开始,API已更改,不再 create_full_tear_sheet 具有 gross_lev 作为命名参数的参数。

Backtrader信贷利息

在某些情况下,真实经纪人的现金金额可能会减少,因为资产操作包括利率。例子: 卖空股票 交易所买卖基金包括多头和空头 这意味着不仅交易构成了系统的盈利能力,因为信贷上的利息在帐户上佔有一席之地。 为了涵盖这种情况, backtrader 包括(从发佈1.8.8.96开始)功能来考虑这一点。