平台概念#

这是平台概念的一些集合。它试图收集可以在使用平台时有用的信息片段。

开始之前 *** ** ** **

所有的小型代码示例都假设以下导入可用:

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

注意

访问子模块如 indicatorsfeeds 的另一种语法:

import backtrader as bt

然后:

thefeed = bt.feeds.OneOfTheFeeds(…) theind = bt.indicators.SimpleMovingAverage(…)数据提要 - 传递数据源

*** ** ** ** ** ** ** ** ** ** **

与平台的工作将以 策略*为基础。而这些策略将会接收 数据源*。平台最终用户不需要关心如何接收它们:

数据源会以数组的形式自动提供给策略作为成员变量,并提供了数组位置的快捷方式

快速预览一个派生自策略类的声明和运行平台的例子:

class MyStrategy(bt.Strategy):

params = dict(period=20)

def __init__(self):

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

cerebro = bt.Cerebro()

…```python

import btfeeds from my_strategy import MyStrategy

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

cerebro.addstrategy(MyStrategy, period=30)

Notice the following:

  • 策略的 __init__ 方法没有接收到任何 *args 或`**kwargs`参数(但仍然可以使用它们)

  • 存在一个名为 self.datas 的成员变量,它是一个数组/列表/可迭代对象,至少包含一个项目(否则将引发异常)

如此而已。 数据源(Data Feeds) 被添加到平台后,它们将按照被添加到系统中的顺序在策略中显示。

注意

这也适用于“指标(Indicators)”。如果最终用户开发自定义指标或查看现有:ref:`indautoref`的源代码时,该规则也适用。

数据源的快捷方式#

self.datas 数组的项目可以使用额外的自动成员变量直接访问:- 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)

    ...

省略数据提供源#

可以进一步简化以上示例代码为:

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

    def __init__(self):``sma = btind.SimpleMovingAverage(period=self.params.period)

    ...

“self.data”已从“SimpleMovingAverage”的调用中完全删除。如果这样做,指标(在本例中为“SimpleMovingAverage”)将接收到创建它的对象(即“策略”)中的第一个数据,即“self.data”(也称为“self.data0”或“self.datas [0]”)。

几乎所有都是“数据源”#

不仅“数据源”是数据,并且可以传递。 “指标”和“操作”的结果也是数据。

在前面的例子中,“SimpleMovingAverage”将“self.datas [0]”作为输入进行操作。包含操作和额外指标的示例:

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)

#这第2个移动平均使用sma1作为“data” sma2 = btind.SimpleMovingAverage(sma1,period = self.p.period2)# 通过算术运算创建的新数据

something = sma2 - sma1 + self.data.close

# 这个第三个移动平均线使用something作为”data” sma3 = btind.SimpleMovingAverage(something, period=self.p.period3)

# 比较运算符也能够工作… greater = sma3 > sma

# 无意义的真/假值移动平均线,但是是有效的 # 这个第四个移动平均线使用greater作为”data” sma3 = btind.SimpleMovingAverage(greater, period=self.p.period4)

基本上,所有的东西都会被转换成一个对象,一旦它们被处理之后就可以作为数据源使用。

参数#

大部分平台中的其他”类”都支持”参数”的概念。

  • 带有默认值的参数被声明为类属性(元组的元组或类似字典的对象)

  • 关键字参数( `` kwargs`` )会被扫描以寻找匹配的参数,如果找到则从 `` kwargs`` 中删除,并将值赋给对应的参数

  • 最后,可以通过访问成员变量 self.params (简写: self.p )来在类的实例中使用参数

前面的快速策略预览已经包含了一个参数的例子,但是为了重复起见,我们只关注参数。使用 tuples 的方式:```python class MyStrategy(bt.Strategy):

params = ((‘period’, 20),)

def __init__(self):

# Get the Simple Moving Average line sma = btind.SimpleMovingAverage(self.data, period=self.p.period)

And using a dictionary:

```python class MyStrategy(bt.Strategy):

params = dict(period=20)

def __init__(self):

# Get the Simple Moving Average line sma = btind.SimpleMovingAverage(self.data, period=self.p.period)

```

Lines *** **

再一次,平台中的大多数对象都是 Lines 对象。从终端用户的角度来看,这意味着:

  • 它可以包含一个或多个线系列,其中线系列是将值放在一起形成一条线的值数组。

