💹

株価をAIで予測するWebアプリを1ヶ月で作った話

に公開

株価の変動をAIで予測するWebアプリ「Xstock」を個人開発したので、工夫点・苦労した点などをつらつらと記述していこうと思います。今回作成したアプリは、以下のyoutubeリンクにて紹介しています。

https://www.youtube.com/watch?v=uCo5gx7ZSTM

1. 前提

1-1. 開発の動機

  • GPTやGeminiを筆頭にするマルチモーダル言語モデル(MLM)を仕事で使う機会が増え、最近始めた株式投資に流用してみたいと思ったため。
  • 買い/売りの判断材料となるテクニカル指標の定性的判断を自動化したいと考えたため
  • 株価のグラフを表示しながら、インタラクティブな操作ができるアプリが欲しいと思ったため
  • Webアプリの個人開発を通した技術力証明のため

1-2. 開発期間

以下は計画ではなく開発完了時点で振り返った結果。毎日開発できるわけではないので、着手日には飛びがある。

内容 着手日 期間
DB設計 2025/02/25~ 1d
バックエンド開発 2025/03/02~ 2wk
フロントエンド開発 2025/03/21~ 2wk

1-3. 開発環境

  • Dockerコンテナ上での動作環境を想定
    • フロントベースイメージ:node:18
    • バックエンドベースイメージ:python:3.11
  • 開発期間短縮のためエディタはcursorで補助

2. 内容

2-1. 使用したライブラリ

[A] フロント:Apach Echarts

  • グラフの種類が多く、動的な描画が可能な点が特徴
    • 単純な棒グラフ・円グラフに限らず、地図上のヒートマップやグラフ構造なども描画できる
  • 株価を表現する「ローソク足チャート」のテンプレートパターンが用意されていたため、Echartsを採用

https://echarts.apache.org/examples/en/index.html#chart-type-line

[B] バックエンド (fastapiベース)

yfinance:株価取得API

  • 無料で利用可能なAPI.
  • 銘柄コードや期間、日足・週足などの集計幅を設定することで株価の値が取得できる。

https://yfinance-python.org/#

TA-Lib:テクニカル指標計算

  • C/C++ベースで記述された、移動平均などの指標計算を行えるライブラリ

https://ta-lib.org/

SQLAlchemy:ローカルDB

  • 株価の値などを定期的に取得 → 蓄積しておくためのRDB
  • テーブルのスキーマ設計が、以下のようにBaseModelを継承したクラス形式で指定できる
  • ORMであり、コードの再利用と保守が容易
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()

class StockPrice(Base):
    __tablename__ = "stock"
    
    symbol = Column(String, primary_key=True, nullable=False)
    ds = Column(DateTime, primary_key=True, nullable=False)
    open = Column(Float, nullable=False)
    close = Column(Float, nullable=False)
    low = Column(Float, nullable=False)
    high = Column(Float, nullable=False)
    volume = Column(Float, nullable=False)

https://www.sqlalchemy.org/

2-2. 工夫した点

[A] useMutationでリアルタイムに線分描画

【ポイント】

  • useQueryを用いて、チャート上に描画する線分の座標 (x_1, y_1, x_2, y_2) のGetterをlineSegmentsキーの名前で設定しておく
  • useMutationに登録したregisterLineSegment関数で、クリックした座標情報をポスト & DB登録を行い、onSuccessで登録完了と同時に、先ほど設定したGetterを動かす
CandleStickChart.js
const CandleStickChart = ({
  data,
  onCandleClick,
  predData,
  ticker,
  title = "",
  showLine = false
}) => {
  const chartRef = useRef(null);
  const [zoomState, setZoomState] = useState({ start: 0, end: 100 });

  // manage linesegments
  const { data: lines = [] } = useQuery({
    queryKey: ["lineSegments", ticker],
    queryFn: () => getLineSegment(ticker),
    enabled: showLine,
  });

  // 略:optionsで株価のローソク足 & AI予測結果 & 取得したlinesなどを設定

  return (
    <ReactECharts
      ref={chartRef}
      option={options}
      onEvents={{ click: onChartClick }}
      style={{ width: "85%", height: "55vh", maxHeight: "370px" }}
      theme="dark"
    />
  );
};

export default CandleStickChart;
useFigureClick.js
const useFigureClick = (ticker, chartRef, xAxisData, setZoomState, showLine) => {
  const queryClient = useQueryClient();
  const [points, setPoints] = useState([]);

  const mutation = useMutation({
    mutationFn: registerLineSegment,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["lineSegments"] });
    },
    onError: (error) => {
      console.error("Error registering line segment:", error);
    },
  });

  useEffect(() => {
    if (points.length === 2) {
      mutation.mutate({ values: points.flat(), symbol: `${ticker}` });
      setPoints([]);
    }
  }, [points, mutation, ticker]);

  const handleAddPoint = useCallback((newPoint) => {
    setPoints((prev) => (prev.length === 1 ? [...prev, newPoint] : [newPoint]));
  }, []);

  useEffect(() => {
    if (!showLine) return;
    if (chartRef.current) {
      let chartInstance = chartRef.current.getEchartsInstance();
      const chartDom = chartInstance.getDom();
      // 略:クリックしたときにhandleAddPointする操作

  }, [...]);

  return {};
};

export default useFigureClick;

[B] MLMの出力したテキストをbuffer/messageに分けてStreaming

【ポイント】

  • StreamingResponseで受け取ったテキストを以下の2つのロールに分ける
    • onBuffer:チャンクレベルで随時表示
    • onLine:改行コード\nを検知し、1行が確定するごとにmessagesに追加
  • 確定したmessagesはMarkdown形式になっているので、<Markdown> {msg} </Markdown>で挟んで表示 → 箇条書き(-)や太字(**message**)などの要素がMarkdown形式で表示される
mlmApi.js
export const fetchStreamText = async (
  ticker,
  rule,
  onLine,
  onBuffer,
  onDone
) => {
  const url = "https://localhost:8000/..."; // fastapiのバックエンドAPIパス
  const response = await fetch(url);
  const reader = response.body.getReader();
  const decoder = new TextDecoder("utf-8");

  let buffer = "";

  while (true) {
    const { value, done } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split("\n");
    buffer = lines.pop();

    for (const line of lines) {
      onLine(line);
    }

    onBuffer(buffer);
  }

  // clear buffer in final line
  if (buffer) {
    onLine(buffer);
    onBuffer("");
  }

  if (onDone) onDone();
};

useStreamText.js

const useStreamText = ({ ticker, rule, isStreaming, onComplete }) => {
    const [messages, setMessages] = useState([]);
    const [buffer, setBuffer] = useState("");
    const contentRef = useRef(null);
  
    useEffect(() => {
      if (!isStreaming) return;
  
      setMessages([]);
      setBuffer("");
      fetchStreamText(
        ticker,
        rule,
        (line) => {setMessages((prev) => [...prev, line]);},
        (buf) => {setBuffer(buf);},
        () => {
          if (onComplete) {
            onComplete();
          }
        }
      );
    }, [isStreaming, ticker, rule, onComplete]);
  
    useEffect(() => {
      if (contentRef.current) {
        contentRef.current.scrollTop = contentRef.current.scrollHeight;
      }
    }, [messages, buffer]);
  
    return { messages, buffer, contentRef };
  };
  
  export default useStreamText;

2-3. 苦労した点

TA-Libのバージョン互換性

  • Dockerコンテナ上にインストールしたTA-Libのpythonバージョンに対する互換性でこけた。
  • Deep Researchに聞いても、C++のコンパイラ互換性の部分で解決せず。結局 githubのissueを見ながら、バージョンの合うta-libの探索を行ってクリアした。

https://openai.com/index/introducing-deep-research/

探索した結果:今回のアプリにおけるTA-Libの最適バージョン
RUN wget -q https://github.com/TA-Lib/ta-lib/releases/download/v0.6.4/ta-lib-0.6.4-src.tar.gz \
 && tar -xzf ta-lib-0.6.4-src.tar.gz \
 && cd ta-lib-0.6.4 \
 && ./configure --prefix=/usr \
 && make \
 && make install \
 && cd .. \
 && rm -rf ta-lib-0.6.4 ta-lib-0.6.4-src.tar.gz

 RUN pip install TA-Lib==0.6.3

3. まとめ

サマリ

  • 株価のAI分析アプリXstockを開発
  • cursorなどのAIエディタを補助的に用いて、簡単なWebアプリなら1ヶ月以内で開発
  • バージョン互換性などの課題は、Deep-researchなどの高度AI推論エージェントを用いるだけでなく、githubのissueを見るなど愚直に探索する必要あり

今後拡張したい点

  • テクニカル指標計算のロジック追加 (MACD, RSI以外にも何か追加)
  • ログイン機能追加

Discussion