第1回:きっかけと全体像 — ChatGPTがファンドマネージャーに勝った日

はじめに

「AIが株価を予測する」と聞くと、少し前まではSFの世界の話のように感じていました。

私は普段、個人でWebサービスを開発しているエンジニアです。株式投資には以前から興味がありましたが、テクニカル分析やファンダメンタルズ分析を体系的に学んだことがあるわけではなく、いわゆる「普通の個人投資家」でした。

そんな私が、LLM(大規模言語モデル)をファインチューニングして株価予測AIを作り、Webサービスとして公開するまでに至った経緯を、この連載で書いていきたいと思います。

成功だけでなく、失敗や遠回りも含めて正直に書くつもりです。個人開発でLLMのファインチューニングに挑戦してみたい方、AIと金融データの組み合わせに興味がある方に、少しでも参考になれば幸いです。


ChatGPTがファンドマネージャーに勝った

2023年、英国の金融比較サイト Finder が行ったある実験が話題になりました。

ChatGPTに38銘柄を選ばせてポートフォリオを構築したところ、63日間で+4.9% のリターンを記録。同じ期間、英国で人気トップ10のファンド(HSBC、Fidelity等)の平均は -0.8% と、プロのファンドマネージャーを大きく上回る結果になったのです。

さらに、2年間の累計リターンでは +41.97% に到達。人気ファンドの平均は+27.63%で、実に14ポイント以上の差をつけました。

もちろん、Finder自身も「これをもってChatGPTを投資に使うべきだと言っているわけではない」と注意喚起しています。限定的な実験であり、たまたまうまくいった可能性もあります。市場環境が異なれば結果も違ったかもしれません。

しかし、この実験が示したことは重要でした。

LLMは自然言語から企業の状況を「理解」し、投資判断に活かせる可能性がある。

「え、本当に? ならば日本株のニュースでも同じことができるのでは?」

これが本プロジェクトの出発点でした。


なぜLLMなのか — 従来手法との違い

株価予測と聞いてまず思い浮かぶのは、LSTM(長短期記憶)やARIMA(自己回帰和分移動平均)といった時系列モデルでしょう。過去の株価データからパターンを見つけ、将来の値動きを予測する手法です。

これらは数値データの扱いに長けていますが、一つ大きな限界があります。ニュースの内容を理解できないということです。

例えば、ある企業が「経常利益51%上方修正」という決算を発表したとします。LSTMは過去の株価チャートのパターンは学習できますが、このニュースの意味を理解して翌日の株価にどう影響するかを推論することはできません。

一方、LLMは自然言語を理解できます。「51%上方修正」がポジティブなニュースであることを理解し、さらに企業の業種や時価総額、直近の株価推移といった文脈と合わせて、翌日の株価変動を予測できる可能性があります。

ニュースという自然言語情報と、株価・財務などの数値データを統合して予測に活用する。 これがLLMファインチューニングという手法を選んだ理由でした。


何を作ったか — 千里眼サービス

最終的に作ったのは、「千里眼(Senrigan)」というAI株価予測Webサービスです。

サービスURL: https://senrigan.tech/

千里眼は、以下の5種類のデータを入力として受け取り、翌日の株価変動を予測します。

#データ種類内容
1企業情報業種、時価総額、会社概要など
2ニュース本文決算発表、PR情報、エクイティ情報など
3株価データ直近5営業日のOHLCV(始値・高値・安値・終値・出来高)
4財務データ2年分の売上高、利益率、EPS、ROA、ROE
5マクロ経済指標CPI、GDP、失業率、政策金利、為替レート

そして、以下の3つの予測値をJSON形式で出力します。

  • 当日終値 → 翌日始値(オーバーナイトの変動)
  • 当日終値 → 翌日終値(1日通しての変動)
  • 翌日始値 → 翌日終値(日中の変動)

実際の予測結果はこのようなJSONで返ってきます。

{
  "当日終値→翌日始値": {"価格": 3045, "変動率": 0.0,  "傾向": "中立"},
  "当日終値→翌日終値": {"価格": 3075, "変動率": 0.98, "傾向": "中立"},
  "翌日始値→終値":     {"価格": 3075, "変動率": 1.0,  "傾向": "中立"}
}

