目次
はじめに
近年、NISAなどの制度充実により、株式投資は以前よりも身近なものになってきました。それに伴い、投資判断の精度を高めるための分析手法にも注目が集まっています。
株式投資を行う際のテクニックの一つに、「テクニカル分析」があります。テクニカル分析では、株価や出来高といった過去の市場データを基に、トレンドや売買のタイミングを判断しますが、複数のテクニカル指標を分析し、それらを総合的に解釈する作業は意外と手間がかかります。
今回、LLMによるアプリケーション開発を効率良く行うためのライブラリであるLangChainを用いて、株式のテクニカル分析レポート(チャート入り)を自動生成するAIエージェントを構築しました。本記事は、構築したAIエージェントの内容に関する記事です。
ソースコード
GitHubからアクセスできます。
AIエージェントの中身
AIエージェントの構成
テクニカル分析では、移動平均、RSI、MACD など、さまざまなテクニカル指標を扱います。しかし、これらを一度に同じ文脈で扱おうとすると、それぞれの指標が本来持つ意味や前提条件が曖昧になり、結果として分析の一貫性や解釈の精度が損なわれる可能性があります。
そこで今回、「メインエージェント」と複数の「サブエージェント」を用意し、これらのエージェント群が分担してタスクを遂行する構成を採用しています。メインエージェントは、(A1)全体の実行計画の作成、(A2)各サブエージェントに割り当てるサブタスクの作成、(A3)各サブエージェントが作成した分析結果に基づいた結論の導出、を行います。また、サブエージェントは割り当てられたサブタスクについて、(B1)分析に必要なデータの取得、(B2)取得したデータに基づいた分析の実行、を行います。このように統合処理と分析処理を明確に分離することで、各テクニカル指標の特性を保ったまま分析を行うことが可能になります。
各エージェントの具体的な役割
メインエージェント
メインエージェントのワークフローは以下に示す通りです。
メインエージェントを構成する各ノードの内容は以下に示す通りです。
| ノード名 | 内容 |
|---|---|
| create_exec_plan | ユーザーから与えられたタスクを完遂するための計画を立案します。 |
| create_subtasks | 立案した計画のうち、サブエージェントに委譲する作業(サブタスク)のリストを作成します。 |
| execute_subtask | サブエージェントにサブタスクを実行させ、その分析結果を受信します。 |
| create_answer | サブエージェントから集約した中間結果を統合し、最終結果を作成します。 |
サブエージェント
サブエージェントのワークフローは以下に示す通りです。
サブエージェントを構成する各ノードの内容は以下に示す通りです。
| ノード名 | 内容 |
|---|---|
| extract_knowledge | サブタスク遂行に必要な前提知識(ナレッジ)を抽出します。 |
| create_exec_plan | メインエージェントから与えられたサブタスクを完遂するための計画を立案します。 |
| create_exec_tools | テクニカル指標データの取得に必要なツールのリストを作成します。 |
| execute_tool | ツールを実行し、テクニカル指標データを取得します。 |
| create_exec_works | テクニカル指標データを分析する作業のリストを作成します。 |
| execute_work | テクニカル指標データの分析作業を実施します。 |
| create_answer | 分析結果を集約し、中間結果を作成します。 |
エージェント内部で活用されている技術
今回作成したエージェントでは、安定したテクニカル分析を行うためにデータの扱い方や推論の進め方にいくつかの工夫を取り入れています。
知識抽出
本エージェントでは、各テクニカル指標の解釈方法などの情報を事前知識として与えることが可能です。各サブエージェントはサブタスクを実行する前に、この事前知識の中からサブタスクの遂行に必要な知識のみを抽出する機構を備えています。
例えばサブエージェントが「移動平均線を用いたトレンド分析をせよ」というサブタスクを委託された場合には、「MA5がMA25を上抜ける場合にはゴールデンクロスと判定される」や「MA5がMA25を下抜ける場合にはデッドクロスと判定される」といった関連した解釈ルールが抽出されるようになっています※。これにより、指標が示す状態や関係性を知識、分析の観点が明確になります。
※LLMに対して前提知識を与える方法として、他にはRAGなどの手法もありますが、今回は与える事前知識の量が比較的少ないと判断し、単純な知識抽出機構の実装に留めています。
ツール呼び出し
本エージェントは、外部ツールからテクニカル指標データを取得する機構を備えています。エージェントは「何を分析すべきか」「どの結果を使って判断すべきか」といった意思決定に専念し、データ取得処理そのものは専用のツールに委譲しています。
移動平均、RSI といったテクニカル指標は、あらかじめツールとして登録されている関数を呼び出すことで取得可能です。エージェントは分析タスクの内容に応じて、必要なツールの選択・呼び出しを行います。このようなツール呼び出し機構を設けることで、数値計算の正確性を担保できるほか、エージェントのプロンプトや推論ロジックをシンプルに保つことができます。
@tool
def get_open_price_history(ticker: str, period: int|str, interval: str="1d")->dict:
...
(中略)
...
# yfinanceモジュールを使用して株価データを取得
ticker_history = get_ticker_history(ticker, interval)
ticker_history.index = ticker_history.index.strftime("%Y-%m-%d")
days_before = get_days_before(period)
# データをjson形式に整形しエージェントに返す
return {"history": json.loads(get_sub_dataframe(ticker_history, ["Open"]).loc[lambda d: d.index > days_before].T.to_json())}
自己内省
本エージェントのワークフローのうち幾つかのノードには、エージェントの分析結果を振り返る「自己内省」のプロセスが実装されています。これは、推論の妥当性を確認し、結論の質を安定させることを目的としています。
具体的には、抽出された知識やツールの実行結果、導き出した結論が、タスクの目的や前提条件と整合しているかを再確認します。これにより、極端な判断や根拠の弱い結論をそのまま提示してしまうことを防ぎ、ユーザーが納得しやすいアウトプットを生成できるようにしています。
class ExecPlanCreator:
def __init__(self, model, prompt_loader: PromptLoader)->None:
...
(中略)
...
def _run(self, task: str)->str:
message_manager = MessageManager()
message_manager.add_system_message(content=self._prompt_loader.get_system_prompt())
message_manager.add_user_message(content=self._prompt_loader.get_user_prompt(mode="prerequisite",
task=task))
for _ in range(MAX_RETRY):
# 回答生成プロセス
message_manager.add_user_message(content=self._prompt_loader.get_user_prompt(mode="answer"))
answer = self._answer_llm.invoke(input=message_manager.get_messages(),
config={"callbacks": [self._callback]})
message_manager.add_ai_message(content=answer.content,
callback=self._callback)
# 自己内省プロセス
message_manager.add_user_message(content=self._prompt_loader.get_user_prompt(mode="reflection"))
reflection = self._reflection_llm.invoke(input=message_manager.get_messages(),
config={"callbacks": [self._callback]})
message_manager.add_ai_message(content=reflection.model_dump_json(),
callback=self._callback)
exec_plan = answer.content
exec_plan_validity = reflection
# 自己内省プロセスで「回答が妥当である」と判定されればループ終了
if exec_plan_validity.check_is_acceptable():
break
return exec_plan, message_manager.get_token_usage()
使い方
初めに、ディレクトリ直下に.envファイルを作成し、その中で環境変数を設定します。
OPENAI_MODEL_NAME="gpt-4o-mini"
OPENAI_API_KEY="...."
OPENAI_BASE_URL="https://api.openai.com/v1"
その後、MainAgentクラスのinvoke関数を呼び出すことで、テクニカル分析を実行することができます。
from lib.model.model import get_llm_model
from lib.main_agent.main_agent import MainAgent
...
(中略)
...
if __name__ == "__main__":
model = get_llm_model(target="openai")
agent = MainAgent(model=model)
task = read_prompt("input/instruction.prompt")
basic_knowledge = read_prompt("input/basic_knowledge.prompt")
response = agent.invoke(task=task,
basic_knowledge=basic_knowledge)
output_content("report.md", response["final_answer"])
最終回答はresponse["final_answer"]にmarkdown形式で格納されます。また同時に、outputディレクトリにpng形式のチャートグラフが保存されます。
例えば、「AAPL(Apple Inc.)について、移動平均線(MA5、MA25、MA75)、ボリュームレシオ、MACD、RSI、ボリンジャーバンドを用いた分析を行い、売買期待度を-100~+100の間で示して」というタスクを与えると、以下の回答が出力されます。なお、本事例では、OpenAI社のgpt-4o-miniを使用しています(最初はgpt-oss-20bで試していましたが、構造化出力を伴う推論に失敗するため、断念しています...)。
テクニカル指標の1つである「ボリュームレシオ」と「出来高」を勘違いして分析をしていますが、それ以外の移動平均線、MACD、RSI、ボリンジャーバンドについては妥当な出力結果ですね。またレポートの最後に、これらテクニカル指標の分析結果に基づいた売買期待度も出力してくれています。
今後の展望
現在はエージェントが呼び出し可能なツールがテクニカル指標関連に限定していますが、今後は財務諸表なども読み込めるようにしたいです。ファンダメンタルの分析もできるようになると、さらに説得力のあるレポートが生成できるようになるのではと思います。
注意事項
本ツールおよびその出力結果の使用によって生じたいかなる損失、損害、その他の不利益についても、作者は一切の責任を負いません。本ソフトウェアが生成した情報に基づいて行われる判断は、すべて利用者自身の責任において行ってください。本ツールの利用は、自己責任でお願いします。
