backtrader文档翻译:概念Concepts

翻译说明

  • Cerebro:西班牙语,大脑,backtrader系统运行工作的核心调度类。文中不翻译。
  • Strategy:策略。用户定义的选股、买卖等操作的逻辑。
  • DataFeeds:数据源。平台中存储的数据,包括价格、交易量、估值等金融数据,也包括计算出的指标等结果。
  • Line:线。backtrader系统组织数据的方式,类似于列表,最大的不同是当前时间下标为0的索引对应值都是最新值。
  • Bar:时间点。最直观的表示是股票软件中的一个蜡烛线对应的时间,可以是一分钟,一小时,也可以是一天,一周。

正文:

本文是一些backtrader平台相关概念的集合,希望有助于更好的使用本平台。

准备工作

所有简短例子都假设已经引入下列模块:

import backtrader as bt
import backtrader.indicators as btind
import backtrader.feeds as btfeeds

注意
也可以用另一种语法,通过访问bt模块访问子模块。

import backtrader as bt
#然后用下列方式访问
thefeed = bt.feeds.OneOfTheFeeds(...)
theind = bt.indicators.SimpleMovingAverage(...)

数据源(DataFeed)-传来传去

在本系统的操作都是通过策略,而策略需要传递数据源。
最终用户不需要太关注怎么接收数据源,它将自动把数组格式的成员变量交给策略,并提供访问数组位置的快捷方式。

先来快速浏览一下一个策略派生类的定义和如何在平台中运行:

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

    def __init__(self):

        sma = btind.SimpleMovingAverage(self.datas[0], period=self.params.period)

    ...

cerebro = bt.Cerebro()

...

data = btfeeds.MyFeed(...)
cerebro.adddata(data)

...

cerebro.addstrategy(MyStrategy, period=30)

注意:

  • 策略初始化函数没有接收任何类似*args or **kwargs的参数。
  • 存在成员变量self.datas,它是数组或列表或迭代器,至少包含有一个元素(否则将会导致一个例外)。

就这样,数据源已经添加到平台中,当我们在策略中先后发出交易单时就可以使用到它。

注意
不管是自己开发的指标还是平台提供的指标也是数据源。

访问数据源的快捷方式

可以通过其它成员变量方式访问self.datas数组。

  • self.data 表示self.datas[0]
  • self.dataX 表示self.datas[X],比如self.data0,self.data1
    例如:
class MyStrategy(bt.Strategy):
    params = dict(period=20)

    def __init__(self):
		#(译注:self.data指向self.datas[0]
        sma = btind.SimpleMovingAverage(self.data, period=self.params.period)

    ...

省略掉数据源

以上例子可以进一步简化为:

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

    def __init__(self):

        sma = btind.SimpleMovingAverage(period=self.params.period)

    ...

这次函数的参数中彻底没有数据源了。此时系统会默认将策略中的第一个数据(self.data0或者说self.datas[0])传入函数。

一切都是数据源

不是只有数据源是可传递的数据。指标和操作结果也是数据。
上一例子中,SimpleMovingAverage函数接受了self.data作为参数。下面的例子中,操作和指标也可以。

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)

        # 这里将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

        # 把比较操作作为数据(虽然没有实际作用)
        sma3 = btind.SimpleMovingAverage(greater, period=self.p.period4)

几乎所有对象在被操作时都转为一个可以数据源对象。

参数

几乎都支持参数使用。

  • 拥有默认值的参数被定义为类属性,可以用元组也可以用字典定义。
  • 实例化类时,会扫描和输入参数名(kwargs)匹配的类参数名(parameter),一旦找到就将其指定给对应的类参数(parameter)。
  • 在类实例中,调用参数作为成员变量,调用格式是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

平台中几乎所有对象也是支持线的对象。以用户的角度看,这意味着这些对象可以拥有一个或多个线序列。一个线序列就是一组按顺序排列成线的数值。

一个很好的例子是股票收盘价组成的线(或者说线序列)。这也是一个广为人知的价格排列方法,即收盘线。使用平台时通常只关心怎么访问线。把上面的策略的小例子稍微扩展一下:

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]:
            print('Simple Moving Average is greater than the closing price')

这其中有两个拥有线的对象。

  • self.data有一个lines属性,包含顺序排列的收盘价数据(self.data.lines.close)。
  • self.movav。这是一个移动均线指标,也有一个lines属性,包含顺序排列的sma指标值(self.movav.lines.sma)。

注意
很明显,lines是有名字的(close,sma)。也可以使用声明顺序依次访问,但是只能在开发指标时这样做。

所有lines都可以用下标访问(close[0],sma[0])。

线的快捷访问方式有:

  • xxx.lines可以写成xxx.l
  • xxx.lines.name可以写成xxx.lines_name
  • 诸如策略和指标这样的复杂对象提供了数据中的线对象的缩写:
    • self.data.lines.name可以写成self.data_name
    • 拥有编号的数据也可以缩写,比如self.data1.lines.name也可以写成self.data1_name

还可以这样访问:
self.data.close以及self.movav.sma
不过这种方式不像之前的方式那样明确线序列是否真正被访问到。

注意:不能用后两种方式对线对象赋值。

lines声明

在指标开发过程中必须声明其拥有的lines。此时线作为类属性,拥有参数,只能用元组定义。因为字典不能按插入顺序存储,所以不能用字典定义。

下面是在移动平均线指标中定义line的例子:

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

注意
如果元组中只有一个字符串,后面必须加上都好,否则字符串中的每个字符会作为一个元素添加到元组中。

这个例子在指标中加入了一个名为sma的线,之后可以在策略逻辑中访问,也可能被其他指标访问,用来创造更复杂的指标。

有时候开发时不使用名称而是用下标访问line更方便,例如:

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

当然还有其他的简单访问方式:

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[Y]。不简写的写法是`self.datas[X].lines[Y]``

访问数据源中的线

访问数据源中的线时可以省略掉lines,这让代码显得更自然。
例如对于收盘价:

data = btfeeds.BacktraderCSVData(dataname='mydata.csv')

...

class MyStrategy(bt.Strategy):

    ...

    def next(self):

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

其中的self.data.close[0]也可以写成self.data.lines.close[0],但是前者更自然。

注意同样的简写方法不能用于指标。因为指标有可能真有close这个属性用于中间计算,计算结果又会作用于线中的close。
数据源可以这样写是因为数据源不会执行任何计算,只是一个数据源(DataSource)。

线的长度

线是一系列点,在执行过程中长度动态增长。任何时候都可以用标准的python len函数测量line的长度。这适用于DataFeeds,策略Strategies,指标Indicators。

数据源在被预加载时还有一个附加的方法buflen,返回数据源可用的bar长度。

len和buflen的区别:

  • len表示有多少bar已经被处理。
  • buflen表示数据源中有多少bar被加载。

如果两者返回相同数字,要不是没有数据被提前加载,要不是所有提前加载的数据都已经处理完了。这意味着如果没有接入实时数据,程序处理已经结束。

线和参数的继承

线和参数定义了一些元语言,用于使其适应标准的python继承规则。

参数继承

  • 支持多重继承
  • 基类的参数会被继承
  • 如果定义了多重继承,且在不同的基类中定义了相同的参数,将会使用继承列表中最后一个基类的默认值。
  • 如果子类中定义了与基类相同的参数,将会使用子类中的参数值。

lines继承

  • 支持多重继承
    所有基类的lines都会被继承。如果多个基类定义了相同的lines,最终子类将会只有一个版本的lines。

索引0和-1

如前所述,Lines是线群,线是一组点的集合,这些点在绘制在一起形成一条线(例如,沿着时间轴将所有收盘价连在一起就形成收盘价曲线)

要在常规代码中访问这些点,一般通过0索引的方式对当前点进行get/set操作。 策略只能读取数据, 指标既可以读取也可以写入数据。

回顾前面简单的示例,策略中的next方法:

def next(self):
    if self.movav.lines.sma[0] > self.data.lines.close[0]:
        print('简单移动平均线大于收盘价')

通过索引0获得移动平均线的当前值和当前收盘价,并比较它们的大小。

注意: 实际上对于索引0,可直接进行逻辑/算术运算操作,如下所示:

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

更多相关说明请参阅文档后面的《操作章节》。

在指标开发的应用中,会出现赋值操作。 例如SimpleMovingAverage的当前值可以通过如下方式进行读写:

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

访问前一个点集合可以按照Python访问数组索引为-1的方式:

  • 它指向数组的最后一项

框架认为最后一项为(读写当前点的前一个点)索引值为-1。 因此,在策略中比较当前收盘价与前一个收盘价是通过 0 vs -1的方式。例如:

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

同理,使用-1,-2,-3,…便访问-1之前项的价格。

切片

backtrader不支持对线对象进行切片,这是遵循[0]和[-1]索引方案的设计决策。 使用常规的可索引Python对象,可以执行以下操作:

# 从开始到结尾的切片
myslice = self.my_sma[0:]

但是请记住,选择0…实际上是当前开始传递的值,之后也没有任何值。

# 从开始到结尾的切片
myslice = self.my_sma[0:-1]

同样,…0是当前值,而-1是先前交付的值。 这就是为什么从0->-1进行的切片反向操作就毫无意义的原因。
如果可以反向操作,那么切片可能应该这样:

# 从当前点向前的切片
myslice = self.my_sma[:0] 
# 从最后的值到当前值
myslice = self.my_sma[-1:0]  
# 从最后的值到倒数第3个值
myslice = self.my_sma[-3:-1]

获取切片

可以获得具有最新值的数组,语法:

# 显示默认值
myslice = self.my_sma.get(ago=0, size=1)

返回一个数组,该数组的大小为1,当前时刻为0,向后获取。
要从当前时间点获取10个值(即:最后10个值):

# ago的默认值为0
myslice = self.my_sma.get(size=10)

常规数组具有你所期望的顺序。最左边的值是最旧的值,最右边的值是最新的值(这是常规的python数组,而不是lines对象)。

# 跳过当前点获取最后10个值
myslice = self.my_sma.get(ago=-1, size=10)

线的延迟索引

[]运算符可用于在next逻辑阶段提取单个值。lines对象支持附加的符号,以便在__init__阶段通过延迟的线对象寻址取值。
假设一条逻辑是将先前的收盘价与简单移动平均线的实际值进行比较。无需在每次next迭代中进行手动操作,而是可以生成预定义的lines对象:

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

    def next(self):
        if self.cmpval[0]:
            print('上一个收盘价高于当前移动平均值')

这里使用"()"延迟符号:(延迟到next时计算)

  • 这提供了收盘价的副本,但延迟了-1。
    比较self.data.close(-1)> self.sma 会生成另一个line对象,如果条件为True,则返回1,否则为0

线(群)的耦合

运算符()可以与延迟的数值一起使用,以提供延迟的line对象。
如果使用中不提供延迟数值,则返回LinesCoupler对象。这是为了在操作具有不同时间范围的数据指标之间建立耦合。
不同时间范围的交易数据具有不同的长度,并且指标在操作这些数据时会复制数据的长度。例如:

  • 日交易数据每年约有250条
  • 周交易数据每年有52条

尝试创建一个比较两个简单移动平均线的操作,每次操作在引用数据时都有可能被中断。因为系统不知道如何在250条的日交易数据和52条的周交易数据之间进行匹配。

读者可以通过找出一天和一周的对应关系进行比较,但是:

  • 指标只是数学公式,没有日期时间信息
    他们对上下文环境一无所知,只要提供了足够的数据,就可以进行计算。

于是()表示法(空调用)可用于解决这个问题:

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

    def __init__(self):

        # data0 是日交易数据
        sma0 = btind.SMA(self.data0, period=15)  # 15天sma
        # data1 是周交易数据
        sma1 = btind.SMA(self.data1, period=5)  # 5周sma

        self.buysig = sma0 > sma1()

    def next(self):
        if self.buysig[0]:
            print('每日sma大于每周sma1')

在这里,较大的时间范围指标sma1通过sma1()与每日时间范围耦合。这将返回与更大数量的sma0兼容的对象,并复制sma1产生的值,从而有效地将52个周数据分散为250个日数据。

通过操作符构造对象

为了实现“简单易用”的目标,backtrader允许(在Python的语法范围内)使用操作符。为了进一步简单化,操作符的使用有两种情景。

情景1-操作符创建对象

我们之前已经看到了一个例子。在指标和策略类的对象初始化阶段(__init__方法)中,操作符创建并保存可以持续使用的对象,供策略逻辑在评估阶段使用。
将SimpleMovingAverage的潜在实现方式进一步细分为多个步骤。
SimpleMovingAverage指标__init__内的代码可能如下:

def __init__(self):
    # N个周期值的总和,数据总和是一个Lines对象
    # 在与运算符[]和索引0查询时
    # 返回当前总和
    datasum = btind.SumN(self.data, period=self.params.period)
    # datasum(虽然是单行,但仍是一个Lines对象)
    # 在这种情况下它可以除以int/float类型的数据。 
    # 但实际上它被除以后得到另外一个Lines对象。
    # 该操作返回分配给av对象
    # 当查询为[0],则返回当前时间点的平均值
    av = datasum / self.params.period
    # av是对新的Lines对象的命名
    # 其他对象使用这个指标可以直接访问计算
    self.line.sma = av

策略初始化期间显示了更加完整的用法:

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中被重载
        # 在python中and不属于运算符,所以backtrader提供一个函数模拟这个功能
        sell_sig = bt.And(close_over_sma, sma_dist_small)

完成上述操作后,sell_sig是一个Lines对象,当指示满足条件时,可以直接在策略中使用。

情景2-逻辑操作符

首先,策略的next方法,系统要处理每个柱时都要调用该方法,这就是操作符处于情景2地方。以前面的示例为基础:

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中被重载
        # 在python中and不属于运算符,所以backtrader提供一个函数模拟这个功能
        sell_sig = bt.And(close_over_sma, sma_dist_small)

    def next(self):
        # 尽管这看起来不像是“操作号”,但确实返回的是正在测试对象的True/False
        if self.sma > 30.0:
            print('sma大于30.0')

        if self.sma > self.data.close:
            print('sma高于收盘价')

        if self.sell_sig:  # if sell_sig == True: would also be valid
            print('卖出标志为True')
        else:
            print('卖出标志为False')

        if self.sma_dist_to_high > 5.0:
            print('sma到high的距离大于5.0')

这不是一个非常有用的策略,只是一个例子。在情景2中,操作符返回期望值(如果测试值为True,则返回布尔值;如果是浮点数进行比较,则返回浮点数),并且算术运算也返回期望值。

注意:
为了进一步简化,比较实际上没有使用操做符。
if self.sma > 30.0: …比较 self.sma[0] 和 30.0
if self.sma > self.data.close: … 比较 self.sma[0] 和 self.data.close[0]

一些不可重载的运算符/函数

Python不允许重载所有内容,因此提供了一些功能函数来应对这种情况。

注意:仅适用于情景1,以创建对象供后面使用。

操作符:

  • and -> And
  • or -> Or

逻辑控制:

  • if -> If

函数:

  • any -> Any
  • all -> All
  • cmp -> Cmp
  • max -> Max
  • min -> Min
  • sum -> Sum
  • reduce -> Reduce

Sum实际上使用math.fsum作为底层操作,因为backtrader使用浮点数计算,如果用常规sum可能会影响精度。

这些实用的操作符/函数可迭代使用。可迭代的元素可以是常规的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  # do something here

例如sma1高于最高价,则必高于收盘价,这里重点是说明bt.And的用法。
bt.If用法:

class MyStrategy(bt.Strategy):

    def __init__(self):
        # 在period=15的data.close上生成SMA
        sma1 = btind.SMA(self.data.close, period=15)
        # 如果sma的值大于close,则返回low,否则返回high
        high_or_low = bt.If(sma1 > self.data.close, self.data.low, self.data.high)
        sma2 = btind.SMA(high_or_low, period=15)

解释说明:

  • 在period=15的data.close上生成sma1
  • 如果sma1的值大于close,则返回low,否则返回high
  • 调用bt.If时不会返回任何实际值。它返回一个Lines对象,就像SimpleMovingAverage一样,这些值将在稍后计算中会用到
  • 然后将bt.If生成的Lines对象赋值给sma2该,sma2有时会使用最低价,有时会使用高价进行计算

这些函数也可以使用数值,修改后得到示例:

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)

现在,sma2使用30.0或最高价进行计算,具体取决于sma1和close的比较结果。

注意:数值30在内部转换为伪迭代,始终返回30

你可能感兴趣的:(backtrader,python,开发语言)