株価分析(RSI)

# はじめに

「[株価分析（SMA) - Backtestingを使ってリターンを計算する](https://qiita.com/mahoutsukaino-deshi/items/8907a34986804c58724a)」の続編となります。今回はRSIを使って株価のリターンをシミュレートしたいと思います。
RSIは、Relative Strength Indexの略で、日本語だと相対力指数と呼ばれています。RSIの値が高いと買われすぎ、低いと売られすぎの指標となるそうです。

さっそくRSIを計算してみます。
まずは株価の取得です。こちらは今まで通りpandas_datareaderを使用して取得しています。一度取得したものは覚えるようにしています。

```python
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のデフォルト値と同じ。）

```python
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をグラフにしてみます。

```python
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()
```

![stock_rsi1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1106967/86806d8e-0d11-4213-6236-dc923836e3df.png)

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に保存します。

```python
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])
```
結果をグラフにしてみます。

```python
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()
```

![stock_rsi2_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1106967/e9a18c0f-e119-0434-26f7-ce78263f6882.png)

計算期間ごとに線を引いています。RSI上限値のグラフでは70%手前から株価が下落する傾向が現れています。RSI下限値のグラフでは25%より少し先まで株価が上昇している傾向があります。ただこのグラフはわかりにくいですね。ヒートマップで作成し直してみます。

```python
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日の範囲で線状となっています。かなりはっきりした傾向が見られます。

![stock_rsi2_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1106967/fcde667d-5ad7-076d-2705-84946483a2f7.png)


# Backtestingを使ってリターンを計算する

続いてBacktestingを使用してどれくらいのリターンが得られるか計算してみます。
Strategyクラスは下記のようにしました。RSIが上限値を超えたときに売り、RSIが下限値を下回ったときに買い、の単純な内容です。

```python
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()
```
シミュレートは下記で実行できます。

```python
    df = get_stock(TICKER, START_DATE, END_DATE)
    bt = Backtest(
        df,
        RsiStrategy,
        cash=INIT_CASH,
        trade_on_close=False,
        exclusive_orders=True
    )
```
結果は25%のマイナスでした。

```text
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時間かかりました。）

```python
    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%となっています。

```text
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
```
下記でトレード結果のグラフを表示します。
ほとんどトレードをしてないようです。ただ、株価が下がったときに一気に買っています。これって新型コロナで株価が暴落したときですね。

```phthon
    bt.plot()
```
![stock_rsi4_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1106967/721ad6c0-315b-38ee-6cbb-f23d6bb29aa4.png)

下記でヒートマップのグラフも表示できます。

```python
    plot_heatmaps(heatmap, agg='mean', plot_width=2048, filename='heatmap')
```

![stock_rsi4_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1106967/a992e426-fdd8-d4bd-bab5-2fec8b76c61b.png)

変数が3つあるので、それぞれの組み合わせのヒートマップとなってます。
これでも傾向はわかりますが、3D散布図を作成してみることにします。

```python
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が高い=良いということです。）

![ezgif.com-gif-maker.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1106967/c3d2ae55-448e-b445-339c-698ba5ba4579.gif)

# 最後に

今回はRSIを使用した取引をシミュレートしてみました。前回のSMAではリターンが140%でしたが、RSIを使用した場合は214%となりました。なかなかの成果です。それにしても新型コロナが買いのチャンスだったとは…
ソースは[GitHub](https://github.com/mahoutsukaino-deshi/stock_rsi)に置いています。