在另一篇伟大的文章中,泰迪·科克(Teddy Koker)再次展示了算法交易策略的发展之路:
- 研究优先应用
pandas - 回溯测试,然后使用
backtrader
荣誉!!!
该帖子可以在以下位置找到:
泰迪·科克(Teddy Koker)给我留言,问我是否可以评论 backtrader的用法。我的观点可以 在下面看到。这只是我个人的拙见,因为作为 backtrader 的作者,我对如何最好地使用这个平台有偏见。
而我个人对如何制定某些结构的品味,不必与其他人喜欢使用平台的方式相匹配。
注意
实际上,让平台 open 插入几乎任何东西,并用不同的方式做同样的事情,是一个有意识的决定,让人们以他们认为合适的方式使用它(在平台目标,语言可能性和我做出的失败的设计决策的限制范围内)。
在这里,我们将只关注本来可以以不同的方式完成的事情。“不同”是否更好总是一个观点问题。 backtrader 的作者并不总是必须正确使用「backtrader」进行开发实际上「更好」(因为实际开发必须适合开发人员而不是」backtrader」的作者)
参数:dict vs tuple of tuples
文档和/或博客中提供backtrader 的许多范例都使用 tuple of tuples 该模式作为参数。例如,从代码中:
class Momentum(bt.Indicator):
lines = ('trend',)
params = (('period', 90),)
与这种范式一起,人们总是有机会使用adict。
class Momentum(bt.Indicator):
lines = ('trend',)
params = dict(period=90) # or params = {'period': 90}
随着时间的流逝,这已经变得更容易使用,并成为作者的首选模式。
注意
作者更喜欢dict(period=90),更容易输入,不需要引号。但是大括弧表示法 {'period': 90},是许多其他人的首选。
和 tuple 方法之间的dict根本区别:
-
tuple of tuples使用参数保留声明的顺序,这在枚举它们时可能很重要。提示
对于Python
3.7中的缺省有序字典,声明顺序应该没有问题(3.6如果使用CPython,即使它是一个实现细节)
在下面作者修改的范例中dict ,将使用符号。
指针Momentum
在本文中,这是指针的定义方式
class Momentum(bt.Indicator):
lines = ('trend',)
params = (('period', 90),)
def __init__(self):
self.addminperiod(self.params.period)
def next(self):
returns = np.log(self.data.get(size=self.p.period))
x = np.arange(len(returns))
slope, _, rvalue, _, _ = linregress(x, returns)
annualized = (1 + slope) ** 252
self.lines.trend[0] = annualized * (rvalue ** 2)
使用力,即:使用已经存在的东西, PeriodN 如指针,它:
- 已经定义了一个
period参数,并知道如何将其传递给系统
因此,这可以更好
class Momentum(bt.ind.PeriodN):
lines = ('trend',)
params = dict(period=50)
def next(self):
...
我们已经跳过了为使用 addminperiod的唯一目的而定义__init__的需求,这应该只在特殊情况下使用。
为了继续, backtrader 定义了一个OperationN 指针,该指针必须定义一个属性,该属性 func 将 period 作为参数传递柱线,并将返回值放入定义的 line中。
考虑到这一点,人们可以想像以下内容是潜在的代码
def momentum_func(the_array):
r = np.log(the_array)
slope, _, rvalue, _, _ = linregress(np.arange(len(r)), r)
annualized = (1 + slope) ** 252
return annualized * (rvalue ** 2)
class Momentum(bt.ind.OperationN):
lines = ('trend',)
params = dict(period=50)
func = momentum_func
这意味着我们已经将指针的复杂性排除在指针之外。我们甚至可以从外部库导入momentum_func ,如果底层函数发生变化,指针也无需更改即可反映新行为。作为奖金,我们有 纯粹的 声明性指针。否 __init__、否 addminperiod 和否 next
战略
让我们看一下这部分__init__ 。
class Strategy(bt.Strategy):
def __init__(self):
self.i = 0
self.inds = {}
self.spy = self.datas[0]
self.stocks = self.datas[1:]
self.spy_sma200 = bt.indicators.SimpleMovingAverage(self.spy.close,
period=200)
for d in self.stocks:
self.inds[d] = {}
self.inds[d]["momentum"] = Momentum(d.close,
period=90)
self.inds[d]["sma100"] = bt.indicators.SimpleMovingAverage(d.close,
period=100)
self.inds[d]["atr20"] = bt.indicators.ATR(d,
period=20)
关于样式的一些内容:
-
尽可能使用参数,而不是固定值
-
使用较短的名称和较短的名称(例如,对于导入),在大多数情况下,它将提高可读性
-
充分利用 Python
-
不要用于
closedata feed。通常传递data feed,它将使用 close。这可能看起来不相关,但是当尝试在任何地方保持代码通用时(如在指针中),它确实有所说明。
人们应该/应该考虑的第一件事是:如果可能的话,将所有内容作为参数。因此
class Strategy(bt.Strategy):
params = dict(
momentum=Momentum, # parametrize the momentum and its period
momentum_period=90,
movav=bt.ind.SMA, # parametrize the moving average and its periods
idx_period=200,
stock_period=100,
volatr=bt.ind.ATR, # parametrize the volatility and its period
vol_period=20,
)
def __init__(self):
# self.i = 0 # See below as to why the counter is commented out
self.inds = collections.defaultdict(dict) # avoid per data dct in for
# Use "self.data0" (or self.data) in the script to make the naming not
# fixed on this being a "spy" strategy. Keep things generic
# self.spy = self.datas[0]
self.stocks = self.datas[1:]
# Again ... remove the name "spy"
self.idx_mav = self.p.movav(self.data0, period=self.p.idx_period)
for d in self.stocks:
self.inds[d]['mom'] = self.p.momentum(d, period=self.momentum_period)
self.inds[d]['mav'] = self.p.movav(d, period=self.p.stock_period)
self.inds[d]['vol'] = self.p.volatr(d, period=self.p.vol_period)
params通过使用和更改一些命名约定,我们使__init__(以及随之而来的策略)完全可定制和通用(没有spy引用任何hwere)
next 及其 len
backtrader 尝试在可能的情况下使用Python范例。它肯定会失败,但它会尝试。
让我们看看在next
def next(self):
if self.i % 5 == 0:
self.rebalance_portfolio()
if self.i % 10 == 0:
self.rebalance_positions()
self.i += 1
这就是Pythonlen 范式说明的地方。让我们使用它
def next(self):
l = len(self)
if l % 5 == 0:
self.rebalance_portfolio()
if l % 10 == 0:
self.rebalance_positions()
如您所见,没有必要保留self.i 计数器。策略和大多数对象的长度由系统一直提供,计算和更新。
next 和 prenext
代码包含此转发
def prenext(self):
# call next() even when data is not available for all tickers
self.next()
而且进入时没有 保障 next
def next(self):
if self.i % 5 == 0:
self.rebalance_portfolio()
...
好吧,我们知道正在使用无生存偏差的数据集,但通常不保护prenext => next 转发不是一个好主意。
-
当所有缓冲区(指针、data feeds)至少可以提供数据点时,backtrader调用
next。100-bar移动平均线显然只有在具有来自data feed的100个数据点时才会提供。这意味着在进入
next时,必须100 data points检查data feed,移动平均线1 data point -
backtrader提供
prenext作为钩子,让开发人员在满足上述保证之前访问内容。例如,当多个data feeds正在运行并且它们的开始日期不同时,这很有用。开发人员可能希望在满足next所有data feeds(和相关指针)的所有保证并首次被要求之前进行一些检查或采取行动。
在一般情况下,prenext => next 转发应该有一个这样的保护设备:
def prenext(self):
# call next() even when data is not available for all tickers
self.next()
def next(self):
d_with_len = [d for d in self.datas if len(d)]
...
这意味着只有 来自的self.datas子集d_with_len可以保证使用。
注意
类似的保护必须用于指针。
因为在策略的整个生命周期内进行这种计算似乎毫无意义,因此可以进行这样的优化。
def __init__(self):
...
self.d_with_len = []
def prenext(self):
# Populate d_with_len
self.d_with_len = [d for d in self.datas if len(d)]
# call next() even when data is not available for all tickers
self.next()
def nextstart(self):
# This is called exactly ONCE, when next is 1st called and defaults to
# call `next`
self.d_with_len = self.datas # all data sets fulfill the guarantees now
self.next() # delegate the work to next
def next(self):
# we can now always work with self.d_with_len with no calculation
...
当满足保证时,将停止调用保护计算prenext , nextstart 然后调用它,通过覆盖它,我们可以重置 list 保存数据集的哪个,作为完整的数据集,即: self.datas
有了这个,所有的警卫都被移除了next。
next 带计时器
虽然作者在这里的意图是每5/10天重新平衡(投资组合/头寸),但这可能意味着每周/每两周重新平衡。
如果出现以下情况,该len(self) % period 方法将失败:
-
数据集不是在星期一开始的
-
在交易假期期间,这将使再平衡变得不一致
为了克服这一点,可以使用内置功能 backtrader
使用它们将确保在预期发生时进行再平衡。让我们想像一下,其目的是在星期五重新平衡。
让我们在我们的策略中params__init__加入一些魔力
class Strategy(bt.Strategy):
params = dict(
...
rebal_weekday=5, # rebalance 5 is Friday
)
def __init__(self):
...
self.add_timer(
when=bt.Timer.SESSION_START,
weekdays=[self.p.rebal_weekday],
weekcarry=True, # if a day isn't there, execute on the next
)
...
现在我们已经准备好知道什么时候是星期五。即使星期五碰巧是交易假期,添加weekcarry=True 也可以确保我们在星期一收到通知(如果星期一也是假期,则为星期二或...
计时器的通知已接收notify_timer
def notify_timer(self, timer, when, *args, **kwargs):
self.rebalance_portfolio()
因为原始代码中的每个柱线10也会发生一个rebalance_positions,所以可以:
-
添加第 2个计时器 ,也适用于星期五
-
使用计数器仅对每个第 2 个 调用运行操作,这甚至可以在计时器本身中使用
allow=callable参数
注意
计时器甚至可以更好地用于实现以下模式:
-
rebalance_portfolio每月第2和第 4个星期 五 -
rebalance_positions仅限每月第 4个星期五
一些额外功能
其他一些事情可能纯粹是个人品味的问题。
个人品味 1
始终使用预先构建的比较,而不是在过程中next比较内容。例如,从代码(多次使用)
if self.spy < self.spy_sma200:
return
我们可以做以下事情。第一个期间__init__
def __init__(self):
...
self.spy_filter = self.spe < self.spy_sma200
以及后来的
if self.spy_filter:
return
考虑到这一点,如果我们想改变spy_filter 条件,我们只需要在代码中的 __init__ 多个位置运行此操作一次,而不必这样做。
这同样适用于这里的其他比较d < self.inds[d]["sma100"] :
# sell stocks based on criteria
for i, d in enumerate(self.rankings):
if self.getposition(self.data).size:
if i > num_stocks * 0.2 or d < self.inds[d]["sma100"]:
self.close(d)
它也可以在期间__init__ 预先构建,因此更改为类似的东西
# sell stocks based on criteria
for i, d in enumerate(self.rankings):
if self.getposition(self.data).size:
if i > num_stocks * 0.2 or self.inds[d]['sma_signal']:
self.close(d)
个人品味 2
使所有内容都成为参数。例如,在上面的 lines 中,我们看到一个0.2 在代码的几个部分中使用的: 使其成为一个参数。与其他值(如 0.001 和 100 )相同(实际上已经建议将其作为创建移动平均线的参数)
将所有内容作为参数允许打包代码,并通过更改策略的实例化而不是策略本身来尝试不同的事情。