量化投资之工具篇一:Backtrader从入门到精通(2)-重要概念以及平台的使用技巧

上一篇文章从总体上介绍了backtrader的功能和使用方法,这篇文章将从局部进行更加细致的讨论,为后续我们使用backtrader打下更加坚实的基础。

目录

一些重要概念

数据源(data feed)

数据源的传递

数据源的快捷访问

数据源的缺省访问

万物皆为数据源

参数

Lines

Lines声明

如何访问数据源中的Lines

Line的长度

Lines和参数的继承

索引0和-1

切片

延迟索引

Lines的耦合(coupling)

操作符

平台的使用

Line迭代器

Indicators的额外方法

最小周期(Minimum Period)

启动和运行

数据源(data Feed)

继承的strategy类

Cerebro

总结


一些重要概念

数据源(data feed)

backtrader策略运行的重要基础上数据源,如果没有数据,策略也就没有执行的逻辑基础。

数据源的传递

在backtrader中,数据源是以数组的形式或者对数组的快捷访问的方式提供给strategy(策略)作为成员变量使用的。这句话怎么理解呢?翻译一下,就是:

1、strategy这个类里面有个成员变量datas是个数组,用于存储数据源。数据源按照加入的先后顺序保存在datas中。策略在执行过程,会使用这些数据。上一篇文章中我们测试的策略中都会使用数据源中的close价格作为策略判断的依据。

2、访问数据源可以通过访问数组的方式,或者对数组的快捷方式访问。比如:

        
def __init__(self):
        # Keep a reference to the "close" line in the data[0] dataseries
        self.dataclose = self.datas[0].close
        self.dataclosetest = self.data0.close

其中datas[0].close就是数组访问方式,self.data0.close就是快捷访问方式,两者保存的数据完全一样,如下:

array('d', [4869.459, 4869.411, 4843.062, 4933.726, 4992.829, 4972.132, 4970.013, 5013.522, 4991.66, 4917.162, 4867.319, 4807.698, 4855.94, 4821.768, 4853.196, 4849.428, 4877.37, 4883.828, 4833.928, 4866.383])
array('d', [4869.459, 4869.411, 4843.062, 4933.726, 4992.829, 4972.132, 4970.013, 5013.522, 4991.66, 4917.162, 4867.319, 4807.698, 4855.94, 4821.768, 4853.196, 4849.428, 4877.37, 4883.828, 4833.928, 4866.383])

有的同学可能眼尖,发现strategy的__init__没有输入参数(除了self自身),我们的数组只是加入到cerebro中,它怎么就能直接使用datas数组了呢?这个是backtrader自身框架处理的,也就是,系统加入的数据源会按照加入的顺序自动呈现到strategy内部的data变量上,大家只需要知道如何使用即可。注意,与此类似的还要Indicators。

数据源的快捷访问

上一节已经说明,这里总结下,对数据源的快捷访问方式如下:

  • self.data 等价self.datas[0]

  • self.dataX 等价 self.datas[X]

示例如下:

class MyStrategy(bt.Strategy):
    params = dict(period=20)
    def __init__(self):
        sma = btind.SimpleMovingAverage(self.data, period=self.params.period)

数据源的缺省访问

如果只有一个数据源,你甚至都不用显式指定,系统会缺省使用self.datas[0],也就是第一个加入的数据源,示例:

class MyStrategy(bt.Strategy):
    params = dict(period=20)
    def __init__(self):
        sma = btind.SimpleMovingAverage(period=self.params.period)
   ...

调用SimpleMovingAverage的时候完全就丢掉self.data了。这种情况下,Indicator(本例中就是SimpleMovingAverage)会使用策略创建时输入的第一个数据,就是self.data (或者self.data0 或者self.datas[0],哥仨是一个意思])

万物皆为数据源

在backtrader中,不仅仅输入的数据是数据源,任何indicator(指标)以及任何对输入数据的操作(加,减,比较,求和,求平均... )结果均可称为数据源。上一个例子中SimpleMovingAverage对self.datas[0]进行求平均,其结果sma就是一个新的数据源。如下例子,展示了作为数据源的指标以及操作结果

