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(...)
在本系统的操作都是通过策略,而策略需要传递数据源。
最终用户不需要太关注怎么接收数据源,它将自动把数组格式的成员变量交给策略,并提供访问数组位置的快捷方式。
先来快速浏览一下一个策略派生类的定义和如何在平台中运行:
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数组。
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)
几乎所有对象在被操作时都转为一个可以数据源对象。
几乎类都支持参数使用。
之前的例子已经包括了参数,不过为了去掉其他内容,只关注参数,下面给出两个例子:
使用元组:
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)
平台中几乎所有对象也是支持线
的对象。以用户的角度看,这意味着这些对象可以拥有一个或多个线
序列。一个线
序列就是一组按顺序排列成线的数值。
一个很好的例子是股票收盘价组成的线
(或者说线序列)。这也是一个广为人知的价格排列方法,即收盘线。使用平台时通常只关心怎么访问线
。把上面的策略的小例子稍微扩展一下:
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是有名字的(close,sma)。也可以使用声明顺序依次访问,但是只能在开发指标时这样做。
所有lines都可以用下标访问(close[0],sma[0])。
线
的快捷访问方式有:
还可以这样访问:
self.data.close以及self.movav.sma
不过这种方式不像之前的方式那样明确线序列是否真正被访问到。
注意:不能用后两种方式对线对象赋值。
在指标开发过程中必须声明其拥有的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的区别:
如果两者返回相同数字,要不是没有数据被提前加载,要不是所有提前加载的数据都已经处理完了。这意味着如果没有接入实时数据,程序处理已经结束。
线和参数定义了一些元语言,用于使其适应标准的python继承规则。
如前所述,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时计算)
运算符()可以与延迟的数值一起使用,以提供延迟的line对象。
如果使用中不提供延迟数值,则返回LinesCoupler对象。这是为了在操作具有不同时间范围的数据指标之间建立耦合。
不同时间范围的交易数据具有不同的长度,并且指标在操作这些数据时会复制数据的长度。例如:
尝试创建一个比较两个简单移动平均线的操作,每次操作在引用数据时都有可能被中断。因为系统不知道如何在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的语法范围内)使用操作符。为了进一步简单化,操作符的使用有两种情景。
我们之前已经看到了一个例子。在指标和策略类的对象初始化阶段(__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对象,当指示满足条件时,可以直接在策略中使用。
首先,策略的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,以创建对象供后面使用。
操作符:
逻辑控制:
函数:
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)
解释说明:
这些函数也可以使用数值,修改后得到示例:
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