之前的文章“在 Elasticsearch 中回测 RSI 交叉策略”,我们介绍了如何使用 Elasticsearch 实现相对强弱指数 (RSI) 指标以获得回测结果。 文章中相对强弱指数的计算使用了简单移动平均 (SMA) 函数。 然而,原作者威尔斯(J. Welless Widler) 使用了他自创的平滑方法,是一种不同形式的指数平均。 在本文中,我们将使用不同的移动平均函数进行深入研究,并检验哪个方法可以得到更好的回测结果。 建议读者快速浏览一下我之前的文章,对使用Elasticsearch的实现细节有一个基本的了解。
相对强弱指数于 1978 年发表在威尔德的著作“New Concepts in Technical Trading Systems“ , 中文翻译本为《技术交易系统新概念》。 该书还介绍了其他著名的指标,包括抛物线转向指标(PSAR)、平均真实范围 (ATR) 和平均趋向指数 (ADX) 等等 。相对强弱指数是一个动量指标,提供有关价格变化的信息,以支持买卖资产的机会。价格变化被转换成近期平均收益(gain)和近期平均损失(loss)两种数据。它的方程可以写成如下,其中 MAgain,n 和 MAloss,n 是近期收益的移动平均和近期亏损的移动平均。 默认情况下,周期 n 为 14。
相对强弱指数交叉策略定义了交叉处的指定值,例如其值大于70为超买信号和少于30为超卖信号。对于其他值,只能等待信号的产生。本文探讨使用不同类型的移动平均函数所产生的相对强弱指数,会对回测结果产生重大影响。威尔德对时间戳t 处的近期增益(Gaint)及近期损失(Losst)的平滑方法写成如下。
根据维基百科(Wikipedia)的移动平均文档,威尔德的平滑平均属于修正移动平均 (MMA)、运行移动平均 (RMA) 或平滑移动平均 (SMMA) 的类型。 当n=14时,平滑移动平均的第一个有效值为前14个周期的平均值。 此后,时间戳 t 的 SMMA 将等于前一个时间戳的平滑移动平均的 13/14加上当前值的 1/14。
根据上述文章,指数移动平均 (EWMA)可以写成如下,其中N是指数移动平均中的周期,而n是平滑移动平均中的周期。
因此,n周期的平滑移动平均相当于N=2n-1周期的指数移动平均。由于n=14是相对强弱指数的默认值,当使用指数移动平均时,周期N为27的α值为0.071428571。在本文中,我们尝试展示移动平均函数对相对强弱指数交叉策略的影响是显着的。使用图表来观察值的变化要容易得多。我们尝试将回溯测试应用于免佣金交易所交易基金 (commission-free ETF),并专注于将 Elasticsearch 作为分析工具。 下面的例子随机选择了“Fidelity International Multifactor ETF”。 其股票代码为FDEV。 将随机抽取另外10只ETF运行,最终结果将在稍后公布。 数据选自Investors Exchange(IEX)提供从2021-02-01 到 2021-05-31 之间的时间范围。。在以下两个图表中,来自不同类型移动平均函数的 相对强弱指数与每日收盘价一起绘制。第一个图表显示了来自 SMMA14 和 EWMA27L 的 相对强弱指数,其中 EWMA27L 表示计算使用较长的移动窗口,大约 110 个数据点,用于 Elasticsearch 的 EWMA 函数。根据上述等式,当前EWMA的计算应该涉及所有先前数据点,所以RSI_EWMA27L和RSI_SMMA14两条线在开始时有点差距,然后在 2021-03-30 左右开始重叠。这种现象表明在开始时没有足够的数据点来计算第一个有效值,读者应注意使用足够数量的数据点来计算第一个有效值。但是,在图中显示RSI_SMMA14并不敏感,不会产生任何高于 70 或低于 30 的值。
第二张图表显示了SMA14和EWMA27S的相对强弱指数,其中 EWMA27S 表示计算使用较短的移动窗口,27 个数据点,用于 Elasticsearch 的 EWMA 函数。 EWMA27S似乎过于敏感并且会产生大量信号,而 SMA14只是仅仅可以接受。
以下阐释如何使用Elasticsearch实现RSI 交叉策略。 假设有一个填充有数据的 Elasticsearch 索引,其使用的数据映射与之前发表的文章相同。以下步骤演示了 REST API 请求正文的代码。
通过搜索操作收集所有相关文档
使用带有必要条件(must)子句的布林查询(bool query)来收集股票代码为FDEV和日期从2021年02月01日到2021年05月31日的文档。 由于需要计算滑动窗口,因此增加了一个半月的数据(从2020年12月15日到2021年01月31日)。
{
"query": {
"bool": {
"must": [
{"range": {"date": {"gte": "2020-12-15", "lte": "2021-05-31"}}},
{"term": {"symbol": "FDEV"}}
]
}
},
从交易日选择文件
使用名为Backtest_RSIs日期直方图(date_histogram)存储桶聚合,并配合参数field(字段)为date和interval(间隔)为 1d(1天)。由于没有内插数据,为了过滤非交易日(空桶),使用名为 SDaily 的“bucket_selector”聚合来选择文档计数(_count)大于 0 的桶。
"aggs": {
"Backtest_RSIs": {
"date_histogram": {
"field": "date",
"interval": "1d",
"format": "yyyy-MM-dd"
},
"SDaily": {
"bucket_selector": {
"buckets_path": {"count":"_count"},
"script": "params.count > 0"
}
},
提取每日的收盘价
由于子聚合使用管道(pipeline)聚合而无法直接采用文档字段,所以额外使用 “平均”聚合,名为 Close,用于检索收盘价。
"aggs": {
"Close": {
"avg": {"field": "close"}
},
计算连续两个交易日收盘价的差值
使用名为DClose的导数(derivative)聚合,并配合参数buckets_path指定Close聚合的值来确定它与前一个时间戳的值。
"DClose": {
"derivative": {"buckets_path": "Close"}
},
过滤掉没有连续两个交易日收盘价差值的文件
使用名为 SDClose 的“bucket_selector”聚合和参数“buckets_path”来指定“DClose” ,选择那些 DClose 值大于 0 的桶。
"SDClose":{
"bucket_selector": {
"buckets_path": {
"PDlose": "DClose"
},
"script": "params.DClose > 0"
}
},
确定连续两个交易日收盘价差值是收益还是损失
使用两个“bucket_script”聚合,命名为 Gain 和 Loss,并使用参数“buckets_path”指定 DClose 聚合的结果来确定值。 两个值都必须是正值或为零值。
"Gain": {
"bucket_script": {
"buckets_path": {DClose": "DClose"},
"script": "(params.DClose > 0) ? params.DClose : 0"
}
},
"Loss": {
"bucket_script": {
"buckets_path": {"DClose": "DClose"},
"script": "(params.DClose < 0) ? -params.DClose : 0"
}
},
计算第 6 项收益和损失的 14 天周期简单移动平均线
使用两个“moving_fn”聚合,命名为 GainSMA14 和 LossSMA14,参数窗口(window)为 14,参数“buckets_path”分别为增益和损失。 参数“shift”设置为 1 以包含当天和过去 13 个交易日的数据。 SMA 是使用未加权平均函数 (MovingFunctions.unweightedAvg) 计算。
"GainSMA14": {
"moving_fn": {
"script": "MovingFunctions.unweightedAvg(values)",
"window": 14, "buckets_path": "Gain", "shift":1
}
},
"LossSMA14": {
"moving_fn": {
"script": "MovingFunctions.unweightedAvg(values)",
"window": 14, "buckets_path": "Loss", "shift":1
}
},
计算第 6 项收益和损失的 27 天周期指数移动平均值
使用四个“moving_fn”聚合,命名为 GainEWMA27S、LossEWMA27S、GainEWMA27L 和 LossEWMA27L,参数窗口为 27 表示使用较短的移动窗口。参数窗口为110 表示使用较长的移动窗口。 参数“buckets_path”分别为增益和损失。 参数“shift”设置为 1 以包含当天和过去交易日的数据。 EWMA 是使用指数移动平均函数 (MovingFunctions.ewma) 计算的,α 值等于 0.071428571。
"GainEWMA27S": {
"moving_fn": {
"script": "MovingFunctions.ewma(values, 0.071428571)",
"window": 27, "buckets_path": "Gain", "shift":1
}
},
"LossEWMA27S": {
"moving_fn": {
"script": "MovingFunctions.ewma(values, , 0.071428571)",
"window": 27, "buckets_path": "Loss", "shift":1
}
},
"GainEWMA27L": {
"moving_fn": {
"script": "MovingFunctions.ewma(values, 0.071428571)",
"window": 110, "buckets_path": "Gain", "shift":1
}
},
"LossEWMA27L": {
"moving_fn": {
"script": "MovingFunctions.ewma(values, , 0.071428571)",
"window": 110, "buckets_path": "Loss", "shift":1
}
},
计算RSIs值
使用三个“bucket_script”聚合,命名为 RSISMA14、RSIEWMA27S 和 RSIEWMA27L。使用参数“buckets_path”来指定来自它们的 GainSMA 和 LossSMA。 然后根据方程写下脚本来计算相应的相对强弱指数指标。
"RSISMA14": {
"bucket_script": {
"buckets_path": {"GainSMA14": "GainSMA14", "LossSMA14": "LossSMA14"},
"script": "100 - 100/(1+params.GainSMA14/params.LossSMA14)"
}
},
"RSIEWMA27S": {
"bucket_script": {
"buckets_path": {"GainEWMA27S": "GainEWMA27S", "LossEWMA27S": "LossEWMA27S"},
"script": "100 - 100/(1+params.GainEWMA27S/params.LossEWMA27S)"
}
},
"RSIEWMA27L": {
"bucket_script": {
"buckets_path": {"GainEWMA27L": "GainEWMA27L", "LossEWMA27L": "LossEWMA27L"},
"script": "100 - 100/(1+params.GainEWMA27L/params.LossEWMA27L)"
}
},
由于SMMA14的定义涉及到它的前一个值,所以Elasticsearch中没有办法指定这样的公式。 如果读者知道怎么做,请发表评论以贡献您的工作。 SMMA14 的计算和交叉交易策略需要编写程序。
如果使用 Python 编程语言,主程序可以写成四个部分。
- 读取开始日期、结束日期、符号代码和包含使用 JSON 格式的 Elasticsearch REST API 请求正文的文件名的命令行参数。
- 根据参数从Elasticsearch服务器获取数据。
- 解析响应数据并处理买卖交易。
- 报告每个 RSI 的回测统计数据(为简单起见,利润并未扣除交易费用)。
主函数如下所示:
def main(argv):
input_file, start_date, end_date, symbol = get_opt(argv)
resp = get_data(input_file, start_date, end_date, symbol)
transactions = parse_data(resp, start_date)
report(transactions, 'RSISMA14')
report(transactions, 'RSIEWMA27S')
report(transactions, 'RSISMMA14')
在本文中,仅显示了 SMMA 的代码段和买入或卖出信号的确定。 读者可以进一步参考 Gitee上的开源项目Backtest_rsis。 parse_data() 函数如下所示。 最后,交易数组将包含有效信号。 由于 RSIEWMA27L 的 RSI 等同于 RSISMMA14 的 RSI,因此跳过 RSIEWMA27L 的交易。
# parse the response data and refine the buy/sell signal
def parse_data(resp, start_date):
result = json.loads(resp)
if "status" in result:
print("Return status: %s, error: %s" % (result['status'], result['error']))
sys.exit(-1)
aggregations = result['aggregations']
if aggregations and 'Backtest_RSIs' in aggregations:
Backtest_RSI = aggregations['Backtest_RSIs']
transactions = []
initGain = 0
initLoss = 0
wsGain = 0
wsLoss = 0
count = 0
if Backtest_RSI and 'buckets' in Backtest_RSI:
for bucket in Backtest_RSI['buckets']:
transaction = {}
transaction['date'] = bucket['key_as_string']
transaction['Close'] = bucket['Close']['value']
transaction['RSISMA14'] = "hold" if 'RSISMA14' not in bucket else ("buy" if bucket['RSISMA14']['value'] < 30 else ("sell" if bucket['RSISMA14']['value'] > 70 else "hold"))
transaction['RSIEWMA27S'] = "hold" if 'RSIEWMA27S' not in bucket else ("buy" if bucket['RSIEWMA27S']['value'] < 30 else ("sell" if bucket['RSIEWMA27S']['value'] > 70 else "hold"))
count += 1
if count <= 14:
initGain += bucket['Gain']['value']
initLoss += bucket['Loss']['value']
wsGain = initGain/count
wsLoss = initLoss/count
else:
wsGain = (13 * wsGain + bucket['Gain']['value'])/14
wsLoss = (13 * wsLoss + bucket['Loss']['value'])/14
wsRSI = 100 - 100/(1+wsGain/wsLoss)
transaction['RSISMMA14'] = "buy" if wsRSI < 30 else ("sell" if wsRSI > 70 else "hold")
if transaction['date'] >= start_date:
transactions.append(transaction)
return transactions
为确保每次只买入及只持有一股,并且在卖出持有的股票之前不发生交易,我们使用布尔变量“hold”来确保交易满足以下条件。
- 当hold为 False 时,买入信号被接受
- 当hold为True时,卖出信号被接受
读者可以进一步参考开源项目中交易策略的report功能。 python程序提供了包括整个买卖交易的“赢”和“输”在内的交易策略的统计如下:
$ ./backtest_rsis.sh -i backtest_rsis.json -s FDEV -b 2021-02-01 -e 2021-05-31 input_file 'backtest_rsis.json', start_date '2021-02-01', end_Date '2021-05-31', symbol 'FDEV' -------------------------------------------------------------------------------- RSISMA14 number of buy: 1 number of sell: 1 number of win: 1 number of lose: 0 total profit: 0.97 profit/transaction: 0.97 maximum buy price: 27.03 profit percent: 3.61% -------------------------------------------------------------------------------- RSIEWMA27S number of buy: 1 number of sell: 1 number of win: 1 number of lose: 0 total profit: 0.83 profit/transaction: 0.83 maximum buy price: 27.12 profit percent: 3.05% -------------------------------------------------------------------------------- RSISMMA14 number of buy: 0 number of sell: 0 number of win: 0 number of lose: 0 total profit: 0.00 maximum buy price: 0.00 --------------------------------------------------------------------------------
以下三个表格收集了日期从2021年02月01日到2021年05月31日,使用 RSISMA14 和 RSIEWMA27S 进行 RSI 交叉交易策略的 11 只随机挑选的 ETF 的所有统计数据。 结果显示使用 SMMA14 时未生成有效交易。 从观察的角度来看,RSISMA14可以提供更好的交易结果。当使用 RSIEWMA27S 时,在11只基金中有4只没有有效的交易信号。
如果在使用SMMA14的情况下考虑降低交叉值的标准,例如下限为35,上限为65,则可以获得比使用SMA14更好的交易结果。 下表显示在11个中有7个具有有效的交易交易。
如果在交易中只考虑这7只基金,那么使用SMMA14(35/65)的总回报比SMA14好。 EWMA27S 的总回报低于其他两个。总之,正如标题所说,移动平均函数对相对强弱指数交叉策略的回测结果有显着影响。
不过,就像大多数交易者的建议,不要根据单一指标进行交易。然而,本文中所使用的 Elasticsearch配合Python编程实例显示无缝整合且易于理解。
备注:
- 感谢Investors Exchange(IEX)开放社区提供相关数据及Gitee开源社区提供存储开源项目。
- 本文基于公开发布技术和研究观点,并不构成任何投资建议,读者在使用时须自行承担责任。
- 文中可能还存在疏漏和错误之处,恳请广大读者批评和指正。
- 作者的中文著作Elasticsearch 数据分析与实战应用(ISBN 978-7-113-27886-1号)已于2021 年 9 月出版。
- 作者的英文著作Advanced Elasticsearch 7.0(ISBN 978-1-789-95775-4号)被bookauthority评为 2021 年最值得阅读的 4 本 Elasticsearch 新书之一。



