????????在我之前的文章“移动平均函数对 RSI 交叉策略回测结果的显着影响”中,我们观察到原作者 J. Welless Wilder 相对强弱指数指标在中等长度的时间段内很难产生信号(实验大约使用四个月的有效数据)。 我们发现使用 14 天周期的简单移动平均线 (SMA) 来计算 相对强弱指数是一个不错的选择。 另外,选择 Wilder相对强弱指数在交叉值 35 与 65 也是一个不错的选择。
????????Tushar Chande 和 Stanley Kroll 于 1994 年开发了随机相对强弱指数 (Stochastic RSI),以提高产生超买和超卖信号的机会。和相对强弱指数一样,随机相对强弱指数也是一个震荡指标,它的数值在 0 到 1 之间。基本上,它是相对强弱指数的衍生物,用来衡量相对强弱指数的强弱程度。 然而,随机相对强弱指数的值变化非常迅速,并且经常达到末端值。 因此,许多使用者建议使用移动平均功能进行平滑,以减少市场噪音及更好地揭示趋势。并且对这两个指标使用的适当时间,给出了一个共同的结论。 当市场持平或波动时,随机相对强弱指数会产生良好的结果,而相对强弱指数则在趋势市场中的表现要好得多。
????????随机相对强弱指数的方程可以写成如下,其中 RSImax 和 RSImin 是 n个数据期间的最大和最小的随机相对强弱指数。 默认情况下,n 为 14。
????????使用图表来观察值的变化要容易得多。 在本文中,我们尝试将回溯测试应用于免佣金交易所交易基金 (ETF),并专注于将 Elasticsearch 作为分析工具。 下面的例子随机选择了“Fidelity International Multifactor ETF”。 其股票代码为FDEV。 另外随机抽取23只ETF运行,最终结果将在稍后展示。 数据选自投资者交易所 IEX 提供的 2021-01-01 和 2021-09-30 之间的时间范围。 但是,实验期仅从 2021-05-01 到 2021-09-30。 原因是需要足够数量的数据点来计算第一个有效相对强弱指数值,尤其是使用指数加权移动平均线时。 建议快速浏览一下我之前的文章,对通过Elasticsearch实现相对强弱指数的细节有一个基本的了解。
????????以下第一个图表显示Wilder的 相对强弱指数RSI(用蓝线绘制)对收盘价的变化是不敏感的,在测试期结束之前没有产生高于 70 或低于 30 的信号。 另一方面,随机相对强弱指数StochRSI(用红线绘制)似乎过于敏感,并且会产生大量信号。 对于随机相对强弱指数,交叉值为 0.8 和 0.2。?
?????????第二张图表显示,使用 7 天简单移动平均来平滑 StochRSI (用午夜蓝线绘制),似乎生成了合理数量的信号。 显然,平滑后的信号是滞后的,不像原始信号那样清晰和具代表性。
?????????以下先介绍一下使用Python和Elasticsearch的实现,后面再展示回测结果。 假设有一个填充数据的 Elasticsearch 索引,其数据映射显示在 Gitee Backtest_SRSI?的开源项目中。 以下步骤演示了 REST API 请求正文的代码。
通过搜索操作收集所有相关文档
使用带有必要条件(must)子句的布林查询(bool query)来收集股票代码为FDEV和日期从2021年05月01日到2021年09月30日的文档。 由于需要计算滑动窗口,因此增加了4个月的数据(从2021年01月01日到2021年04月30日)。
{
"query": {
"bool": {
"must": [
{"range": {"date": {"gte": "2021-01-01", "lte": "2021-09-30"}}},
{"term": {"symbol": "FDEV"}}
]
}
},
从交易日选择文件
使用名为Backtest_SRSI日期直方图(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"
}
},
提取桶的日期
由于额外的数据,后续操作需要稍后过滤掉超出范围的部分。 名为“DateStr”的“min”聚合用于获取存储桶的日期。 在 Elasticsearch 服务器中,日期以纪元时间存储。 时间单位为毫秒,时区为UTC。
"DateStr": {
"min": {"field": "date"}
},
提取每日的收盘价
由于子聚合使用管道(pipeline)聚合而无法直接采用文档字段,所以额外使用 “平均”聚合,名为 Close,用于检索收盘价。
"aggs": {
"Close": {
"avg": {"field": "close"}
},
计算连续两个交易日收盘价的差值
使用名为DClose的导数(derivative)聚合,并配合参数buckets_path指定Close聚合的值来确定它与前一个时间戳的值。
"DClose": {
"derivative": {"buckets_path": "Close"}
},
确定连续两个交易日收盘价差值是收益还是损失
使用两个“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"
}
},
计算第上项收益和损失的 14 天周期简单移动平均和27 天周期指数移动平均
使用四个“moving_fn”聚合,命名为GainSMA, LossSMA, GainEWMA和 LossEWMA,GainSMA和LossSMA的参数窗口(window)为 14,而GainEWMA和 LossEWMA的参数窗口(window)为 100,并且α 值等于 0.071428571。参数“buckets_path”分别为各自的增益和损失。 参数“shift”设置为 1 以包含当天。 SMA 是使用未加权平均函数 (MovingFunctions.unweightedAvg) 计算,而EWMA 是使用指数移动平均函数 (MovingFunctions.ewma) 计算的。
"GainSMA": {
"moving_fn": {
"script": "MovingFunctions.unweightedAvg(values)",
"window": 14, "buckets_path": "Gain", "shift":1
}
},
"LossSMA": {
"moving_fn": {
"script": "MovingFunctions.unweightedAvg(values)",
"window": 14, "buckets_path": "Loss", "shift":1
}
},
"GainEWMA": {
"moving_fn": {
"script": "MovingFunctions.ewma(values, 0.071428571)",
"window": 100, "buckets_path": "Gain", "shift":1
}
},
"LossEWMA": {
"moving_fn": {
"script": "MovingFunctions.ewma(values, , 0.071428571)",
"window": 100, "buckets_path": "Loss", "shift":1
}
},
计算不同类型的相对强弱指数指标
本文讨论了Wilder 相对强弱指数(RSI)、使用14 天周期简单移动平均的相对强弱指数(SMARSI)、随机相对强弱指数(SRSI) 和随机相对强弱指数的7 天周期简单移动平均(MSRSI)。 为了获得SRSI,计算了 14 天期间的最大的 SRSI (HRSI) 和最小的 SRSI (LRSI)。 使用名为 RSI 的“bucket_script”聚合和参数“buckets_path”来指定来自 GainEWMA 和 LossEWMA 的结果。 然后根据相对强弱指数方程指定脚本。 同理,可以使用GainSMA 和LossSMA 得到SMARSI。 HRSI 和LRSI 分别使用MovingFunctions.max() 和MovingFunctions.min() 来找到14 天周期的最大值和最小值。 然后根据随机相对强弱指数程用脚本计算 SRSI。 最后,使用MovingFunctions.unweightedAvg计算SRSI的7天简单移动平均线,得到MSRSI。
"RSI": {
"bucket_script": {
"buckets_path": {"GainEWMA": "GainEWMA", "LossEWMA": "LossEWMA"},
"script": "100 - 100/(1+params.GainEWMA/params.LossEWMA)"
}
},
"SMARSI": {
"bucket_script": {
"buckets_path": {"GainSMA": "GainSMA", "LossSMA": "LossSMA"},
"script": "100 - 100/(1+params.GainSMA/params.LossSMA)"
}
},
"HRSI": {
"moving_fn": {
"script": "MovingFunctions.max(values)", "window": 14,
"buckets_path": "RSI", "shift":1
}
},
"LRSI": {
"moving_fn": {
"script": "MovingFunctions.min(values)", "window": 14,
"buckets_path": "RSI", "shift":1
}
},
"SRSI": {
"bucket_script": {
"buckets_path": {"RSI": "RSI", "HRSI": "HRSI", "LRSI": "LRSI"},
"script": "(params.RSI – params.LRSI) /(params.HRSI - params.LRSI)"
}
},
"MSRSI": {
"moving_fn": {
"script": "MovingFunctions.unweightedAvg(values)", "window": 7,
"buckets_path": "SRSI", "shift":1
}
},
过滤掉额外的文件
使用名为 SStartDate 的“bucket_selector”聚合和参数“buckets_path”指定“DateStr”来选择正确的存储桶。 选择标准是日期在 2021-05-01 GMT 或之后的那些桶(纪元时间为 1619827200000,以毫秒为单位)。
"SStartDate": {
"bucket_selector": {
"buckets_path": {"DateStr": "DateStr"},
"script": "params.DateStr >= 1619827200000L"
}
}
????????实验的结果会不同类型的相对强弱指数值。 由于实验涉及 24 只 ETF,我们编写了一个 Python 程序 index_srsi.py 来执行所有索引工作,包括从 fidelity24_fund 指数中读取每日价值,计算每种类型的对强弱指数并将其值索引到给定的索引名称。因此,回测程序只处理检索出的信号(值小于或大于交叉值)而不是所有值。 REST API 请求正文的以下代码是检索所有具有交叉值 70 和 30 的相对强弱指数信号。
{
"query":{
"bool": {
"must": [{"range": {"date": {"gte": "2021-05-01", "lte": "2021-0930"}}}],
"should":[{"range": {"RSI": {"gte": 70}}},{"range": {"RSI": {"lte": 30}}}],
"minimum_should_match":1
}
},
"sort": {"date":"asc", "symbol":"asc"},
"size": 10000
}
????????为了确保只买入并持有一股,在卖出持有的股票之前没有发生任何交易,在 Python 字典中添加一个字段“share”来确保交易满足以下条件。
- 如果 entry["share"] == 0,则兑现买入信号
- 如果 entry["share"] > 0,则兑现卖出信号
????????从 Elasticsearch 检索信号,并处理响应以执行交易的 process_data() 函数如下所示:
# parse the response data and process the buy/sell signal
def process_data(resp, entries, rsi_type, hwm, lwm, balance_sheet):
result = json.loads(resp)
if "status" in result:
print("Return status: %s, error: %s" % (result['status'], result['error']))
sys.exit(-1)
if result['hits']['total']['value'] > 0:
hits = result['hits']['hits']
for hit in hits:
entry = entries[hit['_source']['symbol']]
if hit['_source'][rsi_type] > hwm:
if entry['share'] > 0:
entry['date'] = hit['_source']['date']
entry[rsi_type] = hit['_source'][rsi_type]
entry['close'] = hit['_source']['close']
trade = (entry['close'] - entry['buy']) * entry['share']
if trade > 0:
entry['num_of_win'] += 1
else:
entry['num_of_loss'] += 1
entry['profit'] += trade
elif hit['_source'][rsi_type] < lwm:
if entry['share'] == 0:
entry['date'] = hit['_source']['date']
entry['rsi'] = hit['_source']['rsi']
entry['buy'] = hit['_source']['close']
entry['share'] = 1
entry['num_of_trade'] += 1
????????Python 程序backtest_srsi.py将记录每个信号,无论该信号是否被兑现。 此外,还提供基准信息(开始日买入,最后交易日卖出)以与 相对强弱指数交易策略进行比较。 下面列出几条消息供参考。
$ ./backtest_srsi.sh -i indicators -s 2021-05-01 -e 2021-09-30 -t srsi -u 0.8 -l 0.2
input_index: 'indicators', start_date: '2021-05-01', end_date: '2021-09-30', type: 'srsi', high mark: '0.80', low mark '0.20'
Balance sheet :
--------------------------------------------------------------------------------
['date:2021-05-03, symbol: FBCG, rsi: 0.000, close: 30.740 buy a share, num_of_trades: 1 ',
'date:2021-05-03, symbol: FENY, srsi: 0.938, close: 13.700 no share, not sell',
……
'date: 2021-05-07, symbol: FCPI, buy: 28.640, close: 29.220, rsi: 1.000, share: 1, profit: 0.580, win: 1, loss: 0, total profit: 0.580',
……
'date: 2021-09-10, symbol: ONEQ, buy: 58.820, close: 56.180, share: 1, trade: -2.64 profit: -2.60',
'symbol: ONEQ, num_of_trade: 4, win: 2, loss: 2, total profit: -2.600, benchmark_profit: 0.041']
--------------------------------------------------------------------------------
????????以下几个表收集了各种相对强弱指数交易策略的统计数据,包括 Wilder's RSI (70,30)、StochRSI、Smoothed StochRSI 、SMA RSI和 Wilder's RSI (65,35) 。数据由24 只随机挑选的 ETF从 2021-05-01 到 2021-09-30组成 。该表包括获胜次数、失败次数和总利润(忽略成本),基准利润也包括在内。 第一个表格显示了 StochRSI 和 Wilder RSI 的表现。 从观察的角度来看,StochRSI 远优于 RSI。 这是因为如果使用 RSI,24 只 ETF 中只有 7 只收到交易信号。 但是,与其他类型的 RSI 相比,StochRSI 的交易信号太多,也没有使结果变得更好。
????????第二个表格显示了平滑后的StochRSI和SMARSI的表现。尽管平滑后的StochRSI交易信号的数量显着减少,但表现更差。 最可能的原因是由于错过了上升和下降的精确峰值。 SMARSI的性能非常接近StochRSI,但交易次数要少得多。 ?
????????第三个表显示了具有交叉值 (35,65) 的 Wilder RSI和基准的性能。 在这个实验中,我得到了与前一篇文章相同的结论,即 RSI(35,65)表现最好。它的收益是基准的3倍。赢输的比例为16比14。大部分亏损是由于在实验最后一天强行抛售造成的。如果要下结论,在使用相对强弱指数时,需要有更长的缓冲时间来等待合适的时机卖出。 否则,当您因某种原因被迫卖出时,价格很可能会远低于您买入的价格。?
?
????????最后一个表格显示了本实验结果汇总。作为大多数交易者的建议,不要使用单一指标进行交易,因为可能会在错误的信号下进行交易。 根据编程代码,Elasticsearch 和 Python 可以无缝集成到技术分析中。
备注:
- 感谢Investors Exchange(IEX)开放社区提供相关数据及Gitee开源社区提供存储开源项目。
- 本文基于公开发布技术和研究观点,并不构成任何投资建议,读者在使用时须自行承担责任。
- 文中可能还存在疏漏和错误之处,恳请广大读者批评和指正。
- 作者的中文著作Elasticsearch 数据分析与实战应用(ISBN 978-7-113-27886-1号)将于2021 年 7 月出版。
- 作者的英文著作Advanced Elasticsearch 7.0(ISBN 978-1-789-95775-4号)被bookauthority评为 2021 年最值得阅读的 4 本 Elasticsearch 新书之一。
|