はじめに
「株価分析(SMA) - Backtestingを使ってリターンを計算する」の続編となります。今回はRSIを使って株価のリターンをシミュレートしたいと思います。
RSIは、Relative Strength Indexの略で、日本語だと相対力指数と呼ばれています。RSIの値が高いと買われすぎ、低いと売られすぎの指標となるそうです。
さっそくRSIを計算してみます。
まずは株価の取得です。こちらは今まで通りpandas_datareaderを使用して取得しています。一度取得したものは覚えるようにしています。
def get_stock(ticker, start_date, end_date):
'''
get stock data from Yahoo Finance
'''
dirname = '../data'
os.makedirs(dirname, exist_ok=True)
period = f"{start_date.strftime('%Y%m%d')}_{end_date.strftime('%Y%m%d')}"
fname = f'{dirname}/{ticker}_{period}.pkl'
if os.path.exists(fname):
df_stock = pd.read_pickle(fname)
else:
df_stock = pandas_datareader.data.DataReader(
ticker, 'yahoo', start_date, end_date)
df_stock.to_pickle(fname)
return df_stock
2011年〜2020年の10年間の日経平均株価を取得してRSIを計算します。RSIの計算期間は14日としています。(Talibのデフォルト値と同じ。)
START_DATE = datetime.date(2011, 1, 1)
END_DATE = datetime.date(2020, 12, 31)
TICKER = '^N225'
df = get_stock(TICKER, START_DATE, END_DATE)
df['RSI'] = talib.RSI(df['Adj Close'].values, timeperiod=14)
日経平均株価と計算したRSIをグラフにしてみます。
fig, axes = plt.subplots(nrows=2, sharex=True, figsize=(10, 6))
axes[0].set_title('RSI')
axes[0].plot(df.index, df['Adj Close'], label=f'{TICKER} Close')
axes[0].grid()
axes[0].legend(loc='upper left')
axes[1].plot(df['RSI'], label='RSI 14days')
axes[1].axhline(y=20, color='blue', linestyle=':')
axes[1].axhline(y=30, color='red', linestyle=':')
axes[1].axhline(y=70, color='red', linestyle=':')
axes[1].axhline(y=80, color='blue', linestyle=':')
axes[1].grid()
axes[1].legend(loc='upper left')
plt.show()
RSIが高いと買われすぎ。つまりRSIが高くなった後は株価が下がる可能性が高くなる。
RSIが低いと売られすぎ。つまりRSIが低くなった後は株価が上がる可能性が高くなる。
80%と70%、30%と20%のところに横線を入れていますが、前者はRSIが高いと判断するときによく使われる値、後者はRSIが低いと判断するときによく使われる値です。
でも、このグラフだけではよくわかりませんね。
RSI閾値に達したときの株価変化率の推移
では、RSIが上限基準値を超えたとき、あるいは下限基準値を下回ったとき、その後の株価の変化はどうなっているのでしょうか?
RSIの計算期間を5日〜40日、RSI上限値を60%〜89%、RSI下限値を10%〜39%の総当たりで確認してみます。RSIの上限値あるいは下限値を超えたときから30日間の株価の平均変化率を求めます。30日間の計算は歴日で行っているので、実際の日数は休日分だけ少なくなってます。計算結果はdf_upper、df_lowerに保存します。
MAX_TIMEPERIOD = 40
df_stock = get_stock(TICKER, START_DATE, END_DATE)
df_upper = pd.DataFrame()
df_lower = pd.DataFrame()
for timeperiod in np.arange(5, MAX_TIMEPERIOD + 1):
df = df_stock.copy()
df['RSI'] = talib.RSI(df['Adj Close'].values, timeperiod=timeperiod)
df = df.dropna()
# RSI上限
results = []
for upper_limit in np.arange(60, 90):
# RSIが上限を超えた日付を探す
dates = df[(df['RSI'].shift() < upper_limit) &
(df['RSI'] >= upper_limit)].index
# RSIが上限を超えた日を基準に株価の変化率を求める
changes = calc_change(df, dates)
# 結果に追加
results.append(
{'limit': upper_limit, 'average': np.average(sum(changes, []))})
# 結果をDataFrameに変換
d = pd.DataFrame(results)
d['timeperiod'] = timeperiod
# 変化率を%にし基準を0とする
d['average'] = d['average'] * 100 - 100
df_upper = pd.concat([df_upper, d])
# RSI下限
results = []
for lower_limit in np.arange(10, 40):
# RSIが下限を下回った日付を探す
dates = df[(df['RSI'].shift() > lower_limit) &
(df['RSI'] <= lower_limit)].index
# RSIが下限を下回った日を基準に株価の変化率を求める
changes = calc_change(df, dates)
# 結果に追加
results.append(
{'limit': lower_limit, 'average': np.average(sum(changes, []))})
# 結果をDataFrameに変換
d = pd.DataFrame(results)
d['timeperiod'] = timeperiod
# 変化率を%にし基準を0とする
d['average'] = d['average'] * 100 - 100
df_lower = pd.concat([df_lower, d])
結果をグラフにしてみます。
fig, axes = plt.subplots(nrows=2, figsize=(8, 8))
for timeperiod in df_upper['timeperiod'].unique():
d = df_upper[df_upper['timeperiod'] == timeperiod]
axes[0].plot(d['limit'], d['average'], label=timeperiod)
for timeperiod in df_lower['timeperiod'].unique():
d = df_lower[df_lower['timeperiod'] == timeperiod]
axes[1].plot(d['limit'], d['average'], label=timeperiod)
for i in range(2):
axes[i].axhline(y=0, color='red', linestyle=':')
axes[i].grid()
axes[i].set_xlabel('RSI閾値')
axes[i].set_ylabel('変化率(%)')
fig.suptitle('RSI閾値と変化率')
plt.subplots_adjust(top=0.9, bottom=0.1, hspace=0.3)
axes[0].set_title('RSI上限値を超えた後')
axes[1].set_title('RSI下限値を下回った後')
plt.show()
計算期間ごとに線を引いています。RSI上限値のグラフでは70%手前から株価が下落する傾向が現れています。RSI下限値のグラフでは25%より少し先まで株価が上昇している傾向があります。ただこのグラフはわかりにくいですね。ヒートマップで作成し直してみます。
fig, axes = plt.subplots(nrows=2, figsize=(8, 16))
sns.heatmap(df_upper.pivot('timeperiod', 'limit', 'average'),
square=True, cmap='Blues_r', ax=axes[0])
sns.heatmap(df_lower.pivot('timeperiod', 'limit', 'average'),
square=True, cmap='Reds', ax=axes[1])
for i in range(2):
axes[i].invert_yaxis()
axes[i].set_xlabel('RSI閾値')
axes[i].set_ylabel('RSI期間(日)')
axes[0].set_title('RSI上限閾値と株価変化率')
axes[1].set_title('RSI下限閾値と株価変化率')
plt.subplots_adjust(top=0.9, bottom=0.1, hspace=0.3)
plt.show()
RSI上限値のグラフでは濃い青色の部分が株価が下落している部分です。このグラフだとRSI上限値が73,74%あたりとなります。このときのRSI計算期間は34〜40日となっています。RSI下限値のグラフでは濃い赤色の部分が株価が上昇している部分です。RSI下限値としては10〜24%、RSI集計期間は11〜38日の範囲で線状となっています。かなりはっきりした傾向が見られます。
Backtestingを使ってリターンを計算する
続いてBacktestingを使用してどれくらいのリターンが得られるか計算してみます。
Strategyクラスは下記のようにしました。RSIが上限値を超えたときに売り、RSIが下限値を下回ったときに買い、の単純な内容です。
class RsiStrategy(Strategy):
'''
RSI Strategy
'''
timeperiod = 14
rsi_upper = 80
rsi_lower = 20
def init(self):
close = self.data['Adj Close']
self.rsi = self.I(talib.RSI, close, self.timeperiod)
def next(self):
'''
RSIが上限値を超えたら買われすぎと判断し売る
RSIが下限値を下回ったら売られすぎと判断し買う
'''
if crossover(self.rsi_lower, self.rsi):
self.buy()
elif crossover(self.rsi, self.rsi_upper):
self.sell()
シミュレートは下記で実行できます。
df = get_stock(TICKER, START_DATE, END_DATE)
bt = Backtest(
df,
RsiStrategy,
cash=INIT_CASH,
trade_on_close=False,
exclusive_orders=True
)
結果は25%のマイナスでした。
Start 2011-01-04 00:00:00
End 2020-12-30 00:00:00
Duration 3648 days 00:00:00
Exposure Time [%] 97.9992
Equity Final [$] 741527
Equity Peak [$] 1.24246e+06
Return [%] -25.8473
Buy & Hold Return [%] 163.934
Return (Ann.) [%] -3.03027
Volatility (Ann.) [%] 21.3582
Sharpe Ratio 0
Sortino Ratio 0
Calmar Ratio 0
Max. Drawdown [%] -70.8682
Avg. Drawdown [%] -9.02723
Max. Drawdown Duration 3131 days 00:00:00
Avg. Drawdown Duration 297 days 00:00:00
# Trades 10
Win Rate [%] 40
Best Trade [%] 42.2201
Worst Trade [%] -42.0814
Avg. Trade [%] -2.95518
Max. Trade Duration 876 days 00:00:00
Avg. Trade Duration 358 days 00:00:00
Profit Factor 0.93255
Expectancy [%] -0.567648
SQN -0.489027
_strategy RsiStrategy
_equity_curve ...
_trades Size EntryBa...
dtype: object
optimizeを使用して最適解を見つけます。(私のPCで2時間かかりました。)
df = get_stock(TICKER, START_DATE, END_DATE)
bt = Backtest(
df,
RsiStrategy,
cash=INIT_CASH,
trade_on_close=False,
exclusive_orders=True
)
stats, heatmap = bt.optimize(
timeperiod=range(5, 41),
rsi_upper=range(60, 91),
rsi_lower=range(10, 41),
return_heatmap=True)
最適時のリターンは214%です。(資産が3倍になったということですね。)
このときのRSI計算期間は19日、RSI上限値が90%、RSI下限値が21%となっています。
Start 2011-01-04 00:00:00
End 2020-12-30 00:00:00
Duration 3648 days 00:00:00
Exposure Time [%] 97.9992
Equity Final [$] 3.14228e+06
Equity Peak [$] 3.14331e+06
Return [%] 214.228
Buy & Hold Return [%] 163.934
Return (Ann.) [%] 12.5035
Volatility (Ann.) [%] 23.5335
Sharpe Ratio 0.531307
Sortino Ratio 0.865442
Calmar Ratio 0.393282
Max. Drawdown [%] -31.7927
Avg. Drawdown [%] -4.09891
Max. Drawdown Duration 840 days 00:00:00
Avg. Drawdown Duration 56 days 00:00:00
# Trades 2
Win Rate [%] 100
Best Trade [%] 107.403
Worst Trade [%] 51.5613
Avg. Trade [%] 77.2973
Max. Trade Duration 3285 days 00:00:00
Avg. Trade Duration 1789 days 00:00:00
Profit Factor NaN
Expectancy [%] 79.4823
SQN 462.381
_strategy RsiStrategy(time...
_equity_curve ...
_trades Size EntryBa...
dtype: object
下記でトレード結果のグラフを表示します。
ほとんどトレードをしてないようです。ただ、株価が下がったときに一気に買っています。これって新型コロナで株価が暴落したときですね。
bt.plot()
下記でヒートマップのグラフも表示できます。
plot_heatmaps(heatmap, agg='mean', plot_width=2048, filename='heatmap')
変数が3つあるので、それぞれの組み合わせのヒートマップとなってます。
これでも傾向はわかりますが、3D散布図を作成してみることにします。
from mpl_toolkits.mplot3d import Axes3D
d = heatmap.reset_index()
fig = plt.figure()
ax = Axes3D(fig)
ax.scatter(d['timeperiod'], d['rsi_upper'], d['rsi_lower'], c=d['SQN'])
ax.set_xlabel('RSI期間(日)')
ax.set_ylabel('RSI上限')
ax.set_zlabel('RSI下限')
plt.show()
図ではイメージと逆ですが、薄い丸の方がSQNが高いことを表しています。(SQNが高い=良いということです。)
最後に
今回はRSIを使用した取引をシミュレートしてみました。前回のSMAではリターンが140%でしたが、RSIを使用した場合は214%となりました。なかなかの成果です。それにしても新型コロナが買いのチャンスだったとは…
ソースはGitHubに置いています。