动量策略
原文:
www.backtrader.com/blog/2019-05-20-momentum-strategy/momentum-strategy/
在另一篇很棒的文章中,Teddy Koker 再次展示了 算法交易 策略的发展路径:
- 首先应用
pandas
进行研究 - 使用
backtrader
进行回测
真棒!!!
文章可以在以下位置找到:
Teddy Koker 给我留了言,询问我是否可以评论 backtrader 的使用。我的观点可以在下面看到。这仅仅是我的个人意见,因为作为 backtrader 的作者,我对如何最好地使用该平台有偏见。
我个人对某些结构如何表述的偏好,不必与其他人使用平台的偏好相匹配。
注意
实际上,让平台能够插入几乎任何内容,并以不同的方式执行相同的操作,是一个有意识的决定,让人们按照他们认为合适的方式使用它(在平台旨在实现的约束条件、语言可能性和我所做的失败设计决定的范围内)。
在这里,我们只关注可以以不同方式完成的事情。是否 “不同” 更好总是一个看法问题。而 backtrader 的作者并不总是必须在使用 backtrader 进行开发时始终正确(因为实际的开发必须适合开发者,而不是 backtrader 的作者)
参数:dict
vs tuple 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
参数保留了声明顺序,这在枚举参数时可能很重要。
提示
在 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
指标,它必须具有定义的属性 func
,该属性将作为参数传递 period
个 bars,并将返回值放入定义的线中。
有了这个想法,一个人可以将以下内容想象成潜在的代码
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
Python 的 len
范式正是所需之处。让我们来使用它。
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
转发不是一个好主意。
- backtrader 在所有缓冲区(指标、数据源)至少可以提供一个数据点时调用
next
。一个100-bar
移动平均线显然只有在从数据源获取了 100 个数据点时才会提供数据。
这意味着在进入next
时,数据源将有100 个数据点
可供检查,而移动平均值只有1 个数据点
- 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()
因为原始代码中还有每10
个条形图进行一次rebalance_positions
,所以可以:
- 添加第 2 个定时器,也适用于星期五
- 使用计数器只在每第 2 次调用时执行操作,甚至可以在定时器本身使用
allow=callable
参数
注意
定时器甚至可以更好地用于实现模式,比如:
- 每月的第 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
(实际上已经建议将其作为创建移动平均值的参数)。
将所有东西都作为参数,可以通过只改变策略的实例化而不是策略本身来打包代码并尝试不同的方法。
2018
改进随机的 Python 互联网学习笔记
原文:
www.backtrader.com/blog/posts/2018-04-22-improving-code/improving-code/
每隔一段时间,互联网上会出现带有backtrader代码的样本。在我看来有几个是中文。最新的一个在这里:
标题是:backtrader-学习笔记 2,显然(谢谢谷歌)被翻译为backtrader-学习笔记 2。如果那些是学习笔记,让我们尝试在那里真正可以改进代码的地方进行改进,在我个人看来,那就是backtrader最擅长的地方。
在学习笔记中策略的__init__
方法中,我们发现以下内容
def __init__(self): ... self.ma1 = bt.indicators.SMA(self.datas[0], period=self.p.period ) self.ma2 = bt.indicators.SMA(self.datas[1], period=self.p.period )
这里没有什么好争论的(风格是非常个人的事情,我不会触及那方面)
在策略的next
方法中,以下是买入和卖出的逻辑决策。
... # Not yet ... we MIGHT BUY if ... if (self.ma1[0]-self.ma1[-1])/self.ma1[-1]>(self.ma2[0]-self.ma2[-1])/self.ma2[-1]: ...
和
... # Already in the market ... we might sell if (self.ma1[0]-self.ma1[-1])/self.ma1[-1]<=(self.ma2[0]-self.ma2[-1])/self.ma2[-1]: ...
这两个逻辑块是可以做得更好的,这样也会增加可读性、可维护性和调整性(如果需要的话)
不是将移动平均值的比较(当前点0
和上一个点-1
)后跟一些除法,让我们看看如何让它预先计算。
让我们调整__init__
def __init__(self): ... # Let's create the moving averages as before ma1 = bt.ind.SMA(self.data0, period=self.p.period) ma2 = bt.ind.SMA(self.data1, period=self.p.period) # Use line delay notation (-x) to get a ref to the -1 point ma1_pct = ma1 / ma1(-1) - 1.0 # The ma1 percentage part ma2_pct = ma2 / ma2(-1) - 1.0 # The ma2 percentage part self.buy_sig = ma1_pct > ma2_pct # buy signal self.sell_sig = ma1_pct <= ma2_pct # sell signal
现在我们可以将其带到next
方法并执行以下操作:
def next(self): ... # Not yet ... we MIGHT BUY if ... if self.buy_sig: ... ... # Already in the market ... we might sell if self.sell_sig: ...
注意,我们甚至不必使用self.buy_sig[0]
,因为通过if self.buy_sig
进行的布尔测试已经被backtrader机制翻译成了对[0]
的检查
在我看来,通过在__init__
中使用标准算术和逻辑操作来定义逻辑(并使用行延迟符号(-x)
)使代码变得更好。
无论如何,作为结束语,人们也可以尝试使用内置的PercentChange
指标(又名PctChange
)
正如名称所示,它已经计算了一定周期内的百分比变化。现在__init__
中的代码看起来是这样的
def __init__(self): ... # Let's create the moving averages as before ma1 = bt.ind.SMA(self.data0, period=self.p.period) ma2 = bt.ind.SMA(self.data1, period=self.p.period) ma1_pct = bt.ind.PctChange(ma1, period=1) # The ma1 percentage part ma2_pct = bt.ind.PctChange(ma2, period=1) # The ma2 percentage part self.buy_sig = ma1_pct > ma2_pct # buy signal self.sell_sig = ma1_pct <= ma2_pct # sell signal
在这种情况下,并没有太大的区别,但如果计算更大更复杂的话,这绝对可以为你省下很多麻烦。
祝愉快的回溯交易!
一个动态指标
原文:
www.backtrader.com/blog/posts/2018-02-06-dynamic-indicator/dynamic-indicator/
指标是棘手的东西。不是因为它们在一般情况下很难编码,而是因为名称具有误导性,并且人们对指标有不同的期望。
让我们至少尝试定义在backtrader生态系统内什么是指标。
它是一个定义了至少一个输出线的对象,可能定义影响其行为的参数,并接受一个或多个数据源作为输入。
为了尽可能使指标通用,选择了以下设计原则:
- 输入数据源可以是任何看起来像数据源的东西,这带来了一个直接的优势:因为其他指标看起来像数据源,所以可以将指标作为输入传递给其他指标。
- 不携带
datetime
线负载。这是因为输入可能没有自己的datetime
负载进行同步。并且根据系统范围内的通用datetime
进行同步可能是不正确的,因为指标可能正在使用来自周时间框架的数据,而系统时间可能以秒为单位进行计时,因为这是多个数据源之一的最低分辨率。 - 操作必须是幂等的,即:如果使用相同的输入两次调用且参数没有更改,则输出必须相同。
请注意,可以要求指标在相同的时间点使用相同的输入多次执行操作。尽管这似乎是不需要的,但如果系统支持数据重放(即:从较小的时间框架实时构建较大的时间框架),则需要这样做。 - 最后:指标将其输出值写入当前时间的时刻,即:索引
0
。否则它将被命名为Study
。Study
将寻找模式并在过去写入输出值。
例如请参阅Backtrader 社区 - ZigZag
一旦(在backtrader生态系统中)定义清晰,让我们尝试看看如何实际编写一个动态指标。似乎我们不能,因为从上述的设计原则来看,指标的操作过程多多少少是……不可变的。
自从……以来的最高高点
通常启动的一个指标是Highest
(别名为MaxN
),以获得给定周期内的最高某物。如
import backtrader as bt class MyStrategy(bt.Strategy) def __init__(self): self.the_highest_high_15 = bt.ind.Highest(self.data.high, period=15) def next(self): if self.the_highest_high_15 > X: print('ABOUT TO DO SOMETHING')
在此代码片段中,我们实例化Highest
以跟踪最近 15 个周期内的最高高点。如果最高高点大于X
,将执行某些操作。
这里的关键是:
- 周期被固定为
15
使其动态
有时,我们需要指标是动态的,并且根据实时条件改变其行为。例如,请参阅 backtrader 社区中的这个问题:自开仓以来的最高高点
当然,我们不知道何时会开/平仓,并且将 period
设置为固定值如 15
是没有意义的。让我们看看我们如何做,将所有东西打包到一个指标中
动态参数
我们首先将使用我们将在指标生命周期中更改的参数,通过它实现动态性。
import backtrader as bt class DynamicHighest(bt.Indicator): lines = ('dyn_highest',) params = dict(tradeopen=False) def next(self): if self.p.tradeopen: self.lines.dyn_highest[0] = max(self.data[0], self.dyn_highest[-1]) class MyStrategy(bt.Strategy) def __init__(self): self.dyn_highest = DynamicHighest(self.data.high) def notify_trade(self, trade): self.dyn_highest.p.tradeopen = trade.isopen def next(self): if self.dyn_highest > X: print('ABOUT TO DO SOMETHING')
让我们来吧!我们拥有它,到目前为止我们还没有违反为我们的指标制定的规则。让我们看看指标
- 它定义了一个名为
dyn_highest
的输出 line - 它有一个参数
tradeopen=False
- (是的,它接受数据源,只是因为它是
Indicator
的子类) - 如果我们总是使用相同的输入调用
next
,它将始终返回相同的值
唯一的事情:
- 如果参数的值发生变化,则输出也会发生变化(上面的规则说,只要参数不发生变化,输出保持不变)
我们在 notify_trade
中使用这个来影响我们的 DynamicHighest
- 我们使用通知的
trade
的值isopen
作为一个标志,以知道我们是否需要记录输入数据的最高点 - 当
trade
关闭时,isopen
的值将为False
,我们将停止记录最高值
供参考:Backtrader Documentation Trade
简单!!!
使用方法
有些人会反对修改在指标声明中的 param
,并且应该只在实例化期间设置。
好的,让我们使用一个方法。
import backtrader as bt class DynamicHighest(bt.Indicator): lines = ('dyn_highest',) def __init__(self): self._tradeopen = False def tradeopen(self, yesno): self._tradeopen = yesno def next(self): if self._tradeopen: self.lines.dyn_highest[0] = max(self.data[0], self.dyn_highest[-1]) class MyStrategy(bt.Strategy) def __init__(self): self.dyn_highest = DynamicHighest(self.data.high) def notify_trade(self, trade): self.dyn_highest.tradeopen(trade.isopen) def next(self): if self.dyn_highest > X: print('ABOUT TO DO SOMETHING')
并没有太大的区别,但现在指标有了一些额外的样板代码,包括 __init__
和方法 tradeopen(self, yesno)
。但是我们的 DynamicHighest
的动态性是相同的。
BackTrader 中文文档(十五)(2)https://developer.aliyun.com/article/1505364