概念
平台概念
这是平台某些概念的集合。它试图收集可在使用平台时有用的信息片段。
开始之前
所有小代码示例都假设以下导入可用:
import backtrader as bt import backtrader.indicators as btind import backtrader.feeds as btfeeds
注意
访问子模块的另一种替代语法,如指标和数据源:
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() ... data = btfeeds.MyFeed(...) cerebro.adddata(data) ... cerebro.addstrategy(MyStrategy, period=30) ...
注意以下内容:
- 策略的
__init__
方法未接收到*args
或**kwargs
(仍然可以使用它们)。 - 存在成员变量
self.datas
,其为包含至少一个项目的数组/列表/可迭代对象(希望至少有一个项目,否则将引发异常)。
是的。数据源被添加到平台上,它们将按照它们被添加到系统中的顺序显示在策略内部。
注意
这也适用于指标
,如果最终用户开发自己的自定义指标或者查看某些现有指标参考的源代码时。
数据源的快捷方式
可以直接使用附加自动成员变量访问 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) # This 2nd Moving Average operates using sma1 as "data" sma2 = btind.SimpleMovingAverage(sma1, period=self.p.period2) # New data created via arithmetic operation something = sma2 - sma1 + self.data.close # This 3rd Moving Average operates using something as "data" sma3 = btind.SimpleMovingAverage(something, period=self.p.period3) # Comparison operators work too ... greater = sma3 > sma1 # Pointless Moving Average of True/False values but valid # This 4th Moving Average operates using greater as "data" sma3 = btind.SimpleMovingAverage(greater, period=self.p.period4) ...
基本上,一旦被操作,一切都会转换为可以用作数据源的对象。
参数
平台中的几乎每个其他class
都支持参数的概念。
- 参数连同默认值声明为类属性(元组的元组或类似字典的对象)
- 关键字参数(
**kwargs
)被扫描以匹配参数,如果找到,则从**kwargs
中删除它们并将值分配给相应的参数。 - 参数最终可以通过访问成员变量
self.params
(简写:self.p
)在类的实例中使用。
前面的快速策略预览已经包含了一个参数示例,但为了冗余起见,再次,只关注参数。使用元组:
class MyStrategy(bt.Strategy): params = (('period', 20),) def __init__(self): sma = btind.SimpleMovingAverage(self.data, period=self.p.period)
并且使用一个dict
:
class MyStrategy(bt.Strategy): params = dict(period=20) def __init__(self): sma = btind.SimpleMovingAverage(self.data, period=self.p.period)
行
再次,平台上几乎每个其他对象都是Lines
启用的对象。从最终用户的角度来看,这意味着:
- 它可以容纳一个或多个线系列,其中线系列是一个值数组,将这些值放在一起形成一条线。
一个line(或lineseries)的很好的例子是由股票收盘价形成的线。这实际上是价格演变的一个众所周知的图表表示(称为Line on Close)
平台的常规使用只关注访问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')
已公开两个具有lines
的对象:
self.data
它有一个lines
属性,其中又包含一个close
属性self.movav
是一个SimpleMovingAverage
指标 它有一个lines
属性,其中又包含一个sma
属性
注
从这可以明显看出,lines
是有名称的。它们也可以按照声明顺序顺序访问,但这只应在Indicator
开发中使用
两个lines,即close
和sma
,都可以查询一个点(索引 0)以比较值。
有一种缩写访问行的方法存在:
xxx.lines
可以缩短为xxx.l
xxx.lines.name
可以缩短为xxx.lines_name
- 类似策略和指标的复杂对象提供了对数据线的快速访问
self.data_name
提供了对self.data.lines.name
的直接访问- 这也适用于编号的数据变量:
self.data1_name
->self.data1.lines.name
此外,线路名称可以直接访问:
self.data.close
和self.movav.sma
但是如果实际上正在访问行,则该符号并不像之前的符号那样清晰。
不是
使用这两个后续符号设置/分配行不受支持
Lines声明
如果正在开发一个Indicator,则必须声明该指标具有的lines。
正如与params一样,这次以类属性的形式发生,仅作为元组。不支持字典,因为它们不按插入顺序存储事物。
对于简单移动平均线,应该这样做:
class SimpleMovingAverage(Indicator): lines = ('sma',) ...
注
如果将单个字符串传递给元组,则元组中需要跟随声明的逗号,否则字符串中的每个字母都将被解释为要添加到元组中的项目。这可能是 Python 语法出错的几个情况之一。
如前面的例子所示,此声明在Indicator中创建了一个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
,省略 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:
。同样的情况不适用于带有以下理由的Indicators
:
Indicator
可能有一个属性close
,它保存一个中间计算结果,稍后交付给实际的lines
,也称为close
在数据源的情况下,不会进行任何计算,因为它只是一个数据源。
行长度
行有一组点,并在执行过程中动态增长,因此可以随时通过调用标准的 Python len
函数来测量长度。
这适用于例如:
- 数据源
- 策略
- 指标
数据源中存在另一个属性,当数据 预加载 时适用:
- 方法
buflen
该方法返回数据源可用的实际条形图数。
len
和 buflen
的区别
len
报告已处理了多少个条形图buflen
报告为数据源加载了多少个条形图
如果两者返回相同的值,则要么没有预加载数据,要么条形图的处理已消耗所有预加载的条形图(除非系统连接到实时数据源,否则这将意味着处理结束)
行和参数的继承
一种元语言用于支持参数和行的声明。为了使其与标准 Python 继承规则兼容,已经尽一切努力。
参数继承
继承应该按预期工作:
- 支持多重继承
- 从基类继承参数
- 如果多个基类定义了相同的参数,则使用继承列表中最后一个类的默认值
- 如果在子类中重新定义了相同的参数,则新的默认值将取代基类的默认值
行继承
- 支持多重继承
- 来自所有基类的行都会被继承。作为命名行,如果相同的名称在基类中使用了多次,则只会有一个版本的行
索引:0 和 -1
行如前所述是线系列,并在绘制时一起组成一条线(就像沿时间轴连接所有收盘价一样)
要在常规代码中访问这些点,选择使用基于0的方法来获取/设置当前get/set即时。
策略只获取值。指标还设置值。
在之前的快速策略示例中,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')
逻辑是通过应用索引0
获取移动平均值和当前收盘价的当前值。
注意
实际上,对于索引0
,并且在应用逻辑/算术运算符时,可以直接进行比较,如下所示:
if self.movav.lines.sma > self.data.lines.close: ...
在文档后面看运算符的解释。
设置是用于开发时使用的,例如,一个 Indicator,因为必须通过该指标设置当前输出值。
可以按以下方式计算当前获取/设置点的 SimpleMovingAverage:
def next(self): self.line[0] = math.fsum(self.data.get(0, size=self.p.period)) / self.p.period
访问先前设置的点是按照 Python 为访问数组/可迭代时定义的-1
进行建模
- 它指向数组的最后一项
平台将最后设置的项(当前实时获取/设置点之前)视为-1
。
因此,将当前close
与前一个close
进行比较是一个0
vs -1
的事情。例如,在策略中:
def next(self): if self.data.close[0] > self.data.close[-1]: print('Closing price is higher today')
当然,从-1
之前设置的价格将使用-2、-3、...
进行访问。
切片
backtrader不支持对lines对象进行切片,这是一种设计决策,遵循了[0]
和[-1]
索引方案。对于常规可索引的 Python 对象,您会执行以下操作:
myslice = self.my_sma[0:] # slice from the beginning til the end
但请记住,在选择为0
时……实际上是当前交付的值,后面没有了。另外:
myslice = self.my_sma[0:-1] # slice from the beginning til the end
再次……0
是当前值,-1
是最新(先前)的交付值。这就是为什么在backtrader生态系统中从0
-> -1
切片没有意义的原因。
如果支持切片,将如下所示:
myslice = self.my_sma[:0] # slice from current point backwards to the beginning
或:
myslice = self.my_sma[-1:0] # last value and current value
或:
myslice = self.my_sma[-3:-1] # from last value backwards to the 3rd last value
获取一个切片
仍然可以获取具有最新值的数组。语法:
myslice = self.my_sma.get(ago=0, size=1) # default values shown
这将返回一个具有1
值(size=1
)的数组,当前时刻0
作为向后查找的起始点。
要从当前时间点获取 10 个值(即:最后 10 个值):
myslice = self.my_sma.get(size=10) # ago defaults to 0
当然,数组具有您期望的顺序。最左边的值是最旧的值,最右边的值是最新的值(它是一个常规的 Python 数组,而不是一个lines对象)
要获取最后 10 个值,仅跳过当前点:
myslice = self.my_sma.get(ago=-1, size=10)
行:延迟索引
[]
操作符语法用于在next
逻辑阶段提取单个值。 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('Previous close is higher than the moving average')
在这里,正在使用(delay)
符号:
- 这提供了一个
close
价格的副本,但是延迟了-1
。
比较self.data.close(-1) > self.sma
生成另一个lines对象,如果条件为True
则返回1
,如果为False
则返回0
线耦合
运算符()
可以像上面显示的那样与delay
值一起使用,以提供lines对象的延迟版本。
如果使用语法WITHOUT提供delay
值,则返回一个LinesCoupler
lines对象。这旨在在操作datas具有不同时间框架的指标之间建立耦合。
具有不同时间框架的数据源具有不同的长度,在其上操作的指标复制数据的长度。例如:
- 每年的日数据源大约有 250 个柱状图
- 每年的周数据源有 52 个柱状图
尝试创建一个操作(例如),比较两个简单移动平均,每个操作在上述引用的数据上运行,会出现问题。不清楚如何将每日时间框架的 250 个柱状图与每周时间框架的 52 个柱状图匹配。
读者可以想象在后台进行date
比较,以找出一天 - 一周的对应关系,但是:
指标
只是数学公式,没有日期时间信息。
它们对环境一无所知,只知道如果数据提供足够的值,就可以进行计算。
()
(空调用)符号来拯救:
class MyStrategy(bt.Strategy): params = dict(period=20) def __init__(self): # data0 is a daily data sma0 = btind.SMA(self.data0, period=15) # 15 days sma # data1 is a weekly data sma1 = btind.SMA(self.data1, period=5) # 5 weeks sma self.buysig = sma0 > sma1() def next(self): if self.buysig[0]: print('daily sma is greater than weekly sma1')
在这里,较大时间框架指标sma1
与每日时间框架耦合为sma1()
。这返回一个与sma0
的更大柱状图兼容的对象,并复制由sma1
产生的值,有效地将 52 周柱状图分散在 250 日柱状图中
BackTrader 中文文档(二)(2)https://developer.aliyun.com/article/1489215