【機械学習入門】初心者でも実装できる株価予測Botを作ろう！

## 今回のゴール!

**下記動画** のように、**特定の銘柄 (シンボル)** を送るだけで、**明日の株価を予測してもらうLINE Bot** を一緒に作っていきましょう！



https://x.com/wJPjd9mWRd20878/status/1926924596319568251

:::note info
この記事を最後まで読めば同じLINE Botを間違いなく実装できます！
一緒に頑張っていきましょう！
:::


<h2 id="自己紹介">自己紹介</h2>

こんにちは！北海道の小売業でお惣菜担当として働いている社会人2年目のヒロです。

入社してから毎日忙しい日々を送っていますが、誰よりも早く出世したいという強い思いを持って、日々の業務に取り組んでいます！
最近は「デジタルツールを使って、今の自分にできることを広げたい！」という気持ちが強く、色々な技術を勉強しています。

そんな私が今特に興味を持っているのが**株式投資**です。
アメリカ株を中心に投資をしていますが、トランプ政権以降の値動きが激しすぎて、毎日肝を冷やしています。
「明日は上がるのか？下がるのか？」とチャートを眺めながら悩む日々……。
そんな中、**「この時間をもっと有効活用できないか？」**
という課題意識から、AIを活用して株価予測をLINEで手軽に確認できるツールを作ろうと思いました。

<h2 id="チャート分析って難しくね…？">チャート分析って難しくね…？</h2>

毎日、株価チャートを眺めては「上がるかな？下がるかな？」と悩み続けていましたが、
正直、**テクニカル分析の知識もあまりない自分がチャートを見ても、意味があるのか分からない**のが本音です。
それでもつい気になってしまい、チャートを見る時間がどんどん長くなってしまう……。
特に今は昇格試験が迫っており、チャートを見ている場合ではないのに、気がつけば株価を確認してしまう日々が続いていました。

国もNISAを推進して投資のリテラシー向上を促していますが、投資初心者にとってテクニカル分析やチャート分析はハードルが高いです。

「これって、自分だけの悩みじゃないのでは？」

「難しいことは分からないけど、明日上がるのか下がるのかくらいは知りたい！」

という人は、私以外にもたくさんいるはずだと感じました。

そこで、**LINEで銘柄のシンボルを送るだけで、明日の株価予測が簡単に確認できるBotを作ろう！**
と思い立ったのが今回の企画のきっかけです。

<h2 id="今回作るBotについて">今回作るBotについて</h2>

初めに説明した通り、今回作成するのは、**LINEで簡単に株価予測が見られるBot**です。

使い方はとてもシンプルで、LINEのトーク画面で**銘柄のシンボル（例: AAPL, GOOG など）を送信するだけ**。
するとBotが以下の情報を返信してくれます。

* 現在の終値
* 明日の予測終値（AIが予測した翌日の価格）
* 明日上がるか下がるか（方向性）
* 予測誤差（RMSE）

これらの予測は、**決定木勾配ブースティング（Gradient Boosting）** という手法を使って計算しています。
実はこの手法、大学時代の研究で使っていたので「これ、今回の予測モデルに使えるんじゃない？」と思って選びました。
決定木勾配ブースティングは、1本の決定木だけではなく、複数の弱い決定木を組み合わせて精度の高いモデルを作る手法です。
簡単に言えば、**「1人の知恵じゃなく、みんなの知恵を合わせて予測する」** みたいなイメージです。
イラストで表すとこんな感じです

<details>
<summary>詳細についてはこちら</summary>

