Backtrader动量策略

  |  

在另一篇伟大的文章中,泰迪·科克(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使用参数保留声明的顺序,这在枚举它们时可能很重要。

    提示

    对于Python3.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 指针,该指针必须定义一个属性,该属性 funcperiod 作为参数传递柱线,并将返回值放入定义的 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 计数器。策略和大多数对象的长度由系统一直提供,计算和更新。

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

  • 当所有缓冲区(指针、data feeds)至少可以提供数据点时,backtrader调用next100-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
        ...

当满足保证时,将停止调用保护计算prenextnextstart 然后调用它,通过覆盖它,我们可以重置 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.001100 )相同(实际上已经建议将其作为创建移动平均线的参数)

将所有内容作为参数允许打包代码,并通过更改策略的实例化而不是策略本身来尝试不同的事情。

推荐阅读

相关文章

Backtrader向 OHLC 提供买入价/卖出价数据

最近,backtrader通过实现line覆盖来运行从 ohlc-land 逃逸,这允许重新定义整个层次结构,例如,具有仅具有 bid,ask 和 datetime lines的data feeds。

Backtrader按日线交易

似乎在世界某个地方有一种权益(Interest)可以总结如下: 使用每日柱线引入订单,但使用开盘价 这来自工单#105订单执行逻辑与当前数据和#101动态投注计算中的对话 backtrader 尝试尽可能保持现实,并且在处理每日柱线时适用以下前提: 当每日柱被评估时,柱线已经结束 这是有道理的,

Backtrader对逐笔报价数据重新采样

backtrader 已经可以从分钟数据中重新采样。接受价格变动数据不是问题,只需将 4 个常用字段(open、 high、 low、 close)设置为价格变动值。 但是传递要重新采样的逐笔报价数据再次生成相同的数据。作为或版本 1.1.11.88,情况已不再如此。

Backtrader期货补偿与现货补偿

版本1.9.32.116 增加了对社区中呈现的有趣用例 的支持 以期货开始交易,包括实物交割 让一个指针告诉你一些事情 如果需要, close 现货价格操作,有效地取消实物交割,无论是为了接收货物还是为了必须交付货物(并希望获利)来头寸。

Backtrader教程:日志记录 - 编写器

将以下内容写出到流中: csv 流,

Backtrader教程:数据馈送 - 展期交割

并非每个供应商都为可以交易的工具提供连续的未来。有时提供的数据是仍然有效的到期日期的数据,即:仍在交易的日期 这在回溯测试方面并不是很有帮助,因为数据分散在几个不同的仪器上,这些仪器另外...时间重叠。 能够正确地将这些仪器的数据从过去连接到连续的流中,可以减轻疼痛。

数据多时间帧

有时投资决策是使用不同的时间框架做出的: 每周评估趋势 每天执行条目 或者5分钟对60分钟。 这意味着需要将多个时间帧的数据组合在 backtrader 中以支援此类组合。 对它的本机支持已经内置。

Backtrader教程:日期时间 - 管理

在 1.5.0 版之前, backtrader 使用直接的方法来进行时间管理,因为数据源计算的任何日期时间都只是按面值使用。 对于任何用户输入也是如此,例如可以提供给任何数据源的参数fromdate (或 sessionstart)的情况 考虑到直接控制冻结的数据源以进行回溯测试,这种方法很好。

Backtrader教程:数据馈送 - 熊猫

注意 pandas 并且必须安装其依赖项 支持Pandas Dataframes似乎受到很多人的关注,他们依赖于已经可用的解析代码来分析不同的数据源(包括CSV)和Pandas提供的其他功能。 数据馈送的重要声明。 注意 这些只是 声明。不要盲目拷贝此代码。

Backtrader动量策略

在另一篇伟大的文章中,泰迪·科克(Teddy Koker)再次展示了算法交易策略的发展之路: 研究优先应用 pandas 回溯测试,然后使用 backtrader 荣誉!!! 该帖子可以在以下位置找到: 泰迪·科克(Teddy Koker)给我留言,问我是否可以评论 backtrader的用法。