スポンサーリンク
FastAPIPython

〜株価分析アプリ開発の第6回:FastAPI × yfinanceで移動平均・RSI・ボリンジャーバンド・MACD をまとめて返す株価特徴量 API を構築する

FastAPI
この記事は約9分で読めます。

株価分析 API のシリーズもいよいよ第6回。 今回は、これまでに作ってきた API をさらに強化し、複数のテクニカル指標を一括で返す“本格的な特徴量 API” を完成させます。

🔄 これまでの5回の振り返り

第1回:FastAPI の基礎と環境構築

FastAPI の基本と API サーバーの立ち上げ方を整理。 最小構成で動く API を作り、開発の土台を固めました。

第2回:yfinance で株価データを取得する API

個別銘柄の株価を取得し、JSON で返す基本 API を実装。 yfinance の使い方とデータ取得の注意点を押さえました。

第3回:月末デデータの抽出と特徴量の基礎

日次データから resample(“ME”) で月末だけを抽出する方法を解説。 分析に使いやすい“月次特徴量”の考え方を整理しました。

第4回:Rolling 関数で移動平均(MA25)を計算

Pandas の rolling() を使って移動平均を計算し、NaN の扱いも整理。 トレンドを捉える基本的なテクニカル指標を API に組み込みました。

第5回:個別銘柄+指数を横結合した特徴量 API を構築

個別銘柄と日経平均・S&P500・ダウを Date で横結合する API を完成。 分析や機械学習にそのまま使える“横持ちデータ”を返せるようになりました。

🎯 第6回のゴール

今回のテーマは、 「複数のテクニカル指標をまとめて返す API を作る」 こと。

追加する指標は以下の4種類:

  • MA(移動平均)5 / 25 / 75
  • RSI(相対力指数)
  • ボリンジャーバンド(20日 ± 2σ)
  • MACD(12-26-9)

これらを 個別銘柄+主要指数(日経平均・S&P500・ダウ)と横結合して返す API を完成させます。

🚀 FastAPI の起動方法

FastAPI は uvicorn で起動します。

▼ 起動コマンド(プロジェクトルートで実行)

uv run uvicorn app.main:app --reload

▼ 起動後の URL

http://127.0.0.1:8000

▼ API のベース URL(今回の API)

http://127.0.0.1:8000/api/v1

▼ Swagger UI(自動ドキュメント)

http://127.0.0.1:8000/api/v1/docs

🌐 今回実装したエンドポイント一覧

✔ /api/v1/hello

GET http://127.0.0.1:8000/api/v1/hello

動作確認用です。

✔ /api/v1/nikkei225

GET http://127.0.0.1:8000/api/v1/nikkei225

日経225 採用銘柄一覧を返す。

✔ /api/v1/nikkei225(今回のメイン)

GET http://127.0.0.1:8000/api/v1/monthly-features?code=7203

今回のメイン。 個別銘柄+主要指数+テクニカル指標をまとめて返す API。(上記はトヨタ)

📘 テクニカル指標の簡易説明

■ MA(移動平均:MA5 / MA25 / MA75)

過去 n 日間の終値の平均。 トレンドを滑らかにして方向性をつかむ指標。

■ RSI(Relative Strength Index)

買われすぎ・売られすぎを判定するオシレーター系指標。

■ ボリンジャーバンド(BB)

価格の標準偏差を使ってボラティリティを測る指標。

■ MACD(Moving Average Convergence Divergence)

短期 EMA と長期 EMA の差を使ってトレンドの強さを測る指標。

🧠 テクニカル指標をすべて計算し、月末データにまとめるコード

📄 app/services/stock_service.py(全文)

import yfinance as yf
from datetime import datetime, timedelta
import pandas as pd


def fetch_monthly_features(code: str):
    # 銘柄コードの判定
    if code.isdigit() and len(code) == 4:
        symbol = f"{code}.T"
    elif code in ["^N225", "^DJI", "^GSPC"]:
        symbol = code
    else:
        raise ValueError("銘柄コードは4桁の数字、または ^N225 / ^DJI / ^GSPC を指定してください。")

    # 過去5年分
    end = datetime.today()
    start = end - timedelta(days=365 * 5)

    df = yf.Ticker(symbol).history(start=start, end=end)
    if df.empty:
        raise RuntimeError(f"{symbol} の株価データが取得できませんでした。")

    # --- MA ---
    df["MA5"] = df["Close"].rolling(5).mean()
    df["MA25"] = df["Close"].rolling(25).mean()
    df["MA75"] = df["Close"].rolling(75).mean()

    # --- RSI ---
    delta = df["Close"].diff()
    gain = delta.where(delta > 0, 0)
    loss = -delta.where(delta < 0, 0)

    roll_gain = gain.rolling(14).mean()
    roll_loss = loss.rolling(14).mean()

    rs = roll_gain / roll_loss
    df["RSI"] = 100 - (100 / (1 + rs))

    # --- ボリンジャーバンド ---
    window = 20
    df["MB"] = df["Close"].rolling(window).mean()
    df["STD"] = df["Close"].rolling(window).std()
    df["UB"] = df["MB"] + 2 * df["STD"]
    df["LB"] = df["MB"] - 2 * df["STD"]

    # --- MACD ---
    ema12 = df["Close"].ewm(span=12, adjust=False).mean()
    ema26 = df["Close"].ewm(span=26, adjust=False).mean()

    df["MACD"] = ema12 - ema26
    df["Signal"] = df["MACD"].ewm(span=9, adjust=False).mean()
    df["MACD_Hist"] = df["MACD"] - df["Signal"]

    # 月末だけ抽出
    monthly_df = df.resample("ME").last()
    monthly_df = monthly_df.dropna()

    monthly_df = monthly_df[[
        "Close", "MA5", "MA25", "MA75",
        "RSI", "MB", "UB", "LB",
        "MACD", "Signal", "MACD_Hist"
    ]].copy()

    monthly_df.reset_index(inplace=True)
    monthly_df["Date"] = monthly_df["Date"].dt.strftime("%Y-%m-%d")

    return monthly_df


def fetch_merged_features(target_code: str):
    # 個別銘柄
    target = fetch_monthly_features(target_code)
    target = target.rename(columns={
        "Close": f"{target_code}_Close",
        "MA5": f"{target_code}_MA5",
        "MA25": f"{target_code}_MA25",
        "MA75": f"{target_code}_MA75",
        "RSI": f"{target_code}_RSI",
        "MB": f"{target_code}_MB",
        "UB": f"{target_code}_UB",
        "LB": f"{target_code}_LB",
        "MACD": f"{target_code}_MACD",
        "Signal": f"{target_code}_Signal",
        "MACD_Hist": f"{target_code}_MACD_Hist"
    })

    # 指数
    nikkei = fetch_monthly_features("^N225").rename(columns={"Close": "Nikkei"})
    sp500 = fetch_monthly_features("^GSPC").rename(columns={"Close": "SP500"})
    dow = fetch_monthly_features("^DJI").rename(columns={"Close": "Dow"})

    # Date で横結合
    merged = target.merge(nikkei[["Date", "Nikkei"]], on="Date", how="inner")
    merged = merged.merge(sp500[["Date", "SP500"]], on="Date", how="inner")
    merged = merged.merge(dow[["Date", "Dow"]], on="Date", how="inner")

    return merged.to_dict(orient="records")

📄 routes.py(全文)

from fastapi import APIRouter, HTTPException
from app.services.nikkei_service import fetch_nikkei225_stocks
from app.services.stock_service import fetch_merged_features

router = APIRouter()

@router.get("/hello")
def hello():
    return {"message": "Hello FastAPI!"}

@router.get("/nikkei225", tags=["Nikkei"])
def get_nikkei225():
    return fetch_nikkei225_stocks()

@router.get("/monthly-features")
def monthly_features(code: str):
    try:
        data = fetch_merged_features(code)
        return data
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))
    except RuntimeError as e:
        raise HTTPException(status_code=404, detail=str(e))

monthly_features関数の引数を修正しただけです。

🎉 まとめ

今回の第6回では、 MA・RSI・ボリンジャーバンド・MACD をまとめて返す“本格的な特徴量 API” を完成させました。

  • トレンド(MA)
  • 過熱感(RSI)
  • ボラティリティ(BB)
  • トレンド強度(MACD)
  • 市場全体の動き(指数)

これらを 1 回の API で取得できる ため、 機械学習モデルの前処理が圧倒的に楽になります。

コメント

タイトルとURLをコピーしました