J-Quants の API は便利ですが、毎回フルで叩くとレート制限に当たり、時間もかかります。本記事では、取得データを Parquet と SQLite でローカル保存し、必要な分だけ差分取得する設計の考え方を整理します。

目次

  1. キャッシュの全体像
  2. ディレクトリ構成の例
  3. Parquet で日次株価を保存する
  4. 差分更新の設計
  5. 全銘柄保存と日付単位ファイル
  6. SQLite で財務情報を保存する
  7. キャッシュの一貫性

キャッシュの全体像

設計の方向性は、データの性質によって変えます。

データ更新頻度推奨フォーマット
銘柄一覧(/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 との相性が良いフォーマットです。pyarrowfastparquet のどちらかが入っていれば使えます。

from pathlib import Path
import 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)
関連ページ: #6-5「日次株価四本値を取得する」

drop_duplicates を入れておくと、再取得時の重複を防げます。

差分更新の設計

毎回フルで取らずに「最終取得日 → 今日まで」だけ取得する流れです。

import json
from datetime import date, timedelta
from 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)
関連ページ: #6-5「日次株価四本値を取得する」

ポイントは次の 2 点です。

  • メタファイル(meta.json)に銘柄ごとの最終取得日を残す
  • 取得範囲は 最終取得日の翌日から今日まで に絞る

これだけでも、フル取得との通信量・時間に大きな差が出ます。

全銘柄保存と日付単位ファイル

「指定日の全銘柄」を取りに行くなら、日付ごとにファイルを分けるのが便利です。

data/jquants/prices_by_date/
2024-01-04.parquet
2024-01-05.parquet
...

新しい日が増えるだけで、過去ファイルは触らずに済みます。バックアップや差分同期も楽です。

SQLite で財務情報を保存する

財務情報は「特定の銘柄の特定期だけ取り出す」「業種で絞る」のようなクエリが入りがちです。SQLite に入れておくと、SQL で扱いやすくなります。

import sqlite3
import pandas as pd
from 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 が扱いやすい
  • 差分更新には「最終取得日」のメタ管理が必須
  • 株式分割や決算修正で過去データが変わるため、定期的な再構築を組み込む
  • 認証情報はキャッシュ対象に含めず、環境変数で管理する