ChatGPTに聞きながらXGBoostで株価の予測をするモデルを作ってみた

# 目的
ChatGPTの可能性を探るため、いろいろ聞きながらXGBoostで株価の予測をするモデルを作ってみました。問題自体が単純だからか、意外とすんなりモデル自体は作成できたと思います。

# プロンプト
最初のプロンプトは以下の通り。

使用モデル：ChatGPT4o

::: note info
企業の財務指標、株価指標、ドル円レート、日本の金利、アメリカの金利、日経平均、S&P500の値、からXGBoostで株価を予測するモデルを作成するスクリプトを作成してください。yfinanceなどから実際のデータを取得してモデルを作成してください。例として、トヨタの株価を予想するモデルを作成してください。
:::

その後もいろいろなやり取りがありましたが、最終的に予測モデル自体はできました。

# 生成されたスクリプト
２０１０年１月１日から２０２４年７月１８日のトヨタの株価を学習して、明日から一週間どのような株価になるかを予測するモデルです。為替レートとか他社の株価とか入れてみた。ハイパーパラメーターの最適化とかもしてくれるようにしたので、結構実行時間がかかる。曜日とかを入れたほうがよい、というのはChatGPTが提案してくれました。

``` Python
import yfinance as yf
import pandas as pd
import numpy as np
import xgboost as xgb
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.metrics import root_mean_squared_error
from datetime import datetime, timedelta
import matplotlib.pyplot as plt

# 日付範囲の設定
start_date = "2010-01-01"
end_date = datetime.today().strftime('%Y-%m-%d')

# データ取得
def get_data(start_date, end_date):
    # トヨタの株価データ取得
    toyota = yf.download("7203.T", start=start_date, end=end_date)
    toyota = toyota[['Close', 'Volume']].rename(columns={'Close': 'Toyota_Close', 'Volume': 'Toyota_Volume'})

    # その他の株価指標データ取得
    sp500 = yf.download("^GSPC", start=start_date, end=end_date)
    sp500 = sp500[['Close']].rename(columns={'Close': 'SP500_Close'})

    nikkei = yf.download("^N225", start=start_date, end=end_date)
    nikkei = nikkei[['Close']].rename(columns={'Close': 'Nikkei_Close'})

    usdjpy = yf.download("JPY=X", start=start_date, end=end_date)
    usdjpy = usdjpy[['Close']].rename(columns={'Close': 'USDJPY_Close'})

    jp_bond = yf.download("^TNX", start=start_date, end=end_date)
    jp_bond = jp_bond[['Close']].rename(columns={'Close': 'JP_Bond_Rate'})

    us_bond = yf.download("^TNX", start=start_date, end=end_date)
    us_bond = us_bond[['Close']].rename(columns={'Close': 'US_Bond_Rate'})

    nissan = yf.download("7201.T", start=start_date, end=end_date)
    nissan = nissan[['Close']].rename(columns={'Close': 'Nissan_Close'})

    honda = yf.download("7267.T", start=start_date, end=end_date)
    honda = honda[['Close']].rename(columns={'Close': 'Honda_Close'})

    denso = yf.download("6902.T", start=start_date, end=end_date)
    denso = denso[['Close']].rename(columns={'Close': 'Denso_Close'})

    ford = yf.download("F", start=start_date, end=end_date)
    ford = ford[['Close']].rename(columns={'Close': 'Ford_Close'})

    gm = yf.download("GM", start=start_date, end=end_date)
    gm = gm[['Close']].rename(columns={'Close': 'GM_Close'})

    # データの統合
    data = toyota.join([sp500, nikkei, usdjpy, jp_bond, us_bond, nissan, honda, denso, ford, gm], how='inner')
    data = data.dropna()

    return data

# データの取得
data = get_data(start_date, end_date)

# テクニカル指標の追加
data['SMA_20'] = data['Toyota_Close'].rolling(window=20).mean()
data['EMA_20'] = data['Toyota_Close'].ewm(span=20, adjust=False).mean()
data['Middle_Band'] = data['Toyota_Close'].rolling(window=20).mean()
data['Upper_Band'] = data['Middle_Band'] + 2*data['Toyota_Close'].rolling(window=20).std()
data['Lower_Band'] = data['Middle_Band'] - 2*data['Toyota_Close'].rolling(window=20).std()

delta = data['Toyota_Close'].diff(1)
gain = delta.where(delta > 0, 0)
loss = -delta.where(delta < 0, 0)
avg_gain = gain.rolling(window=14).mean()
avg_loss = loss.rolling(window=14).mean()
rs = avg_gain / avg_loss
data['RSI'] = 100 - (100 / (1 + rs))

data['Day_of_Week'] = data.index.dayofweek
data['Month'] = data.index.month

# 特徴量と目的変数の設定
data['Toyota_Close_Next'] = data['Toyota_Close'].shift(-1)
data = data.dropna()

X = data.drop('Toyota_Close_Next', axis=1)
y = data['Toyota_Close_Next']

# トレーニングとテストデータに分割
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# ハイパーパラメータの設定
param_grid = {
    'max_depth': [3, 5, 7],
    'learning_rate': [0.01, 0.1, 0.3],
    'n_estimators': [100, 300, 500],
    'subsample': [0.7, 0.8, 0.9],
    'colsample_bytree': [0.7, 0.8, 0.9],
    'gamma': [0, 1, 5],
    'min_child_weight': [1, 3, 5]
}

# GridSearchCVの設定
xgb_model = xgb.XGBRegressor(objective='reg:squarederror', eval_metric='rmse')
grid_search = GridSearchCV(estimator=xgb_model, param_grid=param_grid, cv=3, scoring='neg_mean_squared_error', n_jobs=-1, verbose=1)

# モデルの訓練
grid_search.fit(X_train, y_train)

# 最適なハイパーパラメータの表示
print(f"Best parameters: {grid_search.best_params_}")

# 最適なハイパーパラメータでモデルを再訓練
best_model = grid_search.best_estimator_
preds = best_model.predict(X_test)

# 評価
rmse = root_mean_squared_error(y_test, preds)
print("RMSE: %.2f" % rmse)

# 予測結果の表示
results = pd.DataFrame({'Actual': y_test, 'Predicted': preds}, index=y_test.index)
results = results.sort_index()  # 日付でソート
print(results.head())

# 次の一週間の予測
next_week_preds = []
latest_data = X.iloc[-1:].copy()
current_date = datetime.strptime(end_date, '%Y-%m-%d')

for i in range(7):
    next_day = current_date + timedelta(days=i+1)
    next_day_str = next_day.strftime('%Y-%m-%d')
    
    # 特徴量の更新（例：移動平均やRSIの再計算）
    latest_data['Toyota_Close'] = next_week_preds[-1][1] if next_week_preds else latest_data['Toyota_Close']
    latest_data['SMA_20'] = latest_data['Toyota_Close'].rolling(window=20).mean()
    latest_data['EMA_20'] = latest_data['Toyota_Close'].ewm(span=20, adjust=False).mean()
    latest_data['Middle_Band'] = latest_data['Toyota_Close'].rolling(window=20).mean()
    latest_data['Upper_Band'] = latest_data['Middle_Band'] + 2 * latest_data['Toyota_Close'].rolling(window=20).std()
    latest_data['Lower_Band'] = latest_data['Middle_Band'] - 2 * latest_data['Toyota_Close'].rolling(window=20).std()
    
    delta = latest_data['Toyota_Close'].diff(1)
    gain = delta.where(delta > 0, 0)
    loss = -delta.where(delta < 0, 0)
    avg_gain = gain.rolling(window=14).mean()
    avg_loss = loss.rolling(window=14).mean()
    rs = avg_gain / avg_loss
    latest_data['RSI'] = 100 - (100 / (1 + rs))

    latest_data.index = pd.to_datetime([next_day_str])  # 次の日の日付を設定
    latest_data['Day_of_Week'] = latest_data.index.dayofweek
    latest_data['Month'] = latest_data.index.month

    # モデルに直接特徴量を渡して予測
    next_day_pred = best_model.predict(latest_data)
    next_week_preds.append((next_day_str, next_day_pred[0]))

# 次の一週間の予測結果を表示
print("Next week's predictions:")
for date, pred in next_week_preds:
    print(f"Prediction for {date}: {pred}")

# 実際の株価と予測された株価を時系列でプロット
plt.figure(figsize=(14, 7))
plt.plot(data.index, data['Toyota_Close'], label='Actual Toyota Close Price', color='blue')
plt.plot(results.index, results['Predicted'], label='Predicted Toyota Close Price', color='red')

# 次の一週間の予測値をプロット
next_week_dates = [datetime.strptime(date, '%Y-%m-%d') for date, _ in next_week_preds]
next_week_values = [value for _, value in next_week_preds]
plt.plot(next_week_dates, next_week_values, label="Next Week's Predictions", color='green', linestyle='--')

plt.xlabel('Date')
plt.ylabel('Toyota Close Price')
plt.title('Actual vs Predicted Toyota Close Price')
plt.legend()
plt.show()

# 結果を新しいDataFrameにまとめる
comparison_df = pd.concat([data[['Toyota_Close']], results['Predicted']], axis=1)
comparison_df = comparison_df.rename(columns={'Toyota_Close': 'Actual', 'Predicted': 'Predicted'})

for date, pred in next_week_preds:
    comparison_df.loc[date] = [np.nan, pred]

# 比較結果を表示
print(comparison_df.tail(10))
```

# グラフ
実際の値と予測値を重ねたグラフは、最初は変なグラフだった。
![スクリーンショット 2024-07-18 21.16.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/512829/d01a22f0-53f5-7f36-de24-ea73616f4795.png)

日付でソートしてないことを指摘したら修正してくれた。結構みっちりあってる。RMSE: 25.88でなかなかいい結果なのではないか。
![スクリーンショット 2024-07-18 22.14.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/512829/e3921b19-1944-66cc-3d77-003f394ea637.png)

来週の予測はどんな感じかというと、、、横ばい。うーん、ちょっと無難。
![スクリーンショット 2024-07-18 22.15.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/512829/df3c7b57-02de-187c-9c7b-bd879288af7f.png)

# まとめ
XGBoostも含めていろいろ勉強になりました。一回正解にたどり着ければ、ここからいろんな予測モデルに応用できそうですね。やっぱすげーなChatGPT。