量化交易学习(十二)backtrader规范化创建指标

今天这篇是backtrader文档的学习笔记。主要介绍了怎么规范化(声明式)及不规范化(命令式)创建自定义指标的方法,通过声明式创建指标可以少写很多代码。

官方文档链接:https://www.backtrader.com/blog/2019-07-08-canonical-or-not/canonical-or-not/

规范 vs 非规范指标 (Canonical vs Non-Canonical Indicators)

这个问题经常以各种形式出现,例如:“如何使用 backtrader 最佳/规范地实现这个或那个指标?”

backtrader 的目标之一是尽可能灵活地支持各种情况和用例,因此答案很简单:“有多种方式”。对于指标(这是最常出现问题的对象),有以下三种实现方式:

  1. 完全声明式地在 init 方法中实现。
  2. 完全一步一步命令式地在 next 方法中实现。
  3. 将上述两种方式结合起来用于复杂场景,其中声明式部分无法涵盖所有必要的计算。

快速浏览 backtrader 中内置的指标,可以发现它们都以 声明式 方式实现。原因如下:

  • 更容易编写
  • 更容易阅读
  • 更优雅
  • 矢量化和基于事件的实现是自动管理的

自动向量化

如果一个指标完全在 init 方法内部实现,Python 的元类和运算符重载的魔力将提供以下特性:

  • 向量化实现(回测运行时的默认设置)
  • 基于事件的实现(例如用于实时交易)

另一方面,如果指标的任何部分在 next 方法中实现:

  • 这段代码可以直接在事件回调中运行。
  • 向量化将通过在后台为每个数据点调用 next 方法来模拟。

注意:这意味着即使特定指标没有矢量化实现,所有其他具有矢量化实现的指标仍然会以矢量化方式运行。

以资金流指数为例:

社区用户 @Rodrigo Brito 发布了一个使用 next 方法实现的“资金流量指数”(MFI)指标版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class MFI(bt.Indicator):
lines = ('mfi', 'money_flow_raw', 'typical', 'money_flow_pos', 'money_flow_neg')

plotlines = dict(
money_flow_raw=dict(_plotskip=True),
money_flow_pos=dict(_plotskip=True),
money_flow_neg=dict(_plotskip=True),
typical=dict(_plotskip=True),
)

params = (
('period', 14),
)

def next(self):
typical_price = (self.data.close[0] + self.data.low[0] + self.data.high[0]) / 3
money_flow_raw = typical_price * self.data.volume[0]

self.lines.typical[0] = typical_price
self.lines.money_flow_raw[0] = money_flow_raw

self.lines.money_flow_pos[0] = money_flow_raw if self.lines.typical[0] >= self.lines.typical[-1] else 0
self.lines.money_flow_neg[0] = money_flow_raw if self.lines.typical[0] <= self.lines.typical[-1] else 0

pos_period = math.fsum(self.lines.money_flow_pos.get(size=self.p.period))
neg_period = math.fsum(self.lines.money_flow_neg.get(size=self.p.period))

if neg_period == 0:
self.lines.mfi[0] = 100
return

self.lines.mfi[0] = 100 - 100 / (1 + pos_period / neg_period)

除最后一行计算 mfi 指标之外的所有行都可以进行优化。

我们以 StockCharts 的“资金流指数”定义,来看看上面的实现是否正确。这里“资金流指数”定义的链接:https://school.stockcharts.com/doku.php?id=technical_indicators:money_flow_index_mfi

下面是一个 MFI 指标的 规范 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
class MFI_Canonical(bt.Indicator):
lines = ('mfi',)
params = dict(period=14)

def __init__(self):
tprice = (self.data.close + self.data.low + self.data.high) / 3.0
mfraw = tprice * self.data.volume

flowpos = bt.ind.SumN(mfraw * (tprice > tprice(-1)), period=self.p.period)
flowneg = bt.ind.SumN(mfraw * (tprice < tprice(-1)), period=self.p.period)

mfiratio = bt.ind.DivByZero(flowpos, flowneg, zero=100.0)
self.l.mfi = 100.0 - 100.0 / (1.0 + mfiratio)

应该马上就能注意到以下几点:

  • 只定义了一条线 mfi,没有临时线变量。
  • 代码看起来更简洁,没有使用 [0] 数组索引。
  • 没有任何单独的 if 语句,代码更加紧凑易读。
  • 如果将这两个版本的指标都针对相同的数据集绘制成图表,结果如下:
    图片

图表显示,规范版本和非规范版本的值和趋势几乎完全相同,但在开始部分存在差异。

  • 非规范版本从一开始就提供数值,但这些数值是无意义的。 这是因为它无法正确计算初始值。在开始时,它会输出 100.0,然后再输出一个额外的值,但那个值也不正确。
  • 相比之下,规范版本会自动在达到最小预热期后开始提供数值。 不需要任何人工干预。

受影响区域的放大图片如下:
图片

图片显示,规范版本在达到最小预热期后开始提供值,而非规范版本从一开始就提供值。 规范版本的值是正确的,而非规范版本的值是无意义的。

当然,可以通过以下方式缓解非规范版本中的这种情况:

  • 继承自 bt.ind.PeriodN 类,该类已经拥有 period 参数并知道如何处理它(并在 init 方法中调用 super)。

注意,规范版本也像非规范版本的逐步执行代码一样,考虑公式中可能出现的除零情况。

非规范版本的除零处理

1
2
3
4
5
if neg_period == 0:
self.lines.mfi[0] = 100
return

self.lines.mfi[0] = 100 - 100 / (1 + pos_period / neg_period)

规范版本的除零处理

1
2
mfiratio = bt.ind.DivByZero(flowpos, flowneg, zero=100.0)
self.l.mfi = 100.0 - 100.0 / (1.0 + mfiratio)

相比于拥有多行代码、一个 return 语句以及对输出线进行不同赋值的操作的非规范版本,规范版本仅通过单个 mfiratio 计算声明和一次赋值(遵循 StockCharts 公式)操作就完成了对输出线 mfi 的赋值。

结论

希望通过以上对比,能帮助您更好地理解在规范方式(即在 init 方法中声明式实现)和非规范方式(在 next 方法中逐步执行并使用数组索引)实现某些功能时可能存在的差异。

江达小记