ニュースが発表されると自動的にデータを収集し、AIが翌日の株価変動を予測して、その結果をWebサイトに公開する。毎日自動で動くサービスです。


全体アーキテクチャ

千里眼サービスは、3つのプロジェクトで構成されています。

┌───────────────────┐     ┌──────────────────┐     ┌──────────────┐
│    meloik         │     │ assetai_firebase │     │  stockSite   │
│  ニュース収集      │     │  Firestore同期   │     │  Web UI      │
│  AI予測実行        │ ──► │                  │ ──► │              │
│  データ生成        │     │                  │     │              │
└───────────────────┘     └──────────────────┘     └──────────────┘
    VPS (PHP/MySQL)         VPS (Python)           Vercel (Next.js)

meloik(データ生成 / PHP + MySQL)

すべての始まりはここです。VPS(レンタルサーバー)上で動くPHPのバッチ処理群で、以下を担当しています。

  • ニュース収集: 適時開示情報閲覧サービス(TDnet)など、企業が公的に発表した情報を収集
  • AI予測実行: ファインチューニング済みLLMに予測リクエストを送信
  • 翻訳: ニュースと予測結果の英語翻訳(多言語対応)
  • データ生成: 企業情報、株価、財務データなどの収集・整形

コアとなる予測処理のイメージはこのような流れです。

// 予測用データの取得(企業情報+ニュース+株価+財務+マクロ指標)
$jsonData = Utility::getPredictionData($db, $company['code'], $start_date, $end_date);

// OpenAI APIに予測リクエスト
$response = callFineTunedModel($jsonData);

// 予測結果をMySQLに保存
savePrediction($db, $code, $target_date, $response);

assetai_firebase(Firestore同期 / Python)

meloikのMySQLに格納されたデータを、Firebase(Firestore)にエクスポートするプロジェクトです。

なぜFirestoreを使うのかというと、フロントエンド(Next.js)からサーバーレスで直接データを取得できるからです。MySQLに直接アクセスさせるのはセキュリティ上好ましくありませんし、APIサーバーを別途立てるのもコストがかかります。Firestoreを間に挟むことで、フロントエンドはFirestore SDKで安全にデータを取得できます。

# MySQLからデータを取得して、Firestoreに書き込む
def save_to_firestore(collection_name, doc_id, data, force=False):
    doc_ref = db.collection(collection_name).document(doc_id)
    existing_doc = doc_ref.get()

    new_epoch = data.get("updated_at_epoch")

    if existing_doc.exists and not force:
        existing_data = existing_doc.to_dict()
        old_epoch = existing_data.get("updated_at_epoch")
        if old_epoch and new_epoch and old_epoch >= new_epoch:
            return False  # 既存データが新しければスキップ(コスト削減)

    doc_ref.set(data)
    return True

平日は15分ごとに差分同期を行い、常に最新のデータがFirestoreに反映されるようにしています。

stockSite(Web UI / Next.js + Vercel)

ユーザーが実際に目にするフロントエンドです。Next.jsで構築し、Vercelにデプロイしています。

FirestoreのデータをISR(Incremental Static Regeneration)で取得し、5分間隔でキャッシュを更新。これにより、Firestoreの読み取りコストを抑えつつ、ほぼリアルタイムの情報を表示しています。


データの流れ — ニュースから予測表示まで

千里眼サービスの一連のデータフローを、時系列で整理するとこうなります。

1. ニュースが公開される
   └─► meloik がニュースを収集してMySQLに格納

2. 予測バッチが起動
   └─► 企業情報・株価・財務・マクロ指標をDBから取得
   └─► 5種類のデータをJSONに組み立て
   └─► ファインチューニング済みLLM(OpenAI API)に送信
   └─► 予測結果をMySQLに保存

3. 翻訳バッチが起動
   └─► ニュースと予測理由を英語に翻訳

4. Firestore同期
   └─► assetai_firebase がMySQLの差分をFirestoreにエクスポート

5. Web表示
   └─► stockSite がFirestoreからデータを取得して画面に表示

これらのバッチ処理はcrontabでスケジューリングされており、平日の市場が開いている時間帯に自動で動き続けています。人間が操作する必要は基本的にありません。


LLMファインチューニングへの道のり

