メインコンテンツまでスキップ

CrewAI+ローカルLLMで株の銘柄選定して儲けよう

辻野
ディアシステム(株)開発一部第1課

開発1部の辻野です。

1. はじめに

昨今、技術界隈の話題の中心は言うまでもなくAIですが、今年はOpenClawがバズったりして、AIに実際の仕事をやらせるAIエージェントが本格的に実用段階に入ってきている感じがします。ということで、私もブームに乗っかってAIネタで一本記事を書いてみることにします。ただし、有名どころのChatGPTやClaudeではなく、ローカルLLMとオープンソースのエージェントフレームワークという、いささかニッチな切り口からアプローチしてみます。

この記事では、提供されているサービスの使い方についてではなく、仕組みそのものを自前で構築していく話をします。よって、ほとんどの方にとっては役に立たない内容になるかと思いますが、自分で構築する方が仕組みや構造に対する理解も深まりますし、何より創意工夫するエンジニアリングの楽しみを優先します(実用性はともかくとして)

※ 細かく書くと長くなるので、あまり具体的な詳細については記述しません。あと、私は専門家では無いので記載に誤りがあるかもです。

2. 事の発端

楽して金を稼ぎたい。株で儲けたいが、経済のことは疎いし企業分析とか良く分からない… ならば、買うべき株をAIに選んでもらえば良いのでは?そうすれば知識が無くても手堅く利益を得れるかもしれない。

…しかし、クラウドモデルを契約して金が掛かるのは嫌だし、利用制限とか気にせずに思う存分試行錯誤がしたい。 幸い、手元には値上がり前に確保したNVidiaグラボがある。これでLLMを動かせばランニングコストは電気代くらいで使い放題では。

という経緯で今回のテーマに取り組むに至りました。うまくいくか分かりませんが、とりあえずやってみましょう。 (実際にこれをやったのは数ヶ月前の話で、思い出しながら書いています)

3. 全体概要

まず、自宅PCにローカルLLMのバックエンドを構築します。そして、エージェントの実行環境となるクライアントPC上から、このバックエンドのLLMエンドポイントに接続し、全自動で銘柄選定に必要な情報収集と解析を行っていきます。 タスクは情報収集のフェーズと銘柄選定を行う解析フェーズの大きく2つに分け、情報収集フェーズでは選定対象となる銘柄のテクニカル指標、ファンダメンタルズ指標、国内外の市場の状況などを収集、整形してファイル出力し、解析フェーズではこれを材料に明日上がる銘柄、つまり買うべき株をランキング形式で出力させます。その際、選定の根拠や判断基準も合わせて出力させ、内容が妥当か、適当なことを言っていないかを別のAIに読み込ませ、結果の妥当性をチェックすることにします。 ちなみに自動売買はやりません。既存の口座をAIに好き勝手に触らせるのは危なすぎるし、専用口座を用意するとなると何かと面倒くさいからです。

4. 環境構築

LLMバックエンド

現在、グラボが刺さっているのはWindows PCなので、WSL上にNVIDIA Container Toolkitを入れ、その上にDockerでOllamaコンテナを起動させることにします。 Ollamaは簡単に言うとLLMモデルを読み込み、APIエンドポイント経由で利用できるようにするためのソフトウェアです。各レイヤ構造を雑に示すと以下のような形になります。また、Ollamaは自前のリポジトリを持ち、LLMモデルを管理する機能があるため、モデルのダウンロードも"ollama pull ..."というようなコマンドで出来るようになっています。

LLMモデル(Gemma4)
--------------------
Ollamaコンテナ(Docker)
--------------------
WSL (with NVIDIA Container Toolkit)
--------------------
OS:Windows

LLMモデル選び

クラウド型モデルの影に隠れがちですが、昨今のローカルLLMの進歩は凄まじく、その性能はおよそ1年前のクラウド型モデル並みとも言われています。技術の進歩とともにダウンサイジングが進んでいくのは自然な流れでしょう。現在このジャンルを先行しているのは、GoogleのGemmaとAlibabaのQwenだと思います。ちょうど今回の環境を構築した時点では、GoogleのGemma4が発表されたばかりだったので、これを使うことにします。

モデルが決まれば次はサイズを選ぶわけですが、当然サイズが大きい方が賢いものの、処理コストは高くなるため、応答速度は遅くなります。そして、あまりにサイズが大きすぎるとVRAMに収まりきらずにオフロード(モデルの一部がメインメモリにはみ出す形で乗る)が発生し、実行速度が極端に低下します。 LLMモデル選定で考慮すべきはパラメータサイズと量子化サイズです。パラメータサイズとは簡単に言うとAIの脳の大きさで、B(Billion=10億)という単位で表されます。31Bのモデルだとおよそ310億パラメータという感じですね。ちなみにクラウドモデルのGPT-4は1.8兆パラメータと言われており桁が違います。しかし、Gemma4 31BのベンチのスコアはGPT-4相当と言われているので、このダウンサイジングがいかに凄いことか分かります。 もう一つの量子化とはデータの精度を落とすことでLLMを軽量化する技術のことで、Q4_K_Mとかモデル名の後ろに付与されるあれのことです。例えば16bitで構成されているデータを8bitに丸めればデータサイズとしては約半分、4bitにすればおよそ四分の一までデータサイズを圧縮することができ、この技術を用いることで、家庭用PCには乗らないようなサイズのモデルも動かすことができます。しかし、デメリットもあり、精度を落とせばそれだけ大雑把になり賢さが衰えていきます。一般的にQ3(3bit)以下に落とすと性能劣化が顕著になるとされ、Q4(4bit)あたりがサイズとパフォーマンスのバランスが良いと言われています。量子化には他にも、QAT(Quantization-Aware Training)という技術があり、ある程度量子化されることを前提として学習を行うことで精度の劣化を軽減したり、imatrix(Importance Matrix)と呼ばれる重要なデータとどうでもいいデータで量子化の度合いに差をつけ、均一に切り捨てをしない技法などがあって、なかなか奥が深いジャンルです。HuggingFaceなどで探すと、多くの専門家たちがGGUFと呼ばれる形式で独自の量子化を施したモデルを公開しています。

モデルのサイズはパラメータサイズと量子化のサイズの組み合わせから、VRAMに収まるサイズのものを選ぶことになりますが、試行錯誤を繰り返した結果、現環境(16GB VRAM)では、26B-A4BのQ4_0 QAT に落ち着きました。26B-A4Bはそもそも家庭用グラボで動かすには少々大きいモデルですが、Q4の量子化を施すことで、コンテキストサイズを考慮してもギリギリVRAMに収めることができ、かつQATにより性能劣化もある程度抑えられています。何よりこのモデルの素晴らしいところは、MoEアーキテクチャと呼ばれる構造を採用しており、26Bというサイズではあるものの、実働時には使用するパラメータを厳選し、実質4B相当として動作するところにあります(Active 4B)。この構造により、非力なGPUであっても高速動作が期待でき、応答速度はよりサイズが小さい12Bよりも速くなります。大量のデータを渡して処理させるとそれだけ時間がかかるので、このレスポンスの速さというのは結構重要になってきます。Gemma4でMoEアーキテクチャが採用されているのはこの26B-A4Bのみですが、26Bという莫大な知識を持ちながら4B相当の速度を誇るこのモデルは、現時点で家庭用グラボで動作可能なモデルとして最適解であると思います。

CrewAI - AIエージェントのフレームワーク

タダで使えてシンプルかつプログラマブルに扱えるAIエージェントのフレームワークを探したところ、これを使うことにしました。 https://crewai.com/

当時、候補として検討したのがこのCrewAIとLangChainでしたが、LangChainがなんとなくややこしそうな雰囲気があったのでこちらを使ってみることにしました。これが一体何なのかというと、平たく言うとPythonプログラム上からLLM(ここではOllamaでホストしているAPIエンドポイント)にプロンプトを投げ、その応答からCrewAIが提供(または自作した)Tool Callingを実行し、実行環境に直接作用させる仕組みを提供するものです。つまり、AIに命令を出したり、AIが実行可能な操作を与えたりをプログラムで書いて実現できるという感じです。 ここでAIエージェントの仕組みを簡単に解説すると…

  1. 事前に定義されたTool Callingの構文をLLMに覚えさせ、例えばファイルを読み出したかったらtool_call: FileRead {path: ...}のような応答を返せと指示する。
  2. LLMがファイルを読み出したいと考えたら、tool_call: FileRead {path: ...}というトークンを応答文に埋め込む。
  3. 応答を受け取ったクライアント(ランタイムと言ってもいいかも)がtool_call: FileRead {path: ...}というトークンを発見すると、その記述内容から記載されているファイルを読み出し、内容をコンテキストに注入してLLMに返す。

というような具合です。そもそもLLMモデルは文章を返却することしか出来ないので、返却された文章から命令文を読み取り、実環境(クライアントPC)で実行する仕組みを与えることで、LLM(脳)に手足を生やすようなイメージです。そういう構造なので、AIエージェントはLLM本体とは別の環境で動かしても構わないが、操作したい対象PC上で動作する必要があります。 CrewAIの特徴としては、大きな括りで役割分担を担うAgent(AI的な人の単位。「あなたは〜〜です」みたいなシステムプロンプトレベルで切り分け可)とそのAgentが担うべきTask(実際に実施すべき仕事)をそれぞれ定義し、AgentにTaskを登録することでAgentにその仕事をさせるという抽象化の形態となっています。もちろん、Agent同士が協調動作したり並列で仕事をすることも可能です(ただしスペック的な問題でローカルLLMでは逐次実行一択)。 ほぼPythonプログラム1本である程度の複雑な仕事をさせることができ、前述のTool Callingの拡張(命令セットの自作や他のエージェントフレームワークが提供する機能の呼び出し)も割と簡単に実現できます。

※参考:コードのサンプル https://docs.crewai.com/v1.15.18/en/examples/example

このCrewAIの実行環境もコンテナで作ります。pythonのコンテナイメージをベースイメージにして、必要なライブラリをインストールするだけなので特筆することはありません。 pythonのプログラム実行は"docker exec"で任意のスクリプトを叩く形にしたいので、コンテナの起動コマンドは "tail -f /dev/null"などにして常駐させておきます。

ここまで出来たら環境は完成で、あとは株の銘柄選定を行うプログラムを記述するだけです。

5. 銘柄選定プログラムの設計と実装

母集団の決定

まずは銘柄選定の対象となる母集団を決める必要がありますが、結論としてはローカルLLMというスペック的な制限のある環境で動かす以上、何百という銘柄を対象に付随する情報を取得し比較検討や推論を動かすのは無理があります。LLMはコンテキストのサイズが増加し、無関係な情報が増えればそれだけ推論精度が低下する傾向があり、ある銘柄の値動きを推論しようとしている時に、それとはあまり関係の無い他の銘柄の情報が大量に含まれると、それだけ必要な情報の密度が薄まり、重要な情報や前提を見落としたり、あまり関係ない情報を拾って信用ならない結果を返してきたりします。感覚的には16GB VRAM程度の環境で動作するモデルであれば、コンテキストサイズは32K〜64Kあたりに制限するのが無難なように感じます。上限を超えると切り捨てられてしまうため、そのサイズに収まる程度の情報量で処理できる数に対象を絞る必要があります。

最終的に採用したのが、Trader's WEB※で公開されている「初動銘柄」です。WEB上での説明は以下。

『東証プライム』の初動銘柄
東証プライムの全銘柄について、出来高率上位200銘柄のうち、当日5日移動平均線をはさんで上にブレークした銘柄と下にブレークした銘柄を、出来高率の大きい順にならべています。
5日乖離率、及び25日乖離率の数字の右に表記されている「↑」は、それぞれの移動平均線を上にブレーク、「↓」は下にブレークしたことを示します。
出来高率 = 当日の出来高 / 発行済み株式数 × 浮動株比率 × 100 で求めたものです。

https://www.traders.co.jp/market_jp/breakout/tp

株は一度上昇トレンドが発生すると、しばらくそのトレンドが継続する傾向があると言います。つまり、上昇トレンドに入ったと見なされる初動銘柄を対象に、とりわけ値動きが大きく、勢いが継続しそうな銘柄を厳選すれば勝率は高くなり、母集団としての数も十数銘柄程度に収まるので、ローカルLLMでも精度の高い推論を行うことが出来るはずです。 加えてこのサイトには移動平均の乖離率などテクニカルな指標も掲載されているので、これらもまとめて一緒に取得することにします。「このサイトの内容を読み出し、上にブレークアウトした銘柄の一覧を取得し、JSON形式でファイル出力せよ。」という感じのプロンプトを用意し、プログラム上からLLMに渡すだけです。面倒なスクレイピングの処理を組む必要は無く、LLMが使うツールを指定するだけで実現できます。

ファンダメンタルズ指標、その他情報の取得

選定の対象となる母集団を取得できましたが、これだけの情報で判断させる訳にはいかないです。この時点ではそれぞれの企業が何なのかまったく分かりません。そこで登場するのが、YFinanceです。 YFinanceとは、Yahoo! Financeから株価や為替、財務データなどを取得できるPythonライブラリです。これを使って銘柄コードからその企業のセクタや時価総額、PBRなど判定に使えそうな情報を色々取得します。判定に不要な余計な情報が混ざると精度が低下するため、なんでもかんでも取得することはせず、項目の厳選が重要です。 当然のことながら、CrewAIが用意しているToolにはYFinanceを実行するようなものはありません。そこでYFinanceToolという自作Toolを実装し、LLMからYFinanceを扱えるようにします。このようにして自作ツールを用意することで、LLMができる仕事の範囲を増やしていくことができます。

YFinanceToolの実装(抜粋) 自作ツールは関数を定義する形で簡単に作れます。 コメント部分がツールの使い方として、LLMに渡されるプロンプトにインジェクションされます。 ※ コード中のsleepは連続してAPIを叩きまくるとブロックされる恐れがあるため、少しウェイトを置いています。

import json, time
from crewai.tools import tool
import yfinance as yf

@tool("YFinanceTool")
def yfinance_tool(codes: list[str]):
"""
銘柄コードのリスト(例: ['6976', '3687'])を受け取り、各銘柄の主要指標をまとめて取得する。
"""
time.sleep(1)
try:
codesT = [f"{c.strip()}.T" for c in codes]
tickers = yf.Tickers(" ".join(codesT))
results = {}

for symbol, ticker in tickers.tickers.items():
code = symbol.replace('.T', '')
info = ticker.info
results[code] = {
"longName": info.get("longName", "N/A"),
"sector": info.get("sectorDisp", "N/A"),
"marketCap": info.get("marketCap", "N/A"),

...以降省略...

LLMにはこのツールを使って情報取得を行い、既に取得したjsonデータとマージするように指示します。このようなケースでは出力結果のサンプルがあると間違いが起こりにくいです。

国内外市況の取得

ここまでで銘柄に関する情報は大方揃ったわけですが、これだけではまだ世の中がどうなっているのか?という情報が足りていません。当日の国内市況や直近の米国市況を俯瞰的に説明した資料がほしいところです。 これを得るために、最初はbrowser-useという別のフレームワークを連携させて、LLMにブラウザを使わせて自律的に情報収集させようとしましたが、この方法はうまくいきませんでした。「明日の天気を教えて」などの簡単な内容であればうまくこなせますが、ちょっと命令が複雑になると勝手にウェブサーフィンを始めてしまい、帰ってこなくなってしまいます。ブラウザを操作して自律的に情報収集を行うというのは、ローカルLLMではまだ難しいようです。

結局、同じくTrader's WEBの「マーケットの話題」というコーナーから取得することにしました。このページからスクレイピングのツールを用いて記事のタイトルと日付、本文を取得させ、マークダウンの形式にまとめた上でそれぞれファイル出力するように指示します。

https://www.traders.co.jp/market_jp/wadai https://www.traders.co.jp/market_fo/wadai

これで情報収集は一通り完了になります。

銘柄選定

ここまでの処理で出来上がった材料を元に銘柄の選定を行います。 出来上がった銘柄情報(JSONデータ)、国内市況(マークダウンテキスト)、海外市況(マークダウンテキスト)の3ファイルを資料として読ませ、選定ルールをプロンプトに加えてLLMに解析してもらいます。 しかし、このプロンプトを考えるのがまた厄介で、temperatureを下げて何度か実行を繰り返し、結果が収束するように試行錯誤を繰り返す。また、結果が妥当なのかどうか良くわからないため、結果をクラウド型AIに投げて妥当性を評価してもらい、改善を加えていきます。

具体的なプロンプトの内容は省略しますが、安定した結果を得るために意識したポイントは以下。

  1. 方針の明確化:絶対に損失を出さない。急騰しすぎている銘柄は避ける。保有期間を明示し、デイトレでの売却を想定など、判定の基準となる方向性を明確にする。
  2. 評価順序の明記:まず、ヤバそうな銘柄を除外するなど、評価のステップを明確にする。
  3. ランキング形式の出力とスコアリング、選定理由を明記させ、結果の妥当性を評価できるようにする。

6. 結果 - 実際どうなのか? そして、今後の課題

本来であればデータを集めてバックテストを実施し、勝率や利益率を測定するのが望ましいところですが、面倒なのでそこまではやりません。 スクリプトがある程度動くことが確認できたら、ランキングTop3の銘柄をそれぞれ一株単位で買ってみて、その日のうちに売却するという運用をしばらく続けて感触を確かめてみます。

試してみた所感としては……「あれ?思ったより勝てるのでは?」でした。 なんとなく、割と勝てるような気がします。選定した銘柄が1日で10%程上昇したり、ストップ高になるケースもありました(ただし、一株買いなので利益は少ない)

しかし、当然ですが毎回勝てる訳でもありません。 よくあるパターンが、寄り付きで天井を掴んでしまい、そこからジリジリ利確売りに巻き込まれてマイナスになるケース。確かに前日比では上がってはいますが、一番高いタイミングで掴んでしまっているため、結果的にマイナスに終わってしまう。 初動銘柄を対象に厳選してはいるものの、前日の上昇の勢いがあまり続かず、失速して下がり始めるケースも多く感じます。昨今の自動売買の増加により、一度急激に買われた後は落ち着いてしまうことが多いのかもしれません。

また、使っている証券会社の単元未満株(S株)の取引ルールも短期売買では不利な方向に働きます。取引タイミングが寄り付き(9:00)、後場(12:30)、引け(15:30)のタイミングでしか売買できず、成行きのみ。適当に良さげなタイミングで利確したり、明らかに下がっていくところでも損切りが出来ない仕様です。この問題は取引のロットを100株単位に引き上げ、指値の金額も含めて売買戦略を立てる形にすれば良いのですが、現時点ではちょっとそこまでリスクを取ることは出来ません。

最終的には、結局は市場の地合い次第な感じを受けます。結果はトータルではプラスで終えることが多いものの、それが推論の成果なのか、市場の傾向によるものなのかいまひとつ判断がつきません。結論としては、現時点では大金を投入するにはまだまだ信頼性が低い、という感じがします。

とは言え…システム的な要領はだいたい分かってきました。戦略を練り直したり、売買のやり方を工夫して改善していけば、もしかしたら勝率を上げることが出来るかもしれません。引き続き、検証および改善を続けて、いろいろアイデアを試していこうと思います(やる気があれば)

未経験から始める
システムエンジニア

一生モノのITスキルを身につけよう

あなたの経験とスキルを
ディアシステムで発揮してください!