Skip to content

动量策略

原文: https://www.backtrader.com/blog/2019-05-20-momentum-strategy/momentum-strategy/

在另一篇伟大的帖子中,泰迪·科克再次展示了算法交易策略的发展路径:

  • 研究首次应用pandas
  • 回溯测试,然后使用backtrader

荣誉

该职位可在以下网址找到:

Teddy Koker给我发了条短信,问我是否可以对backtrader的用法发表评论。下面是我的意见。这只是我个人的拙见,因为作为backtrader的作者,我对如何最好地使用该平台存在偏见。

而我个人对于如何制定特定结构的品味,不必与其他人喜欢使用平台的方式相匹配。

笔记

事实上,让平台开放来插入几乎任何东西,用不同的方式来做同样的事情,是一个有意识的决定,让人们以他们认为合适的方式使用它(在平台目标、语言可能性和我所做的失败设计决定的限制范围内)

在这里,我们将只关注本可以以不同方式完成的事情。“不同”是否更好始终是一个意见问题。而backtrader的作者并不一定总是对“更好”“backtrader”(因为实际开发必须适合开发者,而不是“backtrader”的作者)

参数:dictvstuple 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}是许多其他符号的首选。

dicttuple方法之间的潜在差异:

  • 使用tuple of tuples参数时,保留声明的顺序,这在枚举它们时可能很重要。

    提示

    Python3.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计数器。策略和大多数对象的长度一直由系统提供、计算和更新。

nextprenext

代码包含此转发

 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转发不是一个好主意。

  • 反向交易者在所有缓冲区(指示器、数据馈送)至少可以传递数据点时调用next100-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_positions10条发生一次,所以可以:

  • 增加一个 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.001100(实际上已经建议将其作为创建移动平均线的参数)

将所有内容作为参数,只需更改策略的实例化,而不是策略本身,就可以打包代码并尝试不同的事情。


我们一直在努力

apachecn/AiLearning

【布客】中文翻译组