动量策略
原文: https://www.backtrader.com/blog/2019-05-20-momentum-strategy/momentum-strategy/
在另一篇伟大的帖子中,泰迪·科克再次展示了算法交易策略的发展路径:
- 研究首次应用
pandas
- 回溯测试,然后使用
backtrader
荣誉
该职位可在以下网址找到:
Teddy Koker给我发了条短信,问我是否可以对backtrader的用法发表评论。下面是我的意见。这只是我个人的拙见,因为作为backtrader的作者,我对如何最好地使用该平台存在偏见。
而我个人对于如何制定特定结构的品味,不必与其他人喜欢使用平台的方式相匹配。
笔记
事实上,让平台开放来插入几乎任何东西,用不同的方式来做同样的事情,是一个有意识的决定,让人们以他们认为合适的方式使用它(在平台目标、语言可能性和我所做的失败设计决定的限制范围内)
在这里,我们将只关注本可以以不同方式完成的事情。“不同”是否更好始终是一个意见问题。而backtrader的作者并不一定总是对“更好”与“backtrader”(因为实际开发必须适合开发者,而不是“backtrader”的作者)
参数:dict
vstuple of tuples
许多随backtrader
提供的样本以及文档和/或博客中提供的样本都使用tuple of tuples
模式作为参数。例如,代码:
class Momentum(bt.Indicator):
lines = ('trend',)
params = (('period', 90),)
与这个范例一起,人们总是有机会使用dict
。
class Momentum(bt.Indicator):
lines = ('trend',)
params = dict(period=90) # or params = {'period': 90}
随着时间的推移,这已成为更轻的使用,并成为首选模式的作者。
笔记
作者更喜欢dict(period=90)
,因为它更容易打字,不需要引用。但是花括号符号{'period': 90}
是许多其他符号的首选。
dict
和tuple
方法之间的潜在差异:
-
使用
tuple of tuples
参数时,保留声明的顺序,这在枚举它们时可能很重要。提示
Python
3.7
中的默认有序字典的声明顺序应该没有问题(如果使用CPython即使是实现细节,也应该使用3.6
)
在下面作者修改的示例中,将使用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__
的必要性,因为addminperiod
只应在例外情况下使用。
为了继续,backtrader定义了一个OperationN
指标,该指标必须定义了一个func
属性,它将获得作为参数传递的period
条,并将返回值放入定义的行中。
考虑到这一点,我们可以将以下代码想象为潜在代码
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
作为数据馈送。一般地传递数据提要,它将使用 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
引用)
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
转发不是一个好主意。
-
反向交易者在所有缓冲区(指示器、数据馈送)至少可以传递数据点时调用
next
。100-bar
移动平均线显然只有在数据馈送中有 100 个数据点时才会传递。这意味着当输入
next
时,数据馈送将有100 data points
需要检查,移动平均线仅为1 data point
-
backtrader提供
prenext
作为钩子,让开发者在上述保证能够满足之前访问东西。例如,当有多个数据源正在使用且它们的开始日期不同时,这非常有用。开发人员可能希望在满足所有数据源(和相关指标)的所有保证以及第一次调用next
之前进行一些检查或采取一些措施。
在一般情况下,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()
因为在原始代码中还有一个rebalance_positions
每10
条发生一次,所以可以:
-
增加一个 2nd计时器,同样适用于周五
-
使用计数器仅作用于每个 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
让一切都成为参数。例如,在上面的几行中,我们看到一个0.2
,它用于代码的几个部分:将其作为参数。与其他值相同,如0.001
和100
(实际上已经建议将其作为创建移动平均线的参数)
将所有内容作为参数,只需更改策略的实例化,而不是策略本身,就可以打包代码并尝试不同的事情。