class MyStrategy(bt.Strategy):
    params = dict(period1=20, period2=25, period3=10, period4)
    def __init__(self):

        sma1 = btind.SimpleMovingAverage(self.datas[0], period=self.p.period1)
        # sma2是对sma1进行移动平均的结果。
        sma2 = btind.SimpleMovingAverage(sma1, period=self.p.period2)
        # 对数据源进行算数加减的结果
        something = sma2 - sma1 + self.data.close
        # 对算数结果再进行移动平均的结果
        sma3 = btind.SimpleMovingAverage(something, period=self.p.period3)
        # 比较操作也可以
        greater = sma3 > sma1
        # 对True/False值进行移动平均操作,虽然没啥意义,但是也有效。
        sma3 = btind.SimpleMovingAverage(greater, period=self.p.period4)

基本上,所有的通过对数据源进行的各种操作,其结果也会生成一个数据源的对象。

参数

在backtrader中,所有的类都按照如下方法来使用参数:

1、声明一个带有缺省值的参数作为类的一个属性(元组或者字典结构)。

2、关键字类型参数(**kwargs)会扫描匹配的参数,将值赋值给对应的参数,完成后从**kwargs删除。

3、这些参数都可以在类实例中通过访问成员变量self.params(也可以简写为self.p)来使用。

之前的实例中我们已经提供了参数的使用方法,下面看看使用元组(包含元组)以及字典方式的不同之处。

使用元组实例:

class MyStrategy(bt.Strategy):
    params = (('period', 20),)
    def __init__(self):
        sma = btind.SimpleMovingAverage(self.data, period=self.p.period)

使用字典的实例:

class MyStrategy(bt.Strategy):
    params = dict(period=20)
    def __init__(self):
        sma = btind.SimpleMovingAverage(self.data, period=self.p.period)

Lines

Lines是一个非常重要的概念,在上一篇文章中做了详细的说明,从使用用户(通常是strategy)的角度来说,就是包括一个或多个line,这些line是一系列的数据,在图中可以形成一条线(line),例如使用股票的收盘价就可以形成一条线,这个大家在股票软件上就可以看到。

我们关心的是如何访问这个Line,上一篇文章已有示例,这里再看看一个简单的例子:

class MyStrategy(bt.Strategy):
    params = dict(period=20)
    def __init__(self):
        self.movav = btind.SimpleMovingAverage(self.data, period=self.p.period)
    def next(self):
        if self.movav.lines.sma[0] > self.data.lines.close[0]:

本例中,使用了两个Lines对象:

  • self.data: 这个本身包含一个lines属性,该lines还包含一个close属性。这句话啥意思了?就是说,self.data本身包含多条line,其中一个line就是close(收盘价)。参见前一篇文章中的示例图。

  • self.movav:这个是一个SimpleMovingAverage的指标(indicator),本身就包含一个具有lines属性的sma。这里特殊注意一下,计算SimpleMovingAverage使用的self.data,没有指定具体Line的话,缺省用的是close价进行计算。

这两个line,也就是close和sma,可以通过索引0来访问并比对,索引为0的含义前面讲过,就是系统当前正在处理的数据行(bar)。

还有简单的方法访问lines:

  •     xxx.lines 可以简化为 xxx.l

  •    xxx.lines.name可以简化为xxx.lines_name

  一些复杂的对象也可以通过如下方法访问:

  •     self.data_name 等于 self.data.lines.name

  •     如果有多个变量的话,也可以self.data1_name 替代self.data1.lines.name

此外,Line的名字也可以通过如下方式访问:

  • self.data.close and self.movav.sma

通过如下一段代码来看看效果:

       
 self.log('dataclose, %.2f' % self.dataclose[0])
        self.log('data_close, %.2f' % self.data_close[0])
        self.log('data.close, %.2f' % self.data.close[0])
        self.log('data.lines.name, %.2f' % self.data.lines.close[0])
        self.log('movav.lines.sma,%.2f' %self.movav.lines.sma[0])
        self.log('movav.l.sma,%.2f' %self.movav.l.sma[0])
        self.log('movav.sma,%.2f' %self.movav.sma[0])

结果是:

2021-09-23, dataclose, 4853.20
2021-09-23, data_close, 4853.20
2021-09-23, data.close, 4853.20
2021-09-23, data.lines.name, 4853.20
2021-09-23, movav.lines.sma,4905.26
2021-09-23, movav.l.sma,4905.26
2021-09-23, movav.sma,4905.26

可以方便直接看出等价关系。

