量化交易学习(三十四)backtrader文档——指标的开发

今天这篇是backtrader文档的学习笔记。主要介绍了怎样开发指标。

官方文档链接:https://www.backtrader.com/docu/inddev/

指标开发

如果必须开发任何东西(除了一个或多个获胜策略之外),那么这个东西就是自定义指标。

自定义指标的开发要做以下事情:

  • 从 Indicator 派生的类(直接派生或从现有子类派生)

  • 定义它将持有的线对象

指标必须至少有 1 条线。如果从现有线对象派生,则线对象可能已经定义

  • 可以选择定义改变指标行为的参数

  • 可选择提供/定制一些能够合理绘制指标的元素

  • 在__init__中完全定义一个绑定(赋值)了指标类中行对象的运算,或者定义next及once(可选)方法

如果指标可以在初始化期间用逻辑/算术运算完全定义,并将结果分配给线对象。

如果不是这种情况,则必须定义一个next方法,给线对象索引为 0 处赋值

通过定义once方法可以实现runonce模式(批量操作)计算的优化。

重要注意事项:幂等性

指标为它们收到的每个柱数据产生一个输出。无需假设同一个柱数据将被发送多少次。操作必须是幂等的。

这背后的理由:

  • 同一个柱(按索引)可以多次发送,并且值不断变化(即变化的值是收盘价)

例如,这使得能够“重放”每日走势,但可使用由 5 分钟柱线组成的日内数据。

它还可以让平台从实时数据源中获取数据。

模拟的(作为演示的)指标

那么是否可以是:

1
2
3
4
5
6
7
class DummyInd(bt.Indicator):
lines = ('dummyline',)

params = (('value', 5),)

def __init__(self):
self.lines.dummyline = bt.Max(0.0, self.params.value)

完毕!该指标将始终输出相同的值:如果恰好大于 0.0,则为self.params.value否则为 0.0。

相同的指标通过next方法实现:

1
2
3
4
5
6
7
class DummyInd(bt.Indicator):
lines = ('dummyline',)

params = (('value', 5),)

def next(self):
self.lines.dummyline[0] = max(0.0, self.params.value)

完毕!它们能实现同样的操作。

注意:请注意在__init__版本中如何将bt.Max赋值给 Line 对象self.lines.dummyline。

bt.Max返回一个线对象,该对象会自动迭代传递给指标的每个bar。

如果改为使用python标准的max函数,则赋值将毫无意义,因为指标将具有一个具有固定值的成员变量,而不是一条线。

在next方法执行期间直接使用python标准内置的max函数比较浮点值

让我们回想一下,self.lines.dummyline是一个很长的符号,它可以缩短为:

  • self.l.dummyline
    甚至:
  • self.dummyline

仅当代码未使用成员属性掩盖时,后者才可能实现。

第三个也是最后一个版本提供了一种额外的once方法来优化计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
class DummyInd(bt.Indicator):
lines = ('dummyline',)

params = (('value', 5),)

def next(self):
self.lines.dummyline[0] = max(0.0, self.params.value)

def once(self, start, end):
dummy_array = self.lines.dummyline.array

for i in xrange(start, end):
dummy_array[i] = max(0.0, self.params.value)

这样写更有效,但开发once方法不得不触及底层细节。

无论如何,__init__版本是最好的:

  • 一切都在初始化函数中完成
  • next和once(都经过优化,因为bt.Max已经有了它们)自动提供,无需使用索引和/或式子

如果开发需要,该指标还可以重写与next和once相关的方法:

  • prenext和nexstart
  • preonce和oncestart

手动/自动最短周期

如果可能,平台会计算它,但可能需要手动操作。

这是简单移动平均线的潜在实现:

1
2
3
4
5
6
7
class SimpleMovingAverage1(Indicator):
lines = ('sma',)
params = (('period', 20),)

def next(self):
datasum = math.fsum(self.data.get(size=self.p.period))
self.lines.sma[0] = datasum / self.p.period

虽然听起来不错,但即使参数被命名为“period”,平台也不知道最小周期是多少(该名称可能会产生误导,并且某些指标会收到多个具有不同用途的“period”)

在这种情况下,第一个bar之后会调用next方法,并且所有事情都无法正常开展,因为 get 无法返回所需的self.p.period。

