J-Quants の API は便利ですが、毎回フルで叩くとレート制限に当たり、時間もかかります。本記事では、取得データを Parquet と SQLite でローカル保存し、必要な分だけ差分取得する設計の考え方を整理します。
目次
- キャッシュの全体像
- ディレクトリ構成の例
- Parquet で日次株価を保存する
- 差分更新の設計
- 全銘柄保存と日付単位ファイル
- SQLite で財務情報を保存する
- キャッシュの一貫性
キャッシュの全体像
設計の方向性は、データの性質によって変えます。
| データ | 更新頻度 | 推奨フォーマット |
|---|---|---|
銘柄一覧(/equities/master) | 日次 〜 数日 | Parquet(スナップショット単位) |
日次株価(/equities/bars/daily) | 日次(営業日終了後) | Parquet(銘柄 × 月)+ メタ管理 |
財務情報(/fins/summary) | 不定期(開示時) | SQLite(クエリ性が要る場合) |
「読み込みの速さと圧縮率」を重視するなら Parquet、「条件絞り込みのクエリ性」を重視するなら SQLite が無難です。
ディレクトリ構成の例
整理しやすい構成の例です。実プロジェクトでは合うように変えてください。
data/jquants/ listed/ listed_info_2026-05-11.parquet prices/ 7203/ 7203_2024.parquet 7203_2025.parquet 9984/ 9984_2024.parquet statements.sqlite meta.json # 最終更新日などのメタ情報銘柄ごと・年ごとにファイルを分けると、追記時の I/O が小さく済みます。
Parquet で日次株価を保存する
Parquet は列志向で、pandas との相性が良いフォーマットです。pyarrow か fastparquet のどちらかが入っていれば使えます。
from pathlib import Pathimport pandas as pd
def save_prices(df: pd.DataFrame, code: str, base_dir: Path) -> None: """銘柄ごと・年ごとに分けて Parquet で保存する。""" if df.empty: return df = df.copy() df["Date"] = pd.to_datetime(df["Date"]) df["year"] = df["Date"].dt.year out_dir = base_dir / code out_dir.mkdir(parents=True, exist_ok=True) for year, group in df.groupby("year"): path = out_dir / f"{code}_{year}.parquet" # 既存ファイルがあるなら結合して重複除去 if path.exists(): existing = pd.read_parquet(path) merged = pd.concat([existing, group], ignore_index=True) merged = merged.drop_duplicates(subset=["Date", "Code"], keep="last") else: merged = group merged = merged.sort_values("Date").drop(columns=["year"]) merged.to_parquet(path, index=False)
base_dir = Path("data/jquants/prices")# save_prices(prices, "7203", base_dir)drop_duplicates を入れておくと、再取得時の重複を防げます。
差分更新の設計
毎回フルで取らずに「最終取得日 → 今日まで」だけ取得する流れです。
import jsonfrom datetime import date, timedeltafrom pathlib import Path
META_PATH = Path("data/jquants/meta.json")
def load_meta() -> dict: if META_PATH.exists(): return json.loads(META_PATH.read_text(encoding="utf-8")) return {}
def save_meta(meta: dict) -> None: META_PATH.parent.mkdir(parents=True, exist_ok=True) META_PATH.write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8")
def update_prices(api_key: str, code: str, today: date, base_dir: Path) -> None: """code の最終取得日から today までを取って保存する。""" meta = load_meta() last_date_str = meta.get("prices", {}).get(code)
# 最終取得日の翌日から開始(なければ十分に古い日付) if last_date_str: date_from = (date.fromisoformat(last_date_str) + timedelta(days=1)).isoformat() else: date_from = "2020-01-01"
if date_from > today.isoformat(): return # 取得不要
df = fetch_daily_quotes(api_key, code, date_from, today.isoformat()) if df.empty: return save_prices(df, code, base_dir)
meta.setdefault("prices", {})[code] = today.isoformat() save_meta(meta)ポイントは次の 2 点です。
- メタファイル(
meta.json)に銘柄ごとの最終取得日を残す - 取得範囲は 最終取得日の翌日から今日まで に絞る
これだけでも、フル取得との通信量・時間に大きな差が出ます。
全銘柄保存と日付単位ファイル
「指定日の全銘柄」を取りに行くなら、日付ごとにファイルを分けるのが便利です。
data/jquants/prices_by_date/ 2024-01-04.parquet 2024-01-05.parquet ...新しい日が増えるだけで、過去ファイルは触らずに済みます。バックアップや差分同期も楽です。
SQLite で財務情報を保存する
財務情報は「特定の銘柄の特定期だけ取り出す」「業種で絞る」のようなクエリが入りがちです。SQLite に入れておくと、SQL で扱いやすくなります。
import sqlite3import pandas as pdfrom pathlib import Path
def save_statements(statements: pd.DataFrame, db_path: Path) -> None: """財務情報を SQLite に追記する(主キーで重複を弾く)。""" conn = sqlite3.connect(db_path) try: conn.execute( """ CREATE TABLE IF NOT EXISTS statements ( Code TEXT, DiscDate TEXT, DocType TEXT, CurPerType TEXT, Sales REAL, OP REAL, NP REAL, EPS REAL, Eq REAL, TA REAL, PRIMARY KEY (Code, DiscDate, DocType) ) """ ) cols = [ "Code", "DiscDate", "DocType", "CurPerType", "Sales", "OP", "NP", "EPS", "Eq", "TA", ] existing = [c for c in cols if c in statements.columns] records = statements[existing].to_dict(orient="records") placeholders = ", ".join("?" for _ in existing) sql = f"INSERT OR REPLACE INTO statements ({', '.join(existing)}) VALUES ({placeholders})" conn.executemany(sql, [tuple(r.get(c) for c in existing) for r in records]) conn.commit() finally: conn.close()INSERT OR REPLACE を使えば、同じ主キー(銘柄 × 開示日 × 文書種別)で再取得した行を上書きできます。
クエリ例です。
def query_fy(code: str, db_path: Path) -> pd.DataFrame: """通期(FY)決算だけを取り出す。""" conn = sqlite3.connect(db_path) try: return pd.read_sql_query( """ SELECT * FROM statements WHERE Code = :code AND CurPerType = 'FY' ORDER BY DiscDate """, conn, params={"code": code}, ) finally: conn.close()キャッシュの一貫性
ローカルキャッシュを長く使うと、次のような不整合が起きやすくなります。
- 株式分割があり、過去の調整済み終値の値が変わる
- 決算修正の再開示で、過去の
EPSが変わる - 業種コードや市場区分の改定が反映されていない
対策の方向性は次のとおりです。
| 対策 | 説明 |
|---|---|
| 整合性チェック | 直近 N 日分は再取得して一致を確認 |
| 完全再構築モード | コマンドラインオプションで「全消去 → 全取得」を選べるように |
| 更新ログ | 何を、いつ、どこまで取得したかを残す(meta.json の拡張) |
少なくとも、月に 1 回は完全再構築するスケジュールを組んでおくと安全です。
注意点
- ファイル容量が大きくなりやすいため、Parquet の圧縮(snappy / zstd)は有効化しておく
- バックアップを取るかどうかを設計時点で決める(クラウドストレージ・別ディスク等)
- 個人情報・認証情報は キャッシュしない(API キーは環境変数管理のまま。キャッシュ対象はあくまで取得データ)
- 共有マシンで使う場合は、キャッシュの保存場所のアクセス権を確認する
生成AI へのプロンプト例
差分更新ジョブを生成する例です。
J-Quants API の銘柄一覧と日次株価を、以下の方針でローカルに保存・更新するPython スクリプトを書いてください。
要件:- 認証は API キーを環境変数 JQUANTS_API_KEY から読み、x-api-key ヘッダに乗せる- 銘柄一覧は data/jquants/listed/listed_info_<日付>.parquet として保存- 日次株価は銘柄ごと・年ごとに data/jquants/prices/<code>/<code>_<年>.parquet- meta.json に銘柄ごとの最終取得日を残し、差分のみ取得- 取得失敗時は指数バックオフで 3 回までリトライ- pandas 2.2 系の API を使う- 関数は適切に分割し、main() でまとめるまとめ
- 銘柄一覧 / 日次株価は Parquet、財務情報は SQLite が扱いやすい
- 差分更新には「最終取得日」のメタ管理が必須
- 株式分割や決算修正で過去データが変わるため、定期的な再構築を組み込む
- 認証情報はキャッシュ対象に含めず、環境変数で管理する