さて、ここからが本題です。先ほどの全体像の中で「AI予測実行」と書いた部分、つまりファインチューニング済みLLMの構築プロセスが、この連載のメインテーマになります。

結論から言うと、最終的にはOpenAI APIのファインチューニング機能を使って、gpt-4o-miniモデルをカスタマイズしました。しかし、そこに至るまでには大きく3つのフェーズがありました。

Phase 1: ローカルGPU(RTX 3060)での挑戦
         → VRAM不足で断念

Phase 2: Google Colabでのファインチューニング
         → 3つのモデルで試行錯誤、精度が出ず

Phase 3: OpenAI APIファインチューニング
         → 約8分で学習完了、本番採用

正直なところ、Phase 1とPhase 2は「うまくいかなかった話」です。でも、この試行錯誤があったからこそ、LLMのファインチューニングに関する深い理解が得られましたし、Phase 3で正しい判断ができたと思っています。


試したモデルたち

この連載で登場する、試行錯誤の中で触れたモデルを一覧にしておきます。

#モデルパラメータ数フェーズ結果
1ELYZA Llama-3-JP-8B8B(80億)Phase 1, 2ローカルではVRAM不足。Colabでは学習は動いたが精度不足
2llm-jp-3-7.2b-instruct37.2B(72億)Phase 2追加学習の仕組みを実装したが精度不足
3rinna/japanese-gpt2-medium-Phase 2GGUF変換の練習用
4gpt-4o-mini-Phase 3本番採用。安定したJSON出力と十分な精度

最初は「自分だけのLLMを手元に持ちたい」という思いからオープンソースモデルに挑戦しましたが、最終的にはAPIファインチューニングが個人開発にとっての現実解でした。


使った技術・手法

ファインチューニングの過程で様々な技術を使いました。詳しい解説は後の回に譲りますが、全体像として先にリストアップしておきます。

技術概要本連載での扱い
量子化(Quantization)モデルの重みの精度を下げてメモリ削減第3回で解説
LoRA少ないパラメータだけを追加学習する手法第3回で解説
SFTTrainerHuggingFaceの教師ありファインチューニング第4回で使用
GGUF変換ローカル推論用にモデルを変換第4回で使用
OpenAI Fine-tuning APIAPIベースのファインチューニング第5回で詳説

連載の全体見通し

この連載は全8回を予定しています。各回の内容を簡単にまとめます。

タイトル内容
第1回(本記事)きっかけと全体像プロジェクトの動機、千里眼サービスの紹介、全体アーキテクチャ
第2回ローカルGPUでの挑戦と挫折RTX 3060でELYZA 8Bに挑戦し、VRAM不足で断念するまで
第3回LoRAと量子化の技術解説ファインチューニングで使う軽量化手法を図解で解説
第4回Colabで株価予測3つのモデルでの試行錯誤。富士山実験から本番データまで
第5回OpenAI APIファインチューニング方針転換からたった8分で学習完了するまで
第6回訓練データの設計5種類のデータ統合、データクリーニング、正解ラベルの作り方
第7回翻訳LLMの選定DeepSeekからChatGPTへ。安さに惹かれて痛い目を見た話
第8回MySQL→Firestore移行と本番運用RDB→NoSQLの壁と、コスト最適化の工夫

技術的な話が中心ですが、個人開発者としての判断や、失敗からの学びも含めて書いていくつもりです。


個人開発という文脈

最後に一つ強調しておきたいことがあります。

このプロジェクトは、あくまで個人開発です。潤沢なGPUクラスタがあるわけでも、データサイエンスのチームがいるわけでもありません。手元にあるのはノートPCのRTX 3060と、Google Colabの無料枠と、OpenAI APIのクレジットだけでした。

その制約の中で、「いかに現実的にLLMを活用するか」という視点が、この連載を通して一貫するテーマです。最新のGPUや大規模なインフラがなくても、工夫次第でLLMを自分のサービスに組み込める。その道筋をお見せできれば嬉しく思います。

次回は、最初の挑戦であるローカルGPU(RTX 3060)でのファインチューニングに挑み、見事に撃沈した話をお伝えします。


次回: 第2回「ローカルGPUでの挑戦と挫折 — RTX 3060の6GBで8Bモデルに挑む」

この記事をシェア

関連記事