You are here:  Home » 量化交易与机器学习 » backtrader » 动量策略实例源码 – backtrader中文教程

参数:字典与元组的元组
许多随 backtrader 提供的示例以及文档和/或博客中提供的示例,都使用元组模式作为参数。 例如从代码:

class Momentum(bt.Indicator):
    lines = ('trend',)
    params = (('period', 90),)

连同这种范式,人们总是有机会使用 dict.

class Momentum(bt.Indicator):
    lines = ('trend',)
    params = dict(period=90)  # or params = {'period': 90}

随着时间的推移,这变得更易于使用,并成为作者的首选模式。

Tips:作者更喜欢dict(period=90),更容易输入,不需要引号。但是花括号符号 ,{'period': 90}是许多其他人的首选。

dict和方法之间的根本区别tuple

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

在下面作者修改的示例中,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)

使用 force,即:使用已经存在的东西,比如 PeriodN指标,它:

  • 已经定义了一个period参数并且知道如何将它传递给系统

因此,这可能会更好

class Momentum(bt.ind.PeriodN):
    lines = ('trend',)
    params = dict(period=50)

    def next(self):
        ...

我们已经跳过了__init__为 using 的唯一目的而定义的需要,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用于数据馈送。一般传递数据馈送,它将使用关闭。这似乎无关紧要,但在尝试使代码在任何地方都保持通用(如指标)时确实有帮助

一个人会/应该考虑的第一件事:如果可能,将所有内容都保留为参数。因此

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

  • next当所有缓冲区(指标、数据馈送)至少可以提供数据点时,backtrader调用。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)]
    ...

这意味着只有d_with_lenfrom的子集self.datas可以用于保证。

因为在策略的整个生命周期中进行这种计算似乎毫无意义,所以可以进行这样的优化

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

  • 添加第二个计时器,也适用于星期五
  • 使用计数器仅对每个 2 nd调用执行操作,甚至可以在计时器本身中使用allow=callable参数

计时器甚至可以更好地用于实现以下模式:

  • rebalance_portfolio每个月的第 2和第 4星期五
  • rebalance_positions仅在每个月的第四个星期五

一些额外的东西

其他一些事情可能纯粹是个人品味的问题。

个人品味 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 在代码的几个部分中使用了 a :将其设为参数。与其他值相同0.001100实际上已经建议作为创建移动平均线的参数)

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

评论被关闭。