株の売り時/買い時の分類に必要な正解データを作る

# 1.概要
株や仮想通貨が売り時 or 買い時 を分類するモデルを作ってみようと思ったのですが、まず学習用の正解データを作らなければならないことに気づきました。
そこで、チャート上でいう「下にとがっている点」に「買い時ラベル」、「上にとがっている点」に「売り時ラベル」がつくようなコードを書いてみました。
# 2.コード
株や仮想通貨の時系列データに、以下の6つにクラス分けしてみたいと思います。
（必要なラベルは 売り時 or 買い時 or それ以外 だけかもですが…）
 - 1:売り時
 - 2:強い上昇
 - 3:上昇
 - 4:下降
 - 5:強い下降
 - 6:買い時

まず最初に今回必要なモジュールをインポートします。
```python:モジュールのインポート
import datetime
from dateutil.relativedelta import relativedelta
import yfinance as yf
import numpy as np
import matplotlib.pyplot as plt
```
yahooファイナンスのライブラリを使って時系列データを取得します。
ここでは例としてビットコインのデータを取得します。
```python:データ取得
# 取得するデータをビットコインに指定
tickers = ['BTC-JPY']
# 取得終了日
end = datetime.date.today()
# 取得開始日(10年前)
start = end - relativedelta(years=10)
# データの取得を実行 、結果を`df`に代入
df = yf.download(tickers, start=start, end=end)
```
下準備として、以下3つの列を追加します。
 - diff1：終値の1階差分
 - diff2：終値の2階差分
 - trend_class：クラス分け用のラベルが入る列。いったん NaN で初期化