这些方法了解即可,选择一个自己用的顺手的来使用。

Lines声明

如果要开发一个indicator(指标),那么指标拥有的所有的Line必须声明。

声明的方式是在indicator中增加一个lines的类属性,和参数不同的是,这里只能使用元组。没有使用字典的原因是字典无法存储按顺序插入的对象。

对于一个简单的移动平均指标,代码示例如下:

class SimpleMovingAverage(Indicator):
    lines = ('sma',)

    ...

注意,对于元组,如果只是输入一个字符串,那么后面的逗号必不可少,否则的话,字符串的每一个字母都会作为一个元素加入到元组中。

这个例子中,声明了一个sma的line,这个line后续可以在strategy的逻辑处理过程中直接使用(也可以使用其他indicator构造更复杂的indicator,这个我们后面还要详细讲)。

对于程序开发来说,有时候使用数字的方式更方便编程(例如循环啥的),在indicator中,可以按照如下方法访问:

  • self.lines[0] 指向 self.lines.sma

如果你定义了多个line,可以通过1,2等数字作为索引来访问:

  • self.line等价 self.lines[0]

  • self.lineX 等价 self.lines[X]

  • self.line_X 等价 self.lines[X]

对象如果接收了多个数据源,也可以通过如下数值索引方式访问:

  • self.dataY 等价 self.data.lines[Y]

  • self.dataX_Y 等价 self.dataX.lines[X] 也就等价 self.datas[X].lines[Y]

如何访问数据源中的Lines

前面讲过,数据源(data feed)中包含一个或者多个Lines,那么如何访问这些lines呢?有一个方法就是可以忽略lines,这样读起来更自然一些,比如我们可以按照如下方法访问close:

class MyStrategy(bt.Strategy):
    ...
    def next(self):

        if self.data.close[0] > 30.0:

和if self.data.lines.close[0] > 30.0比较,这样写更容易理解一些。但是在Indicators中,却不能这样使用,因为Indicator有时候在close中保存中间计算结果中,忽略lines会引发混乱。

数据源中通常不进行数据的计算,只作为数据的保存。

Line的长度

Line有一系列的数据(类似数组),系统处理过程中需要自动移动向后移动,那么,系统就必须要知道这组数据的长度。

在Backtrader中提供了两个函数来度量长度:

  • len:返回当前系统已经处理的数据(bars)。这个和python标准的len定义差异。

  • lenbuf:返回本数据源预先加载的数据(bars)个数。

只有在如下情况下,这两个值相等:

  • 数据源没有预先加载任何数据。

  • 数据源所有数据已经处理完毕。

Lines和参数的继承

特别说明以下,整个backtrader框架是基于面向对象的方式设计的,关于面向对象,网上文章和书籍都很多,大家需要预先学习下基本知识。

基本上,lines和参数都支持继承。

参数的继承:

  • 支持多重继承。

  • 基类的参数会被继承

  • 多重继承的时候,如果多个基类都定义了相同的参数和缺省值,那么最后一个基类的参数会被继承

  • 如果子类重新定义了和基类相同的一些参数,那么新的缺省值覆盖基类。

Lines的继承:

  • 支持多重继承。

  • 基类的所有lines都会被继承,如果在多个基类中使用相同的lines名称,只有一个命名会被使用。

索引0和-1

如前所述,Lines有一系列的Line组成(不然为啥Lines要用复数啊),Line则是由一组可以形成划线的数据点组成(比如一段时间的开盘价(close))。

可以按照python访问数组的通用方式对这些数据进行访问,需要注意的是,和python不一样的是,0指的是系统当前正在处理的数据,而不是第一个数据。

strategy类只会进行取值操作,而indicator只会进行赋值操作。

从之前的简短例子中,在strategy的next方法可以看出使用的方法:

def next(self):
    if self.movav.lines.sma[0] > self.data.lines.close[0]:
        print('Simple Moving Average is greater than the closing price')

这一段代码的逻辑就是:strategy通过索引0获取移动平均值和close价格的当前值并进行比较。

如果是索引0的话,系统可以按照如下的简单方法直接比对:

if self.movav.lines.sma > self.data.lines.close:
    ...

如下代码演示了如何通过获取当前值计算一个简单的移动平均指标:

def next(self):
  self.line[0] = math.fsum(self.data.get(0, size=self.p.period)) / self.p.period

