在另一篇伟大的文章中,泰迪·科克(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
-
不要用于
close
data 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
)相同(实际上已经建议将其作为创建移动平均线的参数)
将所有内容作为参数允许打包代码,并通过更改策略的实例化而不是策略本身来尝试不同的事情。