RSIでトレード
April 27, 2021
テクニカル分析 pythonはじめに
筆者は、テクニカル分析は盲信しておらず、(いわゆるシステムトレード的な)分析結果に基づく厳密な売買も行ってはいません。一時期盲信していたこともありましたが、今では、株価は所詮ランダムウォークであり、テクニカル分析は後付け解釈だと思っています。
このことを、定量的に裏付けした研究があります。
これらの研究では、移動平均線やMACDのゴールデンクロス後、株価がどう推移したかを調べたもので、結論としては、「移動平均線ゴールデンクロスの〇日後に上昇している確率は約50%」というものです。要は、サイコロを振って売買するのと同じ。MACDのゴールデンクロスの場合は、移動平均線よりも若干ましだけど、〇日後に上昇している確率はせいぜい55%だったそうです。
でも、市場参加者の中には、テクニカル分析を盲信し、分析結果に基づいて売買する方も多数おられることも承知しています。その方々の売買が株価形成に少なからず影響を与え、それがテクニカル分析を正当化する根拠になっている面もあると思います。
そんなこんなで、テクニカル分析には懐疑的な筆者なのですが、rsiによる売買に関する、とても魅力的な研究を見つけました。上の2つと同じ著者によるものです。
本稿は、この研究の追試報告です。
rsiによる売買
rsiって何とか、どうやって計算するのかは参照元記事を見ていただくとして、参照元記事では、TOPIX500採用銘柄を対象に、RSIの設定期間を22日とし、RSIの値が20%を下から上に突き抜けたときの5日後の株価が上昇したものが75%あると言っています。
図は元記事から拝借したものですが、図では30%となっていますが、これが20%として、
チャートがこのような形になった5日後の株価の上昇率の分布は、こうなるということのようです。
ここで言っていることは、
「rsiのシグナルが出た銘柄を購入すると、5日後にこれだけ上がる。もちろん下がる銘柄もあるけれど、それは全体の25.1%にすぎない。」
とても魅力的です。
22日rsiが20%未満になるのは、22日中4日分しか値上がりしなかったということ(実施はちょっと違うけど、あくまで平均として)なので、相当売りが続いた局面ということ。そんな時に2日分くらいの値幅で値上がりするとシグナルが出るようなかんじかな。事象発生自体けっこうレアな気がしますが、これを着実に捉えれば、利が約束されるということなんでしょう。逆張りの最適なタイミングを算出しているような意味合いなのだと思います。
とても魅力的です。さっそく追試しましょう。
データの準備
筆者の場合、無尽蔵というサイトから株価データを取得し、それをpythonで加工して利用しています。
参照元記事では、
- TOPIX500採用銘柄
- 2015年1月1日~2020年1月20日
の株価データを使用して検証しているので、これにあわせたデータを用意します。無尽蔵のデータは株式分割時の補正を行っていないので、ここに記載の方法で簡易的に補正を行いました。
rsiによる売買の追試
元記事の結果の抜粋
閾値 rsi 事象数 5日後 10日後
30% 14 9123 52.5% 54.4%
22 3097 56.4% 56.6%
42 485 66.6% 63.9%
25% 14 3931 55.1% 55.3%
22 959 61.7% 60.9%
42 132 84.8% 69.7%
20% 14 1390 58.6% 59.8%
22 355 74.9% 70.7%
42 13 84.6% 69.2%
この表は、
- 算出期間が14, 22, 42日のrsiの値が閾値ラインを下から上に突破したときに買いシグナルを出し、
- 事象数は、2015年1月1日~2020年1月のTOPIX500銘柄のうち、サインが出た回数。
- サインが出た5日後と10日後に上昇した割合。
をあらわしています。
22日rsiと閾値20%、42日rsiと閾値25%あたりが、事象数もそれなりに多く、魅力的です。
早速、以下のコードで追試してみます。
import os
import glob
import numpy as np
import math
import pandas as pd
import talib
DATA_PATH = "../data/"
def calc_gc(shorts, longs):
# ゴールデンクロス検出
gcs = []
for i in range(2, len(shorts)-1):
short0 = shorts[i-1]
long0 = longs[i-1]
short = shorts[i]
long = longs[i]
if not math.isnan(short0) and not math.isnan(long0) and not math.isnan(short) and not math.isnan(long) :
d0 = short0 - long0
d = short - long
if d0<0 and d>0:
gcs.append(shorts.index[i])
return gcs
def calc_rsi(df, term):
rsi = pd.DataFrame()
rsi['diff'] = df['Close'].diff()
rsi['diff+'] = rsi['diff'].copy()
rsi['diff-'] = rsi['diff'].copy()
rsi['diff+'][rsi['diff']<0] = 0
rsi['diff-'][rsi['diff']>0] = 0
rsi['up-sum'] = rsi['diff+'].rolling(term).sum()
rsi['down-sum'] = rsi['diff-'].abs().rolling(term).sum()
rsi['rsi'] = rsi['up-sum']/(rsi['up-sum']+rsi['down-sum'])*100.0
return rsi
def trade_by_rsi_up(df, term, th):
# 【検証】rsiが20%を下から上に突き抜けたら翌日の寄り付きで買い
rsi = pd.DataFrame()
use_talib = False
if use_talib:
rsi['rsi'] = talib.RSI(df['Close'], timeperiod=term)
else:
myrsi = calc_rsi(df, term)
rsi['rsi'] = myrsi['rsi']
rsi['DL'] = th
gcs = calc_gc(rsi['rsi'], rsi['DL'])
return gcs
def add_stat(df, gcs, hold_days, gainlist):
for gc in gcs:
i = df.index.get_loc(gc)
# GC翌日の寄り付きで買い
buy = df['Open'][i+1]
#buy = df['Close'][i]
if i>len(df)-hold_days-1:
sell = df['Close'][len(df)-1]
else:
# そのhold_days日後の引けで売る
sell = df['Close'][i+hold_days]
gain = (sell-buy)/buy
gainlist.append(gain)
def print_stat(gainlist, day):
x = np.array(gainlist)
ave = x.mean()
n_plus = np.count_nonzero(x>=0)
n_minus = np.count_nonzero(x<0)
n_total = len(x)
npp = n_plus/n_total*100.0
nmp = n_minus/n_total*100.0
print('{}日後のゲイン:'.format(day))
print('平均ゲイン={:.5f}%'.format(ave*100.0))
print('プラスゲイン数={}, ({:.2f}%)'.format(n_plus, npp))
print('マイナスゲイン数={}, ({:.2f}%)'.format(n_minus, nmp))
print('総事象数='+str(n_total), flush=True)
print('')
def process(code, df, term, th, gain5, gain10):
gcs = trade_by_rsi_up(df, term, th)
if len(gcs)>0:
add_stat(df, gcs, 5, gain5)
add_stat(df, gcs, 10, gain10)
def walk_around(process, term, th, gain5, gain10):
for i in range(1, 10):
input_path = DATA_PATH + str(i*1000) + "/*.csv"
csv_files = sorted(glob.glob(input_path))
for csv_file in csv_files:
filename = os.path.basename(csv_file)
code = str(filename[0:4])
code_dir = code[0]+"000"
input_path = DATA_PATH + code_dir + "/" + code + ".csv"
df = pd.read_csv(input_path, header=None,
names=['Date','Open','High','Low','Close','Volume'],
encoding='UTF-8')
process(code, df, term, th, gain5, gain10)
def main():
terms = [14, 22, 42]
ths = [30.0, 25.0, 20.0]
for term in terms:
for th in ths:
gain5 = []
gain10 = []
walk_around(process, term, th, gain5, gain10)
print('■ {}日rsi, 閾値={}%'.format(term, th), flush=True)
print_stat(gain5, 5)
print_stat(gain10, 10)
if __name__ == '__main__':
main()
実行結果はコレ。
$ python test_trade_by_rsi.py
■ 14日rsi, 閾値=30.0%
5日後のゲイン:
平均ゲイン=0.24291%
プラスゲイン数=8902, (52.54%)
マイナスゲイン数=8040, (47.46%)
総事象数=16942
10日後のゲイン:
平均ゲイン=0.49861%
プラスゲイン数=9117, (53.81%)
マイナスゲイン数=7825, (46.19%)
総事象数=16942
■ 14日rsi, 閾値=25.0%
5日後のゲイン:
平均ゲイン=0.15962%
プラスゲイン数=5615, (51.32%)
マイナスゲイン数=5327, (48.68%)
総事象数=10942
10日後のゲイン:
平均ゲイン=0.44817%
プラスゲイン数=5835, (53.33%)
マイナスゲイン数=5107, (46.67%)
総事象数=10942
■ 14日rsi, 閾値=20.0%
5日後のゲイン:
平均ゲイン=0.14011%
プラスゲイン数=3114, (51.10%)
マイナスゲイン数=2980, (48.90%)
総事象数=6094
10日後のゲイン:
平均ゲイン=0.47730%
プラスゲイン数=3240, (53.17%)
マイナスゲイン数=2854, (46.83%)
総事象数=6094
■ 22日rsi, 閾値=30.0%
5日後のゲイン:
平均ゲイン=0.39439%
プラスゲイン数=4604, (53.75%)
マイナスゲイン数=3962, (46.25%)
総事象数=8566
10日後のゲイン:
平均ゲイン=0.70103%
プラスゲイン数=4727, (55.18%)
マイナスゲイン数=3839, (44.82%)
総事象数=8566
■ 22日rsi, 閾値=25.0%
5日後のゲイン:
平均ゲイン=0.40541%
プラスゲイン数=2211, (54.05%)
マイナスゲイン数=1880, (45.95%)
総事象数=4091
10日後のゲイン:
平均ゲイン=0.69559%
プラスゲイン数=2252, (55.05%)
マイナスゲイン数=1839, (44.95%)
総事象数=4091
■ 22日rsi, 閾値=20.0%
5日後のゲイン:
平均ゲイン=0.43092%
プラスゲイン数=872, (54.77%)
マイナスゲイン数=720, (45.23%)
総事象数=1592
10日後のゲイン:
平均ゲイン=0.87668%
プラスゲイン数=913, (57.35%)
マイナスゲイン数=679, (42.65%)
総事象数=1592
■ 42日rsi, 閾値=30.0%
5日後のゲイン:
平均ゲイン=0.35084%
プラスゲイン数=941, (55.48%)
マイナスゲイン数=755, (44.52%)
総事象数=1696
10日後のゲイン:
平均ゲイン=0.72532%
プラスゲイン数=981, (57.84%)
マイナスゲイン数=715, (42.16%)
総事象数=1696
■ 42日rsi, 閾値=25.0%
5日後のゲイン:
平均ゲイン=0.42671%
プラスゲイン数=201, (55.22%)
マイナスゲイン数=163, (44.78%)
総事象数=364
10日後のゲイン:
平均ゲイン=0.92733%
プラスゲイン数=217, (59.62%)
マイナスゲイン数=147, (40.38%)
総事象数=364
■ 42日rsi, 閾値=20.0%
5日後のゲイン:
平均ゲイン=0.08794%
プラスゲイン数=26, (76.47%)
マイナスゲイン数=8, (23.53%)
総事象数=34
10日後のゲイン:
平均ゲイン=0.66696%
プラスゲイン数=22, (64.71%)
マイナスゲイン数=12, (35.29%)
総事象数=34
元記事と条件は合わせたはずなのに、同じ結果にはなりません。
- プラスゲイン数のほうがマイナスゲイン数よりも多い
- 平均ゲインもプラス
ということで、傾向としては似ているといえば似ているのですが。 42日rsi, 閾値=20.0%の結果は結構魅力的ですが、事象数が少ない。
元記事のrsiは、TA-Libで算出しているのではないか?ということで、trade_by_rsi_up()中のuse_talibをTrueに変えて実行してみると、
$ python test_trade_by_rsi.py
■ 14日rsi, 閾値=30.0%
5日後のゲイン:
平均ゲイン=0.41735%
プラスゲイン数=3838, (53.20%)
マイナスゲイン数=3376, (46.80%)
総事象数=7214
10日後のゲイン:
平均ゲイン=0.82581%
プラスゲイン数=4034, (55.92%)
マイナスゲイン数=3180, (44.08%)
総事象数=7214
■ 14日rsi, 閾値=25.0%
5日後のゲイン:
平均ゲイン=0.44158%
プラスゲイン数=1481, (54.27%)
マイナスゲイン数=1248, (45.73%)
総事象数=2729
10日後のゲイン:
平均ゲイン=0.82382%
プラスゲイン数=1557, (57.05%)
マイナスゲイン数=1172, (42.95%)
総事象数=2729
■ 14日rsi, 閾値=20.0%
5日後のゲイン:
平均ゲイン=0.26991%
プラスゲイン数=341, (52.22%)
マイナスゲイン数=312, (47.78%)
総事象数=653
10日後のゲイン:
平均ゲイン=1.00029%
プラスゲイン数=385, (58.96%)
マイナスゲイン数=268, (41.04%)
総事象数=653
■ 22日rsi, 閾値=30.0%
5日後のゲイン:
平均ゲイン=0.67495%
プラスゲイン数=1166, (57.27%)
マイナスゲイン数=870, (42.73%)
総事象数=2036
10日後のゲイン:
平均ゲイン=0.86962%
プラスゲイン数=1178, (57.86%)
マイナスゲイン数=858, (42.14%)
総事象数=2036
■ 22日rsi, 閾値=25.0%
5日後のゲイン:
平均ゲイン=0.42944%
プラスゲイン数=190, (54.29%)
マイナスゲイン数=160, (45.71%)
総事象数=350
10日後のゲイン:
平均ゲイン=1.07517%
プラスゲイン数=207, (59.14%)
マイナスゲイン数=143, (40.86%)
総事象数=350
■ 22日rsi, 閾値=20.0%
5日後のゲイン:
平均ゲイン=-0.36281%
プラスゲイン数=11, (47.83%)
マイナスゲイン数=12, (52.17%)
総事象数=23
10日後のゲイン:
平均ゲイン=-0.51680%
プラスゲイン数=14, (60.87%)
マイナスゲイン数=9, (39.13%)
総事象数=23
■ 42日rsi, 閾値=30.0%
5日後のゲイン:
平均ゲイン=0.23343%
プラスゲイン数=36, (49.32%)
マイナスゲイン数=37, (50.68%)
総事象数=73
10日後のゲイン:
平均ゲイン=1.21435%
プラスゲイン数=46, (63.01%)
マイナスゲイン数=27, (36.99%)
総事象数=73
■ 42日rsi, 閾値=25.0%
5日後のゲイン:
平均ゲイン=-2.42903%
プラスゲイン数=1, (50.00%)
マイナスゲイン数=1, (50.00%)
総事象数=2
10日後のゲイン:
平均ゲイン=0.25968%
プラスゲイン数=1, (50.00%)
マイナスゲイン数=1, (50.00%)
総事象数=2
■ 42日rsi, 閾値=20.0%
総事象数=0
TA-Libでも元記事と同じ結果にはなりませんでした。TA-Libでは、事象数が大きく減ってしまいました。この原因は、TA-Libのrsiの計算方法がよく知られた定義とは異なる方法で計算されているためではないかと疑っています。この件は、ここに書きました。
例えば、2015年1月1日~2020年1月20日の1332のrsiを前出のcalc_rsi()で算出した場合と、TA-Libのtalib.RSI()で算出した場合で比較すると図のような違いがあります。
TA-Libのrsiの方が、振れ幅が小さく、シグナルがより出にくいようなグラフです。
結局、元記事との違いは何なのかわからず、釈然としないまま、自作コードによる追試は諦めました。
rsiによる売買の追試(Take2)
元記事には、rsiによる売買の検証プログラムが公開されていましたので、素直にこれを動かしてみることにします。
実行させると、以下のような結果になりました。
myrsi(20%)
buy_sign_count roc_d1_plus roc_d3_plus roc_d5_plus roc_d10_plus
0 14927 0 7452 7738 7866
1 6185 0 3102 3141 3213
2 1618 0 856 891 908
3 35 0 20 19 23
talib(20%)
buy_sign_count roc_d1_plus roc_d3_plus roc_d5_plus roc_d10_plus
0 3904 0 2106 2101 2028
1 653 0 377 367 366
2 23 0 11 10 12
3 0 0 0 0 0
この表の見方
-
myrsi(20%)は、rsiをcalc_rsi()で算出し、閾値=20.0%で出たシグナルを集計
-
talib(20%)は、rsiをTA-Libのtalib.RSI()で算出し、閾値=20.0%で出たシグナルを集計
-
0行目;9日rsi
-
1行目;14日rsi
-
2行目;22日rsi
-
3行目;42日rsi
-
buy_sign_count;全事象数
-
roc_d5_plus;5日後ゲインがプラスになった事象数
-
roc_d10_plus;10日後ゲインがプラスになった事象数
この表と前節での自作コードによる結果をフォーマットをあわせて表示してみると、以下のようになりました。
myrsi(20%)
buy_sign_count roc_d1_plus roc_d3_plus roc_d5_plus roc_d10_plus
1 6094 - - 3114 3240
2 1592 - - 872 913
3 34 - - 26 22
talib(20%)
buy_sign_count roc_d1_plus roc_d3_plus roc_d5_plus roc_d10_plus
1 653 - - 341 385
2 23 - - 11 14
3 0 - - 0 0
ほぼ一致しました。自作コードでは、シグナルが出た翌日の寄り付きで購入し、5日後と10日後の引けで売却を想定していますが、元記事のコードでは、シグナルが出た日の引けで購入しているので、少しの差が出たのだと思います。
ということで、自作コードでも元記事の検証コードでも同じ結果が得られたので、違いはデータかrsiの算出方法かのいずれかということでしょう。結局、rsiによる売買はそんなに魅力的ではなかったということで、締めたいと思います。
まとめ
rsiが閾値ラインを下から上に突破したときに買うとおいしいかもしれないという先行研究があり、追試してみたが、そこまでおいしくはなかった。