那么之前的数据如何访问呢?在python中,通过-1来访问数组的最后一个数据。而在backtrader中,-1指的是当前处理数据(索引为0)的上一个数据。

比如我们在strategy中比对当前一个close和上一个close,代码如下:

def next(self):
    if self.data.close[0] > self.data.close[-1]:
        print('今天的收盘价比昨天高!')

逻辑上,以当前值0为基准,上一个值索引-1,上上一个值为-2,还可以继续-3,-4...

切片

在backtrader中,不支持对lines的数据进行切片操作,比如在标准python中,可以使用如下方法获取从头至尾的所有数据:

myslice = self.my_sma[0:]  # 从头至尾的所有数据切片

比如我们知道0是当前处理数据,-1是上一个数据,那么如下是不是就可以获取从头至尾的所有数据呢?

myslice = self.my_sma[0:-1]   # 从头至尾的所有数据切片

错了,不行。同样的,如下都不支持:

myslice = self.my_sma[:0]  # 从当前数据返回到开头
myslice = self.my_sma[-1:0]  #上一个值到当前值
myslice = self.my_sma[-3:-1]  # 上一个值到上3个值

那么,如果我们要获取部分数据(切片)该怎么办呢?使用如下代码:

myslice = self.my_sma.get(ago=0, size=1)  #这个函数的缺省值为(0,1)

含义就是以当前值(索引为0),向前返回1个数,这个代码会返回当前值(和索引为0含义一样)如果,size为2,那么返回索引为0,-1的两个值。具体含义就是以索引为ago,向前返回size个值(包含ago索引对应的值)。

返回的值是有顺序的,最左的值对应离ago最远的值,最右的是ago索引对应的值。例如如下是返回最新的10个值(不包括正在处理的值):

myslice = self.my_sma.get(ago=-1, size=10)

延迟索引

在strategy的next函数中,可以通过[]操作来获取处理的值。Lines对象支持在__init__阶段通过延迟索引来返回之前的值。

比如,我们需要将close的上一个值和当前的移动平均指标比对,相比在next中访问上一个值,不如在__init__中通过延迟索引产生一个新的Line。如下代码所示:

class MyStrategy(bt.Strategy):
    params = dict(period=20)
    def __init__(self):
        self.movav = btind.SimpleMovingAverage(self.data, period=self.p.period)
        self.cmpval = self.data.close(-1) > self.sma#延迟索引获取一个新的line vmpval,这个新的line是true/false的数组。
    def next(self):
        if self.cmpval[0]:#直接比对延迟索引产生的新Line。
            print('上一个close比当前移动平均值大')

延迟索引的使用方法是:

  • 通过延迟索引-1获取一个和close一样的line,但是值会向前移动1位。如下是close以及close延迟之后的值。

close    :  [4869.459, 4869.411, 4843.062, 4933.726, 4992.829, 4972.132, 4970.013, 5013.522, 4991.66, 4917.162, 4867.319, 4807.698, 4855.94, 4821.768, 4853.196, 4849.428, 4877.37, 4883.828, 4833.928, 4866.383]

close(-1):[nan         , 4869.459, 4869.411, 4843.062, 4933.726, 4992.829, 4972.132, 4970.013, 5013.522, 4991.66, 4917.162, 4867.319, 4807.698, 4855.94, 4821.768, 4853.196, 4849.428, 4877.37, 4883.828, 4833.928]

从上可以看出,close(-1)中第一个值延迟(向前移动一位)没有值,因此是无效值,而最后一个值对应的是close倒数第二个值,close的最后有一个值没有对应,直接丢弃了。

  • 比较式 self.data.close(-1) > self.sma会产生另外一个 lines object,对象中的值是1(True)或者0(False).

Lines的耦合(coupling)

题外话:coupling咋翻译为好,也可叫挂钩,中美脱钩叫decoupling,重新挂钩叫recoupling。这个词又有点象夫妻,不管是挂钩还是夫妻,反正双方要合适。backtrader对不同的lines进行适配处理,使他们能够挂钩/耦合,就是这么个意思。

耦合主要用于将时间窗口不同的两个line建立关系。比如

不同时间窗口的数据源具有不同的长度,indcator在使用这些数据的时候会复制这个长度。例如:

  • 股票的日线数据,每年大约250个(bar,对应250个工作日)

  • 股票的周线数据,每年大约52个(bar,对应52周)