在解决问题之前,必须考虑一些事情:

  • 传递给指标的数据馈送可能已经带有最小周期

SimpleMovingAverage可以这样实现:

  • 一个有规律的数据源

默认最小周期为 1(只需等待第一个柱进入系统)

  • 另一个移动平均线…而这又已经有一个周期

如果它的周期是 20,并且我们的例子中移动平均线周期也是 20,那么我们最终得到的最小周期为 40 个柱

实际上,内部计算显示为 39……因为一旦第一个移动平均线生成了一根柱线,它就计入下一个移动平均线,从而创建了一个重叠的柱线,因此需要 39 个bar。

  • 其他的指标/对象也会带有周期。

缓解这种情况的方法如下:

1
2
3
4
5
6
7
8
9
10
class SimpleMovingAverage1(Indicator):
lines = ('sma',)
params = (('period', 20),)

def __init__(self):
self.addminperiod(self.params.period)

def next(self):
datasum = math.fsum(self.data.get(size=self.p.period))
self.lines.sma[0] = datasum / self.p.period

addminperiod方法告诉系统考虑该指标所需的额外周期柱,以考虑可能存在的最小周期。

有时不需要这样做,如果所有计算都是使用已经向系统传达其周期需求的对象完成的。

使用直方图快速实现MACD :

1
2
3
4
5
6
7
8
9
10
11
12
from backtrader.indicators import EMA

class MACD(Indicator):
lines = ('macd', 'signal', 'histo',)
params = (('period_me1', 12), ('period_me2', 26), ('period_signal', 9),)

def __init__(self):
me1 = EMA(self.data, period=self.p.period_me1)
me2 = EMA(self.data, period=self.p.period_me2)
self.l.macd = me1 - me2
self.l.signal = EMA(self.l.macd, period=self.p.period_signal)
self.l.histo = self.l.macd - self.l.signal

完毕!无需考虑最小周期。

  • EMA代表指数移动平均线(平台内置别名)

这个(已经在平台中)已经说明了它需要什么

  • 指标“macd”和“signal”的命名线正在被赋值给已经带有声明的(幕后)周期的对象

    • macd 从“me1 - me2”中获取周期,该操作从 me1 和 me2 的周期中获取最大值(它们都是具有不同周期的指数移动平均线)

    • singal 直接采用 macd 的指数移动平均线的周期。该 EMA 还考虑了已经存在的 macd 周期和计算自身所需的样本量 (period_signal)

    • histo 取两个操作数“signal - macd”中的最大值。一旦两者都准备好了,histo 也可以产生一个值

完整的自定义指标

让我们开发一个简单的自定义指标,它“指示”移动平均线(可以使用参数修改)是否高于给定数据:

1
2
3
4
5
6
7
8
9
10
import backtrader as bt
import backtrader.indicators as btind

class OverUnderMovAv(bt.Indicator):
lines = ('overunder',)
params = dict(period=20, movav=btind.MovAv.Simple)

def __init__(self):
movav = self.p.movav(self.data, period=self.p.period)
self.l.overunder = bt.Cmp(movav, self.data)

完毕!如果平均值高于数据,则该指标的值为“1”;如果低于数据,则该指标的值为“-1”。

可以再添加一些画图参数,作图:

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
import backtrader as bt
import backtrader.indicators as btind

class OverUnderMovAv(bt.Indicator):
lines = ('overunder',)
params = dict(period=20, movav=bt.ind.MovAv.Simple)

plotinfo = dict(
# Add extra margins above and below the 1s and -1s
plotymargin=0.15,

# Plot a reference horizontal line at 1.0 and -1.0
plothlines=[1.0, -1.0],

# Simplify the y scale to 1.0 and -1.0
plotyticks=[1.0, -1.0])

# Plot the line "overunder" (the only one) with dash style
# ls stands for linestyle and is directly passed to matplotlib
plotlines = dict(overunder=dict(ls='--'))

def _plotlabel(self):
# This method returns a list of labels that will be displayed
# behind the name of the indicator on the plot

# The period must always be there
plabels = [self.p.period]

# Put only the moving average if it's not the default one
plabels += [self.p.movav] * self.p.notdefault('movav')

return plabels

def __init__(self):
movav = self.p.movav(self.data, period=self.p.period)
self.l.overunder = bt.Cmp(movav, self.data)

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

江达小记