一个好的例子是某只股票收盘价格形成的线(或线系列)。这实际上是股票价格演变的众所周知的图表表示(称为“收盘线”)。

使用平台通常只涉及 访问 lines 。前面的迷你策略示例再次有用: ```python class MyStrategy(bt.Strategy):

params = ((‘period’, 20),)

def __init__(self):

# 获取简单移动平均线 sma = btind.SimpleMovingAverage(self.data, period=self.p.period)

``````python 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(‘简单移动平均线大于收盘价’)

两个包含 lines 属性的对象已经暴露出来:

  • self.data 它有一个 lines 属性,并包含一个 close 属性

  • self.movav 是一个 SimpleMovingAverage 指标 它有一个 lines 属性,并包含一个 sma 属性

而且, lines ,即 closesma 可以从点( 索引 0 )进行查询并比较值。

还存在一种简写访问方式:

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

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

  • Strategies 和 Indicators 这类复杂对象提供了快速访问数据的 lines

`- ``self.data_name``提供了直接访问``self.data.lines.name``的方式 - 同样适用于编号数据变量: ``self.data1_name -> self.data1.lines.name

此外,可以直接访问线路名称:

  • 通过 self.data.closeself.movav.sma

但是,与前面的表示不同,这种表示方式无法清楚地表明是否正在访问 lines

注意

不支持使用这两种后记表示设置/分配行。

Lines 声明


如果正在开发一个 Indicator ,则必须声明指标所拥有的 lines

params 一样,这是作为类属性来完成的,但这次仅限于元组。不支持使用字典,因为它们不按插入顺序存储事物。

以简单移动平均为例,可以这样做:

class SimpleMovingAverage(Indicator):

lines = (‘sma’,)…

注意

在元组中,声明后面的*逗号*是必须的,如果将单个字符串传递给元组, 否则字符串中的每个字母都会被解释为要添加到元组中的项。 这可能是 Python 语法中少数几个出错的地方。

如前面的例子所示,此声明在*指标*中创建了一条 sma 线, 可以在策略的逻辑中(可能还有其他指标)后续访问该线,以创建更复杂的指标。

对于开发来说,有时以通用的非命名方式访问线路非常有用,这就是编号访问的方便之处:

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

如果定义了更多的行,可以通过索引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


数据源 中, lines 也可以省略来访问它们。这样做可以更自然地处理 close 价格之类的东西。

例如:

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

...

class MyStrategy(bt.Strategy):

    ...

    def next(self):

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

这看起来比也有效的方式:if self.data.lines.close[0] > 30.0: 更自然。对于``指标``,情况不一样,原因是:- 指示器(Indicator) 可以具有一个名为 close 的属性,其中保存着一个中间计算结果,该结果稍后传递给名为 close 的实际 lines (传输线)。

对于 数据源(Data Feeds) 来说,不进行任何计算,因为它只是一个数据来源。

传输线长度#

传输线在执行过程中会动态增长,因此可以随时通过调用Python的标准 len 函数来测量其长度。

这适用于例如:

  • 数据源(Data Feeds)

  • 策略(Strategies)

  • 指示器(Indicators)

在数据被 预加载 的情况下,另外一个属性适用于 数据源(Data Feeds)

  • 方法 buflen

该方法返回 数据源(Data Feed) 可用的实际柱数量。

len 和`buflen`之间的区别 - len 报告已处理的条的数量
  • buflen 报告数据源已加载的总条数

如果两者返回相同的值,那么要么没有预加载的数据,要么条的处理已经消耗了所有的预加载条(除非系统连接了实时数据源,否则这意味着处理结束)

Lines和Params的继承#

一种”元语言” 用于支持 Params 和*Lines*的声明。已经尽力与Python的标准继承规则兼容。

继承应该按预期工作:

  • 支持多重继承

  • 继承基类的Params

  • 如果多个基类定义了相同的param,则使用继承列表中最后一个类的默认值

  • 如果在子类中重新定义了相同的param,则新的默认值将覆盖基类的默认值

  • 支持多重继承

  • 继承所有基类的Lines。如果在基类中多次使用了相同的名称,则只会有一个版本的线路

