平台概念#
这是平台概念的一些集合。它试图收集可以在使用平台时有用的信息片段。
开始之前 *** ** ** **
所有的小型代码示例都假设以下导入可用:
import backtrader as bt import backtrader.indicators as btind import backtrader.feeds as btfeeds
注意
访问子模块如 indicators 和 feeds 的另一种语法:
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 ,即 close
和 sma
可以从点( 索引 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.close
和self.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
方法:
- 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”点的简单移动平均值:
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
的事情。例如,在策略中:
- if self.data.close[0] > self.data.close[-1]:
print(‘今天的收盘价较高’)
当然,在逻辑上, -1
之前的“set”价格将以 -2,-3,...
进行访问。
切片 *** ** **
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 的初始化期间展示了更完整的用例:
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 的对象。
以下是一个生成非常简单的购买信号的示例代码:
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=15
的SMA
计算- 然后如果 sma 的值大于
close
,则返回low
,否则返回high
请记住,当调用
bt.If
时,并不会返回实际值。它返回一个类似于 SimpleMovingAverage 的 Lines 对象。系统运行时将会计算这些值。
生成的
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)
- 现在,第二个移动平均线根据
sma
和close
的逻辑状态,选择使用30.0
或者high
价格进行计算。.. 注意:: 值
30
在内部被转换为一个伪可迭代对象,该对象总是返回30