量化交易学习(三十六)backtrader文档——观察者(1)

今天这篇是backtrader文档的学习笔记。主要介绍了观察者。

官方文档链接:https://www.backtrader.com/docu/observers-and-statistics/observers-and-statistics/

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

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

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

【没翻译,感觉英文原文更好懂】

  • Cash and Value (what’s happening with the money in the broker)
  • Trades (aka Operations)
  • Buy/Sell Orders

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

  • stdstats(默认值:True)
    如果遵循默认设置,Cerebro会执行以下等效的用户代码:
1
2
3
4
5
6
7
8
9
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 个默认观察者的常规图表(即使没有发出订单,没有发生交易,并且现金和投资组合价值没有变化)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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()

7063c9730851260400a1b333c2ece497.png

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

1
cerebro = bt.Cerebro(stdstats=False)

现在图表不同了。

229fc197472c1dc6832d9bb8722e59e3.png

访问观察者

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

  • stats

它只是一个占位符。如果我们还记得上面例子中添加的默认观察者之一:

1
2
3
...
cerebro.addobserver(backtrader.observers.Broker)
...

显而易见的问题是如何访问Broker观察者。例如,这是如何通过策略中的next方法访问:

1
2
3
4
5
6
7
8
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有 2 行:

  • cash
  • value

观察者的实现

其实现与指标的实现非常相似:

1
2
3
4
5
6
7
8
9
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观察者例子中,它只是盲目地记录经纪人在每个时间点的现金和投资组合价值。

将观察者添加到策略中

正如上面已经指出的,Cerebro通过stdstats参数来决定是否添加 3 个默认观察者,从而减轻最终用户的工作量。

我们还可以添加其他观察者。

让我们采用一个简单的单均线策略,即当close价格高于SimpleMovingAverage时买入,否则卖出。

加上一个“补充”说明:

  • DrawDown 是backtrader生态系统中已经存在的观察者
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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()

视觉输出显示了回撤的变化

25f66576ee875caa7fe2a4eb0d6c76eb.png

以及部分文本输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
...
2006-12-14T23:59:59+00:00,最大回撤:2.62
2006-12-15T23:59:59+00:00,回撤:0.22
2006-12-15T23:59:59+00:00,最大回撤:2.62
2006-12-18T23:59:59+00:00,回撤:0.00
2006-12-18T23:59:59+00:00,最大回撤:2.62
2006-12-19T23:59:59+00:00,回撤:0.00
2006-12-19T23:59:59+00:00,最大回撤:2.62
2006-12-20T23:59:59+00:00,回撤:0.10
2006-12-20T23:59:59+00:00,最大回撤:2.62
2006-12-21T23:59:59+00:00,回撤:0.39
2006-12-21T23:59:59+00:00,最大回撤:2.62
2006-12-22T23:59:59+00:00,回撤:0.21
2006-12-22T23:59:59+00:00,最大回撤:2.62
2006-12-27T23:59:59+00:00,回撤:0.28
2006-12-27T23:59:59+00:00,最大回撤:2.62
2006-12-28T23:59:59+00:00,回撤:0.65
2006-12-28T23:59:59+00:00,最大回撤:2.62
2006-12-29T23:59:59+00:00,回撤:0.06
2006-12-29T23:59:59+00:00,最大回撤:2.62

注意:从文本输出和代码中可以看出,DrawDown观察者实际上有 2 个线对象:

  • drawdown
  • maxdrawdown

图中没有绘制maxdrawdown线,但是用户还是可以在程序中使用它

实际上,maxdrawdown的最后一个值也可以直接在名称为maxdd的属性(不是线对象)中获取


这一篇就到这里啦。欢迎大家点赞、转发、私信。还没有关注我的朋友可以关注 江达小记

江达小记