现在如果我们要比对日线和周线的移动平均,这两个line一个长度为52个,一个250个,长度都不一样,如何比对呢?

我们可以想象一下,系统该如何处理?在backtrader中,使用一个空括号()来完成,如下示例:

class MyStrategy(bt.Strategy):
    params = dict(period=20)

    def __init__(self):

        # data0 是日线数据
        sma0 = btind.SMA(self.data0, period=15)  # 15 天的平均
        # data1 是周线数据
        sma1 = btind.SMA(self.data1, period=5)  # 5 周的平均
        self.buysig = sma0 > sma1()

    def next(self):
        if self.buysig[0]:
            print('日均线大于周均线!')

sma1通过加一个括号()将数据适配到每日的时间窗口。这个操作会返回一个对象,这个对象和具有较多bars的sma0的时间窗口一样,并将数据进行拷贝,也就是将52周的数据适配到250个bar(每日)。

特别说明:这个机制目前尚未验证,后续如有使用再深入研究具体机制。

操作符

为了更易于使用,和python一样,backtrader提供很多操作符,可以直接对line进行操作在,主要认为两个步骤:

第一步:通过操作符创建对象。

虽然没有明确说明,之前已经使用过这个方法。在Indicators(指标)和strategy(策略)的初始化阶段(__init__函数),通过操作符创建一些对象,这些对象后续在策略的逻辑处理中会被用到

如下示例是SimpleMovingAverage 指标在__init__中的实现:

def __init__(self):

    datasum = btind.SumN(self.data, period=self.params.period)
    av = datasum / self.params.period
    self.line.sma = av

更复杂的例子,在strategy中初始化:

class MyStrategy(bt.Strategy):

    def __init__(self):

        sma = btind.SimpleMovinAverage(self.data, period=20)
        close_over_sma = self.data.close > sma
        sma_dist_to_high = self.data.high - sma
        sma_dist_small = sma_dist_to_high < 3.5
        # 注意and在python中不能重写,所以只能通过函数实现。
        sell_sig = bt.And(close_over_sma, sma_dist_small)

上述步骤完成后,sell_sig作为一个Lines的对象后续会在strategy的逻辑处理中用到,作为某种条件是否满足的判决。

第二步:操作符的使用(True or False)

在strategy中next函数中,系统逐一对数据(line)的每一个数据(bar)进行处理,这里就是操作符实际使用地方,基于前一个例子,可以看出这一阶段的用法:

class MyStrategy(bt.Strategy):

    def __init__(self):

        sma = btind.SimpleMovinAverage(self.data, period=20)
        close_over_sma = self.data.close > sma
        sma_dist_to_high = self.data.high - sma
        sma_dist_small = sma_dist_to_high < 3.5
        # 注意and在python中不能重写,所以只能通过函数实现。
        sell_sig = bt.And(close_over_sma, sma_dist_small)

    def next(self):

        # 在这里,操作符用于判决。
        if self.sma > 30.0:
            print('sma is greater than 30.0')
        if self.sma > self.data.close:
            print('sma is above the close price')
        if self.sell_sig:  # if sell_sig == True: 这种写法也可以
            print('sell sig is True')
        else:
            print('sell sig is False')
        if self.sma_dist_to_high > 5.0:
            print('distance from sma to hig is greater than 5.0')

策略具体逻辑没啥用,就是用于展示操作符的使用。第二阶段,操作符返回期望的值,和普通的算数操作符一样。

特别注意的是,比较的时候使用[]操作符:

if self.sma > 30.0: … 比较的是self.sma[0] to 30.0 (第一个Line 和当前值)

if self.sma > self.data.close: … 比较 self.sma[0] 和self.data.close[0]。

一些不可重写的操作符/函数

python并不允许重写所有操作符,这种情况下backtrader通过函数来实现。注意,这些只用于第一阶段,用于创建对象,然后在第二阶段提供具体的值。

参见如下操作符/函数以及对应重写的函数。

操作符:

and -> And

or -> Or

逻辑控制:

if -> If

函数:

any -> Any

all -> All

cmp -> Cmp

max -> Max

min -> Min

