株価分析 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 で取得できる ため、 機械学習モデルの前処理が圧倒的に楽になります。


コメント