決定木勾配ブースティングはかなり奥が深く、専門的な内容なので詳細については割愛します。
興味がある方は[こちら](https://qiita.com/kuroitu/items/57425380546f7b9ed91c)の記事がわかりやすいです。


</details>


![ChatGPT Image 2025年5月26日 16_27_05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4079471/feb7c295-f76b-4426-a874-8f23268c9a21.png)

<h2 id="使った技術">使った技術</h2>

このBotを実現するために、以下の技術を組み合わせました。

* LINE Messaging API
* yfinance（株価データ取得）
* LightGBM（株価予測モデル）
* FastAPI（API構築）
* Render（APIのホスティング）
* Make（LINE Bot連携）
## 実際に作った流れ

1. **「明日の株価予測APIがないかな？」と探し回る**  
   まずは「明日の株価を予測してくれるAPIなんて都合のいいものがあるはず！」と思って探しましたが、そんなものは存在せず…。  
   **「ないなら作るしかない！」** と腹をくくりました。 
   <br>

2. **大学時代に触ったPythonを思い出しつつ、コードを書き始める**  
   予測モデルの作成には、**yfinance** での株価データ取得、**LightGBM** での予測、そして **FastAPI** でAPI化する方法を選びました。  
   API化によって、どこからでも株価予測ができるようにするため、データ取得からモデル構築、APIへの組み込みまで一気通貫で実装しました。


   実際に作成したコードは以下の通りです。（コメント付きで解説しています）  
   **コピペOKです！自由に使ってください！**


```python
from fastapi import FastAPI  # FastAPIをインポートしてAPIアプリを作成する
import yfinance as yf  # yfinanceを使って株価データを取得する
import pandas as pd  # データ処理用のpandasをインポート
import lightgbm as lgb  # LightGBMで株価予測モデルを作成
from sklearn.metrics import mean_squared_error  # モデル評価指標（RMSE）を計算するために使用
import numpy as np  # 数値計算用のnumpyをインポート
from sklearn.preprocessing import StandardScaler  # データの標準化に使用

app = FastAPI()  # FastAPIアプリのインスタンスを作成

@app.get("/predict/{symbol}")  # GETリクエストで/predict/シンボル のエンドポイントを作成
def predict_stock(symbol: str):  # リクエスト時にシンボルを受け取る関数
    try:
        df = yf.download(symbol, period="2y")  # 2年間分の株価データをyfinanceで取得
        if df.empty or len(df) < 60:  # データが取得できない場合や少なすぎる場合はエラーを返す
            return {"error": f"データが少なすぎるか取得できませんでした: {symbol}"}

        if isinstance(df.columns, pd.MultiIndex):  # 列名がMultiIndexの場合は1列にまとめる
            df.columns = ['_'.join(col).strip() for col in df.columns.values]

        close_cols = [col for col in df.columns if 'close' in col.lower()]  # 'close'を含む列名を探す
        volume_cols = [col for col in df.columns if 'volume' in col.lower()]  # 'volume'を含む列名を探す
        if not close_cols:  # Close列が見つからない場合はエラー
            return {"error": f"Close列が見つかりません: {symbol}"}
        if not volume_cols:  # Volume列が見つからない場合はエラー
            return {"error": f"Volume列が見つかりません: {symbol}"}

        close_col = close_cols[0]  # Close列名を決定
        volume_col = volume_cols[0]  # Volume列名を決定

        df = df.reset_index()[['Date', close_col, volume_col]]  # 日付、Close、Volume列だけに絞って取得
        df = df.rename(columns={close_col: 'close', volume_col: 'volume'})  # 列名を統一

        df['log_volume'] = np.log1p(df['volume'])  # 出来高を対数変換（0回避のためlog1p）
        df['price_volume'] = df['close'] * df['volume']  # 株価×出来高の値を計算
        df['log_price_volume'] = np.log1p(df['price_volume'])  # 株価×出来高の対数変換

        df['close_lag1'] = df['close'].shift(1)  # 1日前の終値
        df['close_lag2'] = df['close'].shift(2)  # 2日前の終値
        df['ma5'] = df['close'].rolling(5).mean()  # 5日移動平均
        df['ma10'] = df['close'].rolling(10).mean()  # 10日移動平均
        df['return'] = df['close'].pct_change()  # 前日比（リターン）
        df['volume_lag1'] = df['volume'].shift(1)  # 1日前の出来高
        df['volume_ma5'] = df['volume'].rolling(5).mean()  # 5日移動平均の出来高
        df['volatility'] = df['return'].rolling(5).std()  # 5日間のリターンの標準偏差（ボラティリティ）
        df['volatility_10'] = df['return'].rolling(10).std()  # 10日間のリターンの標準偏差
        df = df.dropna()  # 欠損値が含まれる行を削除

        scaler = StandardScaler()  # 標準化用のscalerを作成
        scaled_features = scaler.fit_transform(df[['return', 'volatility', 'volatility_10']])  # 特定の列を標準化
        df[['return_scaled', 'volatility_scaled', 'volatility_10_scaled']] = scaled_features  # 標準化後の値を新しい列に追加

        feature_cols = [  # モデルに使う特徴量のリスト
            'close_lag1', 'close_lag2', 'ma5', 'ma10', 'return_scaled',
            'volume_lag1', 'volume_ma5', 'volatility_scaled', 'volatility_10_scaled',
            'log_volume', 'log_price_volume'
        ]

        X = df[feature_cols].copy()  # 特徴量データをXとして作成
        X.columns = [f"feature_{i}" for i in range(X.shape[1])]  # 列名をシンプルな名前にリネーム
        y = df['close']  # 目的変数（予測対象）は終値

        split_idx = int(len(df) * 0.8)  # データの8割を学習用、2割を検証用に分ける
        X_train, X_val = X.iloc[:split_idx], X.iloc[split_idx:]  # 学習データと検証データに分割
        y_train, y_val = y.iloc[:split_idx], y.iloc[split_idx:]  # ラベルも分割

        if len(X_train) < 30 or len(X_val) < 30:  # データ量が少なすぎる場合はエラー
            return {"error": f"学習データが少なすぎます: {symbol}"}

        model = lgb.LGBMRegressor(  # LightGBM回帰モデルを作成
            n_estimators=5000,  # 決定木の最大数
            learning_rate=0.01,  # 学習率
            random_state=42  # 乱数シード
        )

        model.fit(  # モデルを学習
            X_train, y_train,
            eval_set=[(X_val, y_val)],  # 検証データで評価
            eval_metric='rmse',  # 評価指標はRMSE
            callbacks=[lgb.early_stopping(stopping_rounds=50, verbose=False)]  # 50回改善しなければ早期終了
        )

        last_row = df.iloc[-1]  # 最新のデータ（予測に使う）
        X_pred = pd.DataFrame({X.columns[i]: [last_row[feature_cols[i]]] for i in range(len(feature_cols))})  # 最新データを特徴量形式に変換
        pred_close = model.predict(X_pred)[0]  # 翌日の株価を予測

        val_pred = model.predict(X_val)  # 検証データでの予測
        rmse = np.sqrt(mean_squared_error(y_val, val_pred))  # RMSEを計算
        trend = "↑" if pred_close > last_row['close'] else "↓"  # 予測値が終値より高いか低いかで方向を決定

        return {  # 結果を返す
            "symbol": symbol,
            "現在の終値": round(last_row['close'], 2),
            "予測終値": round(pred_close, 2),
            "方向": trend,
            "RMSE（誤差）": round(rmse, 2),
            "学習回数": int(model.best_iteration_) if model.best_iteration_ is not None else model.n_estimators
        }

    except Exception as e:  # エラー発生時はエラー内容を返す
        return {"error": str(e)}
```
　　
3. **FastAPIで作ったAPIをRenderでデプロイし、常時稼働できるようにする**  
   作成したFastAPIアプリは、**無料で使えるRender** というクラウドホスティングサービスにデプロイしました。  
   Renderを使うことで、**作成したAPIを24時間稼働させ、外部からのリクエストに常に応答できる状態にしました**。
   <details>
<summary>Renderの詳細についてはこちら</summary>

[こちら](https://www.youtube.com/watch?v=nOP8khZhjhk)の動画がわかりやすくFastAPIで作ったAPIをRenderでデプロイするまで解説してくれています。
英語の動画ですが、**プログラミングやITツールを使用する方は英語での情報収集は避けては通れない**ので翻訳等を駆使しながらチャレンジしてみてください

</details>
   
　　
4. **MakeでLINE Botと連携**  
   MakeでLINE Botと連携させる部分では、HTTPモジュールを使ってAPIにリクエストを送るように設定しました。  
   LINEで送信されたメッセージ（シンボル）を受け取り、APIで株価予測を取得し、その結果をLINEに返信するという流れです。

   実際のMakeシナリオはこんな感じです。
   <details>
<summary>各モジュールの詳細についてはこちら</summary>

LINE (Watch Events)モジュール

![スクリーンショット 2025-05-26 233140.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4079471/39ad61f9-d9f5-4e23-bebc-5aa44086e119.png)


THHP　(Make a request)モジュール

![スクリーンショット 2025-05-26 233754.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4079471/7a6f424a-dade-43e2-8120-37640990ed5a.png)


LINE (Send a Reply Message)モジュール

![スクリーンショット 2025-05-26 233830.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4079471/936b5266-a610-46e8-a21d-404f49b48de7.png)

画像のように設定してください。
LINE (Watch Events)モジュールについてはMessaging API設定が必要になります。わからなければ[こちら](https://note.com/shuzooooo/n/n311b619a3183?utm_source=chatgpt.com)の記事がわかりやすいです。



</details>


   ![スクリーンショット 2025-05-26 140319.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4079471/4473338b-714a-4fd2-a258-79f8e2c04c1f.png)

   LINEから送られたシンボルをAPIに渡し、取得した予測結果をLINEで受け取れるようにしています。  
   このシンプルなシナリオのおかげで、デジタルが苦手な叔父でも簡単に使えるBotが完成しました。
　　
5. **実際にテストを繰り返し、デプロイ**  
   テストで何度もデジタルが苦手な叔父に実際に使ってもらい、「これなら使える！」という状態までブラッシュアップを重ねました。  
　　
6. **完成！**  
Renderのサーバーを起動させて、MakeでもRun onceボタンを押してLINEでAPPLと送ってみましょう！
冒頭で紹介したように、**明日の予測株価が送られてきたら成功**です！
お疲れ様でした！
<h2 id="苦労したこと">苦労したこと</h2>

初心者向けに記事を書いてますが、実は私もデジタルについては初心者です…
このBotの作成過程で苦労したことを紹介します

* **Pythonがそもそもよくわからなかった**  
  作りたいものは頭の中にあるのに、**それをどうやってコードに落とし込めばいいかが全く分からなかったです…**。  
  何度もChatGPTに「こういうことがしたいんだけど、どう書けばいい？」と質問したり、インターネット検索で似た例を探したり、大学時代に使っていた参考書を引っ張り出してきたりして、少しずつ形にしていきました。
<br>

* **yfinanceでデータ取得エラー**  
  一部の銘柄で「終値（Close）」が取得できず、エラーが頻発…。  
  **終値がない場合は調整後終値（Adj Close）を扱う** ことで対応し、無事に解決できました！

  　　
* **かっこの閉じ忘れやインデントエラー**  
  Pythonコードでのシンプルなミスで何度もつまずきましたが、**その都度ChatGPTに相談しながらデバッグを繰り返し、問題を解決していきました**。  
  **「これ便利すぎるだろ」と思いながら、気づいたらエラーも解消していて本当に助かりました！**
<br>

* **No further splits with positive gain 警告に悩まされた**  
  LightGBMで学習を進めると、`No further splits with positive gain` という警告が大量に出ることに悩まされました。  
 **これは「これ以上データを分けても意味がないので、木を成長させません」という意味の警告で、分割条件が見つからないときに出ます。**  
  特に株価データは連続的でノイズが多いため、特徴量の分散が小さいと分割できないことが多いです。 
  
:::note info
株価データはいろんな要因が複雑に絡む事や見えないデータが多いが多いことでノイズが多いと言われています！
:::



  そこで、**特徴量エンジニアリング（データを加工して新しい特徴を作ること）** を行い、分割しやすいデータにしました!

  ### 対策として行った特徴量エンジニアリング

  | 問題            | 特徴量エンジニアリング                   |
  | ------------- | -------------------------------- |
  | 値が大きすぎて分けられない | **対数変換で値を小さくした**                 |
  | 値の範囲がバラバラ     | **標準化（平均0、標準偏差1）** で値のスケールを統一した  |
  | 分けられる情報が少ない   | **移動平均、リターン、ボラティリティ、ラグ特徴量** を作った |

  これだけ対策しても`No further splits with positive gain`の警告は消えませんでした…
しかし！特徴量を増やしたことでモデルの精度は上がったので、**「まあこのままでいいか！」** と判断して、そのまま使っています！
<h2 id="天才にはなれなかった…">天才にはなれなかった…</h2>

実は失敗したけどチャレンジしていたことがあります。
作成した株価予測データを使って、**OpenAIのAPIを利用し、アナリスト風に「今は買い時です！」とか「最近のニュースではこういう動きがありました！」といった解説をしてくれる**という機能も導入しようと思っていました。
しかし、OpenAIのAPIは**有料プラン**が必要で、個人で試すにはハードルが高すぎると感じたため、今回は断念しました。

<details><summary>🟢追記！：Geminiを使うことで無料で実装できました！詳細は折り畳みご確認ください</summary>

OpenAIのAPIは有料ですが、GeminiのAPIはなんと完全無料で使用できます！
導入方法下記に記します

1.GeminiのAPIキーを[こちら](https://aistudio.google.com/app/prompts/new_chat)から取得

2.MakeでGoogle Gemini AIモジュールをHTTPモジュールとLINE(Send a Replay Message)の間に追加してAPIキーの入力後、下記のように設定（プロンプトは好みで変更してください）
![スクリーンショット 2025-05-29 002555.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4079471/6116f5c8-a1fc-4036-b505-016d89b4d0fe.png)
![スクリーンショット 2025-05-29 004744.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4079471/2a177e41-64e8-402e-935e-ec84ac110ee7.png)

3.完成！Run OnceしてAPPLと送ってみましょう！[こちらの動画](https://youtube.com/shorts/GmjUK_9R6as)のようになれば成功です！

結構簡単でしたね！






</details>


また、**APIがスリープしないようにするための仕組み作り**にもチャレンジしました。
Renderの無料プランでは、**15分間アクセスがないとサーバーがスリープ状態に入り、再起動に約5分かかる**という仕様があります。
そのため、サーバーが寝ている状態でBotを起動すると、**APIのアクセスがタイムアウトしてエラー**になってしまう問題が発生しました。

これを解決するために、**Makeで5分に1回APIにアクセスするシナリオ**を3個作り、それを**15分に1回起動するように設定して、無限ループでAPIを叩き続ける仕組み**を作成しました。
作ったシナリオはこちら

![Makeのシナリオ画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4079471/d80662cc-4f00-49a6-930b-58aadf3a491d.png)
  <details>
<summary>各モジュールの詳細についてはこちら</summary>

THHP (Make a reqest)モジュール
![スクリーンショット 2025-05-26 235650.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4079471/4e0c2064-645c-43dc-ae43-5713f0f84ef9.png)

Tools(Sleep)モジュール
![スクリーンショット 2025-05-26 235733.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4079471/0de28109-1a28-447b-b0f7-2eec93236492.png)


各モジュールを全て画像のように設定しています。

</details>

このアイデアが思いついたときは「これ天才じゃね！？」と思い、記事に書くのが楽しみだったんですが……

実はこれも失敗しました。
理由は、**Makeの無料プランには月間のシナリオ実行回数に上限があり、1日と少しで上限に達してしまった**からです。
「なんでこれに気づかなかったんだろう？」と思ったのですが、**Makeのダッシュボードが全部英語で、無料プランの制限内容や実行状況が分かりにくかった**ことが原因でした。

上限に達した時の画面はこちら

![上限に達した時の画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4079471/bfbea2a6-6d8c-493f-8db2-2ddc0d156725.png)

これからは、英語の勉強もちゃんとしようと思います…

<h2 id="今後の可能性">今後の可能性</h2>

今回作ったシステムは、株価をAIで予測し、LINEで毎日通知するところまででしたが、ここからさらに発展させていきたいと考えています。

まず、予測を作ったら終わりではなく、**実際に当たっているのかを振り返る仕組み**を作りたいです。過去の予測値と実際の株価を並べてグラフで確認し、予測の精度を可視化することで、**AIの予測がどれくらい信頼できるのか**を確かめたいと思っています。そのために、データベースとして**SQLite**や**PostgreSQL**を使い、可視化ツールとしては**Streamlit**や**Gradio**を活用しようと考えています。グラフの描画には**Matplotlib**や**Plotly**を使い、データ処理は**Pandas**や**Numpy**で行う予定です。正解率や誤差（RMSE）、上昇・下降トレンドの的中率なども確認できるようにして、毎日の予測がどれくらい当たっているかを**一目で把握できるダッシュボード**にしたいと思っています！

さらに、**株価チャートを自動で画像生成し、LINEに送信できるようにする**機能も追加したいです。今はLINEで数字のメッセージを受け取るだけなので、そこにチャート画像を加えることで**視覚的に株価の動きを確認できる**ようになります。これには**Matplotlib**や**Plotly**でチャートを作り、**Pillow**で画像ファイルを保存し、LINEで画像を送信する流れを考えています。毎朝「今日の株価予測」の通知と一緒に、直近の株価チャート画像がLINEで届くようにできれば、**より直感的に株価の動きを把握できて便利**だと思いませんか？

これらの機能を追加することで、ただの「株価予測Bot」から、  
**「予測の根拠を確認できて、視覚的にもわかりやすい投資サポートツール」**  
へと進化させていきたいと考えています。
さらに将来的には、**決算情報やニュースをAIに分析させて予測に取り入れる**ことで、**より精度の高い予測**ができるようになることを目指しています！

この記事を読んで株価予測Botを作った皆様も作って終わりではなく、**何かオリジナルのアイディアを付け足して自分だけのツール**に昇華させてください。
面白いものが作れたらぜひ教えてくださいね！


## 実際に制作・使用してわかったこと

今回は **デジタル知識ほぼ0の私** が、**APIを自作し明日の株価予測Botを作成** することができました。

様々な文献やエンジニア様方の記事や生成系AIを活用することで、知識がなくてもある程度はなんでも作れるものなんですね。

ちなみに今回作ったLINE Botツールを実際に使ってみると、精度は体感で **75％** ってところです！（現時点では）

低くはないですが、信用して投資できるほどではないですね…。  
まだまだ改善の余地はありますが、当初の課題である **毎日チャート見る時間を減らすという目標は達成** しました！

次は実用化に向けて頑張ってみます！


<h2 id="まだまだ努力！">まだまだ努力！</h2>
まだまだデジタル人材とまでは言えませんが、社内のデジタルリーダー目指して頑張ります！
最後に参考文献や記事ご紹介します。ありがとうございました。

https://note.com/o_ob/n/naa0a7dc70afc

https://qiita.com/s053050g/items/ec57a6f0130a9c06c460

https://qiita.com/aya451778/items/e13f2dafd72bf4a38c9c