sum -> Sum(Sum实际上是使用math.fsum作为底层操作,因为backtrader应用中需要处理大量浮点数据,而普通的sum函数精度会有影响。)

reduce -> Reduce

这些操作符/函数针对迭代器运行。迭代器中的元素可以是常规的Python数值类型(int、float等),也可以是Lines的对象。

如下是一个比较傻的购买信号的示例:

class MyStrategy(bt.Strategy):
    def __init__(self):
        sma1 = btind.SMA(self.data.close, period=15)
        self.buysig = bt.And(sma1 > self.data.close, sma1 > self.data.high)
    def next(self):
        if self.buysig[0]:
            pass  # 根据信号做点啥。。

以上仅仅是示例如何使用bt.And,请勿用于实际重做,亏钱不负责.

还可以使用bt.If

class MyStrategy(bt.Strategy):
    def __init__(self):
        sma1 = btind.SMA(self.data.close, period=15)
        high_or_low = bt.If(sma1 > self.data.close, self.data.low, self.data.high)
        sma2 = btind.SMA(high_or_low, period=15)

这段代码做了啥?

  • 使用第一个数据源中的close Line求出15天移动平均(sma1)

  • 如果sma1大于close,那么返回数据源的low line,否则返回high line。特别注意的是在这里并没有返回实际有效的值,只是返回lines对象,类似我们之前使用SimpleMovingAverage。具体的值后续在系统运行的时候才会具体计算。

  • bi.If返回的line再次求一次sma,这次求平均的值可能是high或者low。

这些函数还可以应用于数值,如下:

class MyStrategy(bt.Strategy):
    def __init__(self):
        sma1 = btind.SMA(self.data.close, period=15)
        high_or_30 = bt.If(sma1 > self.data.close, 30.0, self.data.high)
        sma2 = btind.SMA(high_or_30, period=15)

这一次,第二个sma求的是30或者high值的移动平均。你可以理解数值30在内部转换为始终返回30的伪迭代器。

平台的使用

Line迭代器

为了参与操作,plaftorm使用行迭代器的概念。它们模仿Python的迭代器,但实际上与它们并没有多大关系。

strategies和Indicators都是Line迭代器。

迭代中最关键的就是next函数,就像python普通的迭代器一样:

  • next方法

每一次迭代,next都会被调用。如前面所述,数据源(datas)中的line(例如close)是逻辑计算的基础,line是一组数据,在next中会自动移动到下一个索引(就是对line中的bars逐一处理)。next只有最小周期(minimum period)满足才会被调用,什么是最小周期?后面会详细详细说明。

由于并非标准迭代器,backtrader还提供如下两个函数:

  • prenext

在最小周期满足之前,每次迭代都会调用。

  • nextstart

当最小周期满足的时候,调用一次

系统缺省是调用next,如果需要的话,可以重写上面两个函数。

Indicators的额外方法

为了加快操作速度,Indicators支持称为runonce的批量操作模式,严格来讲,这个并不是必须的(next方法足够使用了),但是却确实有效减少了操作时间,在大数据处理的时候有用。

once方法实现机制是:取消原来通过索引0来设置或读取数据,而是依赖于直接访问包含原始数据的底层的数组,以及不同状态下传递正确的索引。

once主要包括:

  • once(self, start, end):

        当最小周期满足后就调用。在strart和end之间的数据会必须处理,strart和end指的是0为开始的内部数组的索引。

  • preonce(self, start, end)

        最小周期满足前调用

  • oncestart(self, start, end)

        最小周期满足的时候调用一次。

缺省是调用once,需要的话可以重写函数:

注意:以上方法暂未验证,具体使用待定。

最小周期(Minimum Period)

最小周期之前咱们也描述过,这里再重点介绍下,无图无真相,下面以一个例子(SimpleMovingAverage )来说明。

class SimpleMovingAverage(Indicator):
    lines = ('sma',)
    params = dict(period=20)
    def __init__(self):
        ...  # 本例无关,省略
    def prenext(self):
        print('prenext:: current period:', len(self))
    def nextstart(self):
        print('nextstart:: current period:', len(self))
        # emulate default behavior ... call next
        self.next()
    def next(self):
        print('next:: current period:', len(self))

在strategy中初始化:

sma = btind.SimpleMovingAverage(self.data, period=25)

简要解释下:

假设计算移动平均原始数据从第一个数据就是有效数据。

当参数period=25的时候,那么SimpleMovingAverage其中方法调用次数说明如下:

  • prenext: 24 次。因为周期为25的时候,前24个数据无效,每一个无效数据会被调用一次。

  • nextstart: 1 次 (紧接着就会调用next)

  • next: 再调用n次,直到所有数据处理完毕。

再来看复杂的调用:

sma1 = btind.SimpleMovingAverage(self.data, period=25)
sma2 = btind.SimpleMovingAverage(sma1, period=20)

发生了啥呢?

  • sma1同上一个例子一样。

  • sma2是计算sma1(25为最小周期的数据源)的移动平均,在sma2中:

  • prenext: 开始的 25 + 18 次 ,总共 43 times。

  • 25次让sma1产生第一个有效值

  • 18 用于累积第一个sma2的值,一共19个值 (25次之后1次然后18次)

  • nextstart 1 time (紧接着就会调用next)

  • next: 在调用n次,直到所有数据处理完毕。

系统在处理44个值(bars)之后,然后调用next

最小周期会自动调整到输入的数据

strategy和Indicator遵循如下规则:

只有自动计算的最小周期到达之后,next才会被调用(禁止对nextstart的调用)。

注意,复杂调用机制未经验证,后续使用的时候再分析。

启动和运行

启动和运行需要至少3个Lines对象:

  • 一个数据源(data Feed)

  • 一个策略(Strategy,继承Strategy的类)

  • 一个大脑(cerebro)

数据源(data Feed)

这个前面已有详细描述,就是通过计算(直接计算或者通过指标(Indicators))进行回测的数据(例如股票价格)。

平台支持如下几种数据源:

  1. 几种CSV格式和一个通用的CSV 读取器(reader)

  2. Yahoo在线获取(这个咱们用不上)

  3. 支持接受panda DataFrames以及Blaze(也是python的大数据计算接口)

  4. 一些券商提供的实时数据(这个咱们也用不上,国内券商不开放接口)

如果原始数据跨度大,可以通过如下方法设定时间段:

data = btfeeds.YahooFinanceCSVData(
    dataname=datapath,
    reversed=True
    fromdate=datetime.datetime(2014, 1, 1),
    todate=datetime.datetime(2014, 12, 31))

继承的strategy类

我们使用backtrader回测数据,主要就是在strategy中实现的,至少有两个方法需要定制:

  • __init__

  • next

在初始化阶段,基于数据源的计算结果或者指标(Indicators)类会被创建(注意这个时候并没有实际数据!!),后续在next中用于逻辑处理。

next方法随后会被调用针对每一行数据(bars)进行逻辑处理。

如果数据源是不同的时间跨度(因此有不同的bars),next会基于主数据源(第一个输入的数据源)进行处理,因此,第一个数据源应该有更小的时间跨度。

如果使用了数据重放(Replay) 功能,next会针对相同的bars被调用多次。

一个简单的继承类如下所示:

class MyStrategy(bt.Strategy):
    def __init__(self):
        self.sma = btind.SimpleMovingAverage(self.data, period=20)
    def next(self):
        if self.sma > self.data.close:
            self.buy()
        elif self.sma < self.data.close:
            self.sell()

还可以重写strategy类的其他方法:

class MyStrategy(bt.Strategy):
    def __init__(self):
        self.sma = btind.SimpleMovingAverage(self.data, period=20)
    def next(self):

        if self.sma > self.data.close:
            submitted_order = self.buy()

        elif self.sma < self.data.close:
            submitted_order = self.sell()

    def start(self):
        print('回测开始...')

    def stop(self):
        print('回测结束!')

    def notify_order(self, order):
        print('订单创建/更改/执行/取消了!')

start和stop看名字就知道啥意思了,不细说。

当strategy需要获知订单的变化的时候,就需要重写notify_order,主要用于:

  • 买入/卖出将返回提交给券商的订单,strategy需要记录已提交订单的状态,比如说,如果已经提交了一个订单,需要确保新的订单不会被提交。

  • 如果订单被接受/执行/取消/更改,那么券商(broker)就会通知状态的改变。

在第一篇文章中完整的示例中展示了notify_order的用法。

strategies还有一些方法需要了解:

  • sell/buy/close

使用strategies底层的broker(券商)以及sizer(买卖的大小,例如股票多少手)给broker发送一个买卖的订单。当然,我们也可以自己手动创建一个订单,然后发送给broker,这样做比较麻烦而已,怎么简单怎么来。

close会获取当前的市场postion(持仓,包括头寸以及价格)并且立即关闭(也就是清空)。

  • getposition (或者直接使用属性position)

返回市场持仓信息。

  • setsizer/getsizer (或者直接使用属性sizer)

这两个函数可以设置或者获取策略的底层sizer。可以为相同情况下提供不同的交易数量(固定大小,与资本成比例,指数型,这些后续在sizer类中再详细介绍)。在市场上,不同情况下下注多少可是一门大学问,可以参看《Definitive Guide to Position Sizing》( 头寸管理权威指南(范撒普)),这可是量化投资的权威著作,咱们后面会介绍。

Cerebro

cerebro,西班牙就是大脑,你可以这个就是backtrader的CPU。

一旦数据源(data feed)和strategies准备好了,那么就可以创建一个cerebro实例来执行具体的动作。实例化一个cerebro比较简单:

cerebro = bt.Cerebro()

如果什么参数也没有的话,broker会执行如下动作:

  • 创建一个缺省的broker;

  • 操作不花费佣金

  • 预装载数据源(Data Feed)

  • 缺省的执行模式是runonce(批量操作),这样速度会更快。所有的Indicators必须支持runonce模式。定制的Indicator不需支持,cerebro会模拟,也就是不支持runonce的indicators会执行得慢点,但是大多数会按照批量操作运行。

data Feed和strategy预备好之后,cerebro按照如下方法将其组合起来:

cerebro.adddata(data)
cerebro.addstrategy(MyStrategy, period=25)
cerebro.run()

注意:

增加data Feed的实例
加入strategy类以及传递给它的参数。对strategy的实例由cerebro在后台完成,实例化使用的参数就是就是addstrategy携带的参数。

用户 可以按需要添加任意多个strategies和data feeds。策略如何协调使用这些数据和策略,平台并不限制。

当然,cerebro还提供了其他可选项:

  • 决定预装载和操作模式

cerebro = bt.Cerebro(runonce=True, preload=True)

这里有一个限制条件,runonce模式下,必须使用preload。如果没有启用preload,批量处理就不会进行。反之却不必,也就是启用preload,却不必使用runonce。

  • setbroker / getbroker (或者broker的属性)

broker可以按需要定制。

  • 画图,一般很简单:

cerebro.run()
cerebro.plot()

    plot也有一些定制化参数。

    numfigs=1

    如果图太紧密了,可以分为多个图

    plotter=None

    可以传递一个定制的plotter实例,这样cerebro就不会实例化一个缺省的plotter

    **kwargs - 标准的字典参数,传递给plotter。

后续会在plotter类说明中再详细介绍。

  • strategy的优化

如前所述,cerebro会有一个strategy的类(而不是实例)以及传递给它的字典参数,这个参数用于实例化。实例化会在run被调用的时候执行。

这样就可以对strategy进行优化,因为cerebro会基于新的参数实例多次。这也是为啥strategy要传类,而不是实例。传了实例,这个就无法执行了。

策略优化可以按照如下方法:

cerebro.optstrategy(MyStrategy, period=xrange(10, 20))

optstrategy 方法和addstrategy方法一样,但是会按照期望对strategy进行优化测试。在optstrategy中,针对一个参数,可以传递一组数据。cerebro在实例化的时候,按照顺序遍历这一组数据来实例化多个strategy。这样多个strategy就会被执行,本例中,会创建周围分别为10-19(20是上限)的strategy来进行回测。

如果策略还有更多的参数的话,那么不参与优化的参数只需要传递一个值就可以,无需构造一个每次返回固定值的伪迭代器,示例如下:

cerebro.optstrategy(MyStrategy, period=xrange(10, 20), factor=3.5)

总结

本文详细介绍了使用backtrader必须掌握的一些关键知识,有了这些知识,我们可以尝试写一些量化策略。但是为了更灵活的使用backtrader,我们还需要更深入的学习它提供的类,了解如何正确的使用和扩展这些类,磨刀不误砍柴工,下一篇文章我们开始学习backtrader的类。

  • 29
    点赞
  • 52
    收藏
    觉得还不错? 一键收藏
  • 18
    评论
评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值