```python:差分データとか作成
# Closeの差分データを算出し、diff1という新たな列に代入
df['diff1'] = df['Close'].diff()
# diff1の差分データを算出し、diff2という新たな列に代入
df['diff2'] = df['diff1'].diff()
# trend_classという列を作成し、データをNaNで初期化
df['trend_class'] = np.nan
```
diff1（終値の1階差分）が2日連続で正の場合はその点を上昇トレンドとしてラベル3、負の場合はその点を下降トレンドとしてラベル4をつけます。
どのラベルもつかず、trend_class が NaN の場合は、直後の非欠損値で置き換えます。
（ここの「XX日連続」や「XX日中XX」を変えることで、結果も結構変わります。）
```python:ラベル付け①
# diff1列を使ってrollingを適用し、条件に基づいてtrend_classを設定
def classify_trend(window):
    threshold = int(len(window))
    pos_count = (window > 0).sum()
    neg_count = (window < 0).sum()
    if pos_count >= threshold:
        return 3
    elif neg_count >= threshold:
        return 4
    else:
        return np.nan
df['trend_class'] = df['diff1'].rolling(window=2).apply(classify_trend, raw=True)
df['trend_class'] = df['trend_class'].bfill()
```
さらに trend_class がラベル3（上昇トレンド）かつ、diff1（終値の2階差分）が正かつ、diff2（終値の2階差分）が正の場合は、強い上昇トレンドにあるとしてその点をラベル2にします。
（前日よりもさらに大きい幅で上昇しているため）
逆に trend_class がラベル4（下降トレンド）かつ、diff1（終値の2階差分）が負かつ、diff2（終値の2階差分）が負の場合は、強い下降トレンドにあるとしてその点をラベル2にします。
```python:ラベル付け②
# dfのデータを1行ずつ確認し、条件に基づいてtrend_classを更新
for i in range(1, len(df)):
    if df['trend_class'].iloc[i] == 3 and df['diff1'].iloc[i] > 0 and df['diff2'].iloc[i] > 0:
        df.at[df.index[i], 'trend_class'] = 2
    elif df['trend_class'].iloc[i] == 4 and df['diff1'].iloc[i] < 0 and df['diff2'].iloc[i] < 0:
        df.at[df.index[i], 'trend_class'] = 5
```
最後に上昇トレンド（ラベル2,3） → 下降トレンド（ラベル4,5）に変化する点を売り時としてラベル1にします。
逆に下降トレンド（ラベル4,5） → 上昇トレンド（ラベル2,3）に変化する点を買い時としてラベル6にします。
```python:ラベル付け③
# trend_classの変化点を検出して更新
for i in range(1, len(df) - 1):
    if (df['trend_class'].iloc[i] in [2, 3]) and (df['trend_class'].iloc[i + 1] in [4, 5]):
        df.at[df.index[i], 'trend_class'] = 1
    elif (df['trend_class'].iloc[i] in [4, 5]) and (df['trend_class'].iloc[i + 1] in [2, 3]):
        df.at[df.index[i], 'trend_class'] = 6
```
ここまでの結果を確認します。
見えやすさのために直近半年くらいのデータに絞ってみます。
```python:結果確認
# 直近のデータ確認
df = df.tail(180)
print(df)
```
```python:出力
                  Open        High         Low       Close   Adj Close          Volume      diff1      diff2  trend_class
Date
2024-05-17  10223451.0  10326851.0  10031225.0  10136792.0  10136792.0   4906361601001   -88414.0  -679314.0          5.0
2024-05-18  10136748.0  10491554.0  10145188.0  10436284.0  10436284.0   4362926555869   299492.0   387906.0          4.0
2024-05-19  10438516.0  10490523.0  10377840.0  10421010.0  10421010.0   2601683785160   -15274.0  -314766.0          5.0
2024-05-20  10420562.0  10538310.0  10256523.0  10323983.0  10323983.0   2998373708858   -97027.0   -81753.0          5.0
2024-05-21  10324041.0  11177707.0  10302802.0  11174654.0  11174654.0   6858338601112   850671.0   947698.0          4.0
...                ...         ...         ...         ...         ...             ...        ...        ...          ...
2024-11-08  11672155.0  11757562.0  11477265.0  11625945.0  11625945.0   9721004336154   -46518.0 -1215316.0          3.0
2024-11-09  11625635.0  11801052.0  11581242.0  11688108.0  11688108.0   8425227731126    62163.0   108681.0          2.0
2024-11-10  11689744.0  11744171.0  11567248.0  11720678.0  11720678.0   4428442224534    32570.0   -29593.0          3.0
2024-11-11  11720171.0  12445996.0  11690077.0  12305469.0  12305469.0  12626034745346   584791.0   552221.0          2.0
2024-11-12  13623837.0  13800385.0  13102732.0  13353579.0  13353579.0  23548624109568  1048110.0   463319.0          2.0

[180 rows x 9 columns]
```
グラフでも見てみます。
終値を折れ線グラフとして表示し、上昇トレンド区間の背景を薄い赤、下降トレンド区間の背景を薄い緑にします。
また、売り時に赤いマーカー、買い時に緑のマーカーをつけます。
```python:グラフ作成
# Closeの折れ線グラフを作成
plt.figure(figsize=(12, 6))
plt.plot(df.index, df['Close'], label='Close', color='blue')

# 背景色を設定
for i in range(len(df)):
    current_class = df['trend_class'].iloc[i]
    if current_class == 2:
        plt.axvspan(df.index[i-1], df.index[i], color='red', alpha=0.3)
    elif current_class == 3:
        plt.axvspan(df.index[i-1], df.index[i], color='red', alpha=0.1)
    elif current_class == 4:
        plt.axvspan(df.index[i-1], df.index[i], color='green', alpha=0.1)
    elif current_class == 5:
        plt.axvspan(df.index[i-1], df.index[i], color='green', alpha=0.3)

# マーカーを追加
for i in range(len(df)):
    if df['trend_class'].iloc[i] == 1:
        plt.plot(df.index[i], df['Close'].iloc[i], 'ro')
    elif df['trend_class'].iloc[i] == 6:
        plt.plot(df.index[i], df['Close'].iloc[i], 'go')

plt.xlabel('Date')
plt.ylabel('Close')
plt.title('Close Price with Trend Classification')
plt.legend()
plt.grid(True)
plt.show()
```
グラフはこんな感じ
![peak.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3836959/f2c62bf9-77a0-d1ec-7fab-ebd65f51afe4.png)
割と特徴は捉えられてる気がする（たぶん）

今回特に高度なライブラリは使わず、diff関数をごちゃごちゃ組み合わせるだけでしたが、ある程度の結果は得られたのではと思います。