Streamlit の最初の実用アプリとして、銘柄コードを入力すると J-Quants から株価を取得し、ローソク足チャートを表示する「株価ビューワー」を作ります。本記事は最短実装と、認証情報の扱い・キャッシュ設計・落とし穴を通しで公開します。
目次
- 完成イメージ
- 必要なライブラリ
- 認証情報の扱い
- アプリ本体
- キャッシュ設計のポイント
- エラー処理の最低限
- 落とし穴
完成イメージ
| パーツ | 役割 |
|---|---|
| サイドバー | 銘柄コード入力、期間スライダー、SMA 期間スライダー |
| メイン上部 | 最新終値・前日比のメトリクス |
| メイン中段 | ローソク足 + 出来高 + SMA(25 / 75) |
| メイン下段 | 直近 30 日のテーブル |
必要なライブラリ
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.toml | Streamlit の標準。st.secrets で読める |
| 環境変数 | サーバー / CI 環境 |
Streamlit Cloud にデプロイする場合は secrets.toml が標準です。本記事ではローカル前提で .env を使います。
JQUANTS_API_KEY=xxxxxxxxxxxxアプリ本体
"""app.py — J-Quants 連携の株価ビューワー"""from __future__ import annotations
import osfrom datetime import date, timedelta
import pandas as pdimport requestsimport streamlit as stfrom dotenv import load_dotenvfrom plotly.subplots import make_subplotsimport 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"]])
streamlit run app.py で起動します。サイドバーで銘柄コードを変えるたびに、キャッシュが効いていない組み合わせだけ J-Quants を呼びます。
キャッシュ設計のポイント
| 関数 | キャッシュ時間 | 理由 |
|---|---|---|
fetch_prices | 30 分 | 当日中の更新は分単位で起きないため |
@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()する