Streamlit の最初の実用アプリとして、銘柄コードを入力すると J-Quants から株価を取得し、ローソク足チャートを表示する「株価ビューワー」を作ります。本記事は最短実装と、認証情報の扱い・キャッシュ設計・落とし穴を通しで公開します。

目次

  1. 完成イメージ
  2. 必要なライブラリ
  3. 認証情報の扱い
  4. アプリ本体
  5. キャッシュ設計のポイント
  6. エラー処理の最低限
  7. 落とし穴

完成イメージ

パーツ役割
サイドバー銘柄コード入力、期間スライダー、SMA 期間スライダー
メイン上部最新終値・前日比のメトリクス
メイン中段ローソク足 + 出来高 + SMA(25 / 75)
メイン下段直近 30 日のテーブル

必要なライブラリ

Terminal window
pip install streamlit pandas requests plotly python-dotenv

検証バージョン: Python 3.12.5 / pandas 2.2.3 / streamlit 1.40 / plotly 5.20

認証情報の扱い

J-Quants の API キーをコードに直接書くのは禁止です。次のいずれかで管理します。

方法用途
.env + python-dotenvローカル開発
~/.streamlit/secrets.tomlStreamlit の標準。st.secrets で読める
環境変数サーバー / CI 環境

Streamlit Cloud にデプロイする場合は secrets.toml が標準です。本記事ではローカル前提で .env を使います。

.env
JQUANTS_API_KEY=xxxxxxxxxxxx

アプリ本体

"""app.py — J-Quants 連携の株価ビューワー
"""
from __future__ import annotations
import os
from datetime import date, timedelta
import pandas as pd
import requests
import streamlit as st
from dotenv import load_dotenv
from plotly.subplots import make_subplots
import plotly.graph_objects as go
load_dotenv()
API_KEY = os.environ["JQUANTS_API_KEY"]
JQ_BASE = "https://api.jquants.com/v2"
# --- J-Quants クライアント -----------------------------------------------------
@st.cache_data(ttl=60 * 30)
def fetch_prices(code: str, start: date, end: date) -> pd.DataFrame:
"""銘柄 1 つの日次株価を J-Quants から取得する。"""
headers = {"x-api-key": API_KEY}
params = {"code": code, "from": start.isoformat(), "to": end.isoformat()}
rows: list[dict] = []
pagination_key: str | None = None
while True:
if pagination_key:
params["pagination_key"] = pagination_key
res = requests.get(
f"{JQ_BASE}/equities/bars/daily", headers=headers, params=params, timeout=15,
)
res.raise_for_status()
data = res.json()
rows.extend(data["data"])
pagination_key = data.get("pagination_key")
if not pagination_key:
break
df = pd.DataFrame(rows)
if df.empty:
return df
df["Date"] = pd.to_datetime(df["Date"])
df = df.sort_values("Date").reset_index(drop=True)
return df
# --- チャート --------------------------------------------------------------
def build_chart(df: pd.DataFrame, sma_short: int, sma_long: int) -> go.Figure:
df = df.copy()
df[f"sma{sma_short}"] = df["C"].rolling(sma_short).mean()
df[f"sma{sma_long}"] = df["C"].rolling(sma_long).mean()
fig = make_subplots(
rows=2, cols=1,
shared_xaxes=True,
row_heights=[0.75, 0.25],
vertical_spacing=0.03,
)
fig.add_trace(go.Candlestick(
x=df["Date"], open=df["O"], high=df["H"],
low=df["L"], close=df["C"], name="OHLC", showlegend=False,
), row=1, col=1)
fig.add_trace(go.Scatter(x=df["Date"], y=df[f"sma{sma_short}"],
name=f"SMA{sma_short}", line=dict(width=1)), row=1, col=1)
fig.add_trace(go.Scatter(x=df["Date"], y=df[f"sma{sma_long}"],
name=f"SMA{sma_long}", line=dict(width=1)), row=1, col=1)
fig.add_trace(go.Bar(x=df["Date"], y=df["Vo"], name="Vo",
marker=dict(line=dict(width=0)), showlegend=False),
row=2, col=1)
fig.update_layout(
height=600,
xaxis_rangeslider_visible=False,
margin=dict(l=40, r=20, t=20, b=20),
template="plotly_white",
)
return fig
# --- アプリ本体 -------------------------------------------------------------
st.set_page_config(page_title="株価ビューワー", layout="wide")
st.title("株価ビューワー")
with st.sidebar:
st.header("設定")
code = st.text_input("銘柄コード", value="7203")
days = st.slider("表示期間(営業日換算)", 60, 1000, 300, step=20)
sma_short = st.slider("短期 SMA", 5, 60, 25, step=5)
sma_long = st.slider("長期 SMA", 20, 200, 75, step=5)
end = date.today()
start = end - timedelta(days=int(days * 1.5)) # 営業日換算で広めに取る
with st.spinner("J-Quants から取得しています..."):
df = fetch_prices(code, start, end)
if df.empty:
st.warning("データが取得できませんでした。コードと期間を確認してください。")
st.stop()
# 表示用に末尾だけ
df_view = df.tail(days).reset_index(drop=True)
latest = df_view.iloc[-1]
prev = df_view.iloc[-2]
pct = (latest["C"] / prev["C"] - 1) * 100
col1, col2, col3 = st.columns(3)
col1.metric("最新終値", f"{latest['C']:.0f} 円", f"{pct:+.2f}%")
col2.metric("最新出来高", f"{latest['Vo']:,.0f}")
col3.metric("最新日付", latest["Date"].strftime("%Y-%m-%d"))
st.plotly_chart(build_chart(df_view, sma_short, sma_long), use_container_width=True)
st.subheader("直近 30 日")
st.dataframe(df_view.tail(30)[["Date", "O", "H", "L", "C", "Vo"]])
J-Quants 連携の株価ビューワー画面イメージ

streamlit run app.py で起動します。サイドバーで銘柄コードを変えるたびに、キャッシュが効いていない組み合わせだけ J-Quants を呼びます。

キャッシュ設計のポイント

関数キャッシュ時間理由
fetch_prices30 分当日中の更新は分単位で起きないため

@st.cache_data は引数が同じ間だけ結果を再利用します。ユーザーが同じ銘柄を何度切り替えても、API への問い合わせは初回 + キャッシュ切れ後のみです。

エラー処理の最低限

J-Quants との通信は失敗する前提で書きます。

try:
df = fetch_prices(code, start, end)
except requests.HTTPError as e:
st.error(f"取得に失敗しました: {e.response.status_code}")
st.stop()
except requests.Timeout:
st.error("J-Quants の応答がタイムアウトしました")
st.stop()

st.stop() を呼ぶと、それ以降のレンダリングは止まります。空のチャートを描画してしまうのを防げます。

落とし穴

  • API キー漏れ: .env を git に入れない。.gitignore を必ず確認
  • 無限レンダリング: ウィジェットの値で st.experimental_rerun() を呼ぶと無限ループになる場合がある。基本は不要
  • キャッシュキー衝突: 同名の関数を 2 箇所で定義するとキャッシュが食い違う。1 箇所に集約する
  • ローソク足の欠損日: 営業日に穴があると間隔がブレる。fig.update_xaxes(rangebreaks=...) で土日祝を非表示にできる
  • 大量データ: 数年分 × 大量銘柄を 1 セッションで取り込むとメモリを食う。期間を絞るか、銘柄を分ける

生成AI へのプロンプト例

目的:
J-Quants から日次株価を取得し、Streamlit でローソク足ビューワーを表示する。
入力:
- 銘柄コード(text_input)
- 表示期間(slider, 60-1000 営業日)
- 短期/長期 SMA 期間(slider)
要件:
- J-Quants の id_token は refresh_token から取得し @st.cache_data でキャッシュ
- 株価取得関数も @st.cache_data でキャッシュ(ttl=30 分)
- ローソク足 + 出来高(2 段)、Plotly の make_subplots
- 上部に最新終値・前日比・最新日付のメトリクス
- 直近 30 日のテーブル
- エラー時は st.error → st.stop
制約:
- streamlit 1.40 系 / pandas 2.2 系 / plotly 5.20 系
- 認証情報は .env から python-dotenv で読み込み
- ファイル名は app.py

まとめ

  • 銘柄コード入力 → J-Quants 取得 → ローソク足表示が 1 ファイルで実装できる
  • 認証情報は .env または secrets.toml。コードに書かない
  • @st.cache_data の TTL を関数ごとに使い分ける(token は長め、prices は中間)
  • HTTP エラー / タイムアウトは必ず捕まえて st.stop() する