索引:0 和 -1 *** ** ** ** ** ** ** ** ** Lines*如前所述是线系列,它们由一系列的点组成,当这些点在时间轴上连接在一起时形成一条线(就像将所有收盘价沿时间轴连接在一起时)

要在常规代码中访问这些点,选择使用以 0 为基础的方法来获取/设置当前的 get/set 实例。

策略仅用于 获取*值。指标还可 设置*值。

从前面简单策略示例中可以简单看到 next 方法:

```python def next(self):

if self.movav.lines.sma[0] > self.data.lines.close[0]:

print(‘Simple Moving Average is greater than the closing price’)

```

该逻辑是通过应用索引 0 来获取移动平均线和当前收盘价的当前值。

注意

实际上,对于索引 0 和应用逻辑/算术运算符进行比较时,可以直接进行比较,如下所示:

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

``` 请看本文档后面的操作符解释。

设置被用于开发一个`指标`,因为指标需要通过设置当前输出值来进行计算。可以按照以下方式计算当前“get/set”点的简单移动平均值:

```python def next(self):

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

```

访问前一个“set”点是根据Python对数组/可迭代对象的定义进行建模:

  • 它指向数组的最后一个元素

该平台认为在当前实时的“get/set”点之前,最后一个“set”项为 -1

因此,将当前的“close”与 前一个 “close”进行比较是一个 0-1 的事情。例如,在策略中:

```python def next(self):

if self.data.close[0] > self.data.close[-1]:

print(‘今天的收盘价较高’)

```

当然,在逻辑上, -1 之前的“set”价格将以 -2,-3,... 进行访问。

切片 *** ** **

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

myslice = self.my_sma[0:] # 从开头到结尾取一个切片

但请记住,选择0意味着选择当前的值,之后没有值了。另外:

myslice = self.my_sma[0:-1] # 从开头到结尾减去1取一个切片

再次说明…0表示当前的值,-1表示最新(前一个)的值。这就是为什么从0到-1的切片在 backtrader 生态系统中没有意义。

如果将来支持切片,它可能会是这样的:

myslice = self.my_sma[:0] # 从当前点向前切片到开头

或者:

myslice = self.my_sma[-1:0] # 最后一个值和当前值

或者:

myslice = self.my_sma[-3:-1] # 从最后一个值向前切片到倒数第三个值 ```获取切片 =========

仍然可以获取具有最新值的数组。语法为:

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

这将返回一个包含 1 个值( size=1 )的数组,以当前时刻 0 作为向后查找的起点。

要从当前时刻获取10个值(即最后10个值):

myslice = self.my_sma.get(size=10)  # 默认时刻为0

当然,数组的顺序是您期望的。最左侧的值是最旧的值,最右侧的值是最新的值(它是一个普通的Python数组,而不是一个 lines 对象)

要跳过仅当前时刻并获取最后10个值:

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

行:延迟索引 *** ** ** ** ** ** [] 操作符的语法用于在“next”逻辑阶段提取单个值。 Lines 对象还支持通过 延迟的Lines对象 来通过 __init__ 阶段访问值的附加表示法。

假设我们对逻辑感兴趣的是将之前的 close 值与 简单移动平均线 的实际值进行比较。可以在每个“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(‘之前的收盘价高于移动平均线’)

在这里,使用了”(delay)”符号:

  • 这会生成一个 close 价格的副本,但是延迟了“-1”。

    而比较 self.data.close(-1) > self.sma 生成另一个 lines 对象,如果条件为 True ,则返回 1 ,如果为 False ,则返回 0

Lines耦合 **************操作符 () 可以像上面展示的那样与 delay 值一起使用,以提供 lines 对象的延迟版本。

如果使用语法时 未提供 delay 值,则返回一个 LinesCoupler lines 对象。它旨在在具有不同时间框架的 datas 之间建立耦合。

具有不同时间框架的数据源具有不同的 长度,在其上操作的指标会复制数据的长度。例如:

  • 每年的日数据源大约有250个柱状图

  • 每年的周数据源有52个柱状图

尝试创建一个操作(例如)来比较两个在上述数据上操作的 简单移动平均线 将会失败。不清楚如何将每日时间框架的250个柱状图与每周时间框架的52个柱状图配对。

读者可以想象背后进行了一种 date 比较,以找出一天 - 周的对应关系,但是:

  • Indicators 只是数学公式,不包含 datetime 信息

    它们对环境一无所知,只知道如果数据提供了足够的值,就可以进行计算。

空调用符 () 来拯救我们: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(‘每日均线大于每周均线1’)

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

运算符,使用自然结构 *** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** **

为了实现“易用性”的目标,该平台允许(在Python的限制下)使用运算符。为了进一步增强此目标,使用运算符分为两个阶段。

阶段1 - 运算符创建对象#

一个示例已经被看到,即使并非特意如此。在初始化阶段(__init__方法)中,像指标和策略这样的对象会由运算符创建可以进行操作、赋值或保留为后续在策略逻辑的评估阶段中使用的对象。再次给出 SimpleMovingAverage 的一个潜在实现,进一步分解成步骤。

SimpleMovingAverage 指标的 __init__ 方法中的代码可能如下所示:

```python
def __init__(self):

# 求 N 期值的和 - datasum 现在是一个 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

```

在 Strategy 的初始化期间展示了更完整的用例:

```python

class MyStrategy(bt.Strategy):

```def __init__(self):

sma = btind.SimpleMovingAverage(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 对象, 可以在策略的逻辑中使用,指示条件是否满足。

第二阶段 - 自然的运算符#

首先我们要记住,策略有一个 next 方法,在系统处理每个柱时调用这个方法。在第二阶段,运算符实际处于模式2中。基于前面的例子:

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

  self.sma = sma = btind.SimpleMovinAverage(self.data, period=20)

  close_over_sma = self.data.close > sma
  self.sma_dist_to_high = self.data.high - sma

  sma_dist_small = sma_dist_to_high < 3.5

  # 不幸的是,在 Python 中无法重写"and",因为它是一种语言结构而不是运算符,所以平台必须提供一个函数来模拟它

  self.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 高于收盘价’)```python

if self.sell_sig: # 如果 sell_sig 为 True,也可以写成:if sell_sig == True:

print(‘sell sig 为 True’)

else:

print(‘sell sig 为 False’)

if self.sma_dist_to_high > 5.0:

print(‘距离 sma 到高点的距离大于 5.0’)

这不是一个非常有用的策略,只是一个例子。在阶段 2 中,操作符返回预期的值(如果测试真值则返回布尔值,如果与浮点数进行比较则返回浮点数),并且算术运算也会返回预期的结果。

一些未覆盖的操作符/函数#

Python 不允许覆盖所有内容,因此提供了一些函数来处理这些情况。

操作符:逻辑控制:

  • and -> And

  • or -> Or

逻辑控制:

  • if -> If

函数:

  • any -> Any

  • all -> All

  • cmp -> Cmp

  • max -> Max

  • min -> Min

  • sum -> Sum

    Sum 实际上使用 math.fsum 作为底层操作,因为该平台处理的是浮点数,而应用常规的 sum 可能会影响精度。

  • reduce -> Reduce

这些实用运算符/函数用于操作可迭代对象。可迭代对象的元素可以是常规的 Python 数值类型(整数、浮点数,…),也可以是具有 Lines 的对象。

以下是一个生成非常简单的购买信号的示例代码:

class MyStrategy(bt.Strategy):```python

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 # 在这里做些什么

```

显然,如果 sma1 高于最高价,则它肯定比收盘价更高。但是,重点是展示了 bt.And 的用法。

使用 bt.If

```python 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)

```

解析:

  • data.close 进行 period=15SMA 计算- 然后

    • 如果 sma 的值大于 close ,则返回 low ,否则返回 high

      请记住,当调用 bt.If 时,并不会返回实际值。它返回一个类似于 SimpleMovingAverageLines 对象。

      系统运行时将会计算这些值。

    • 生成的 bt.If Lines 对象随后被输送到第二个 SMA 中,该对象有时会使用 low 价格,有时会使用 high 价格进行计算。

这些 函数 也接受数值参数。下面是一个带有修改的示例:

``` python 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)

```

现在,第二个移动平均线根据 smaclose 的逻辑状态,选择使用 30.0 或者 high 价格进行计算。.. 注意::

30 在内部被转换为一个伪可迭代对象,该对象总是返回 30