J-Quants APIの無料プランで詰まった2つのバグ:日付範囲とasyncpgの型変換
400エラーが空リストとして返ってくる・asyncpgがDATE型を勝手に変換しない。どちらも公式ドキュメントには書いていなかった。
J-Quants APIは日本株の株価データを取得できるAPIで、無料プランでも過去データにアクセスできる。ただし無料プランには制約があり、その制約の挙動が直感に反するところがあった。
バグ1:to_date が範囲外のとき400エラーが空リストとして返ってくる
J-Quants APIの無料プランでアクセスできるデータには範囲制限がある。2026年時点では、取得可能なデータは「直近12週間を除いた過去2年分程度」になっている。この範囲外の日付を to_date に指定すると、APIが400エラーを返す。
問題はその扱いだ。Pythonでリクエストを送ってレスポンスを処理するとき、エラーハンドリングが甘いと400のレスポンスボディをそのまま空リストとして処理してしまう。
# ❌ 問題のあるコード
response = requests.get(url, headers=headers, params=params)
data = response.json().get("daily_quotes", [])
# to_dateが範囲外だと400が返るが、.get()のデフォルト値[]が使われる
これで「データが0件」と認識してしまい、エラーに気づかない。データが取れていないのに処理が正常終了するので原因の特定に時間がかかる。
対処:
# ✅ ステータスコードを先に確認する
response = requests.get(url, headers=headers, params=params)
response.raise_for_status() # 4xx/5xx で例外を投げる
data = response.json().get("daily_quotes", [])
raise_for_status() を入れておけば400が例外として浮上する。無料プランで取得できる日付範囲は公式ドキュメントの「制約」セクションで確認する。
バグ2:asyncpg が DATE 型の文字列を自動変換しない
asyncpg(PostgreSQLの非同期Pythonドライバ)でJ-Quantsから取得した日付データをDBに保存しようとしたとき、型変換エラーが出た。
J-Quants APIが返す日付は文字列形式(例:"2024-03-15")だ。PostgreSQLの DATE 型のカラムに文字列を直接 INSERT しようとすると、asyncpg はエラーを返す。
# ❌ 動かないコード
await conn.execute(
"INSERT INTO stock_prices (date, close) VALUES ($1, $2)",
row["Date"], # "2024-03-15" という文字列
row["Close"],
)
# asyncpg.exceptions.DataError: invalid input for query argument $1
asyncpg は型に厳格で、DATE 型には Python の datetime.date オブジェクトを渡す必要がある。文字列からの自動変換はしない。psycopg2 は暗黙変換してくれるので、そちらから移行してきた場合は特に気づきにくい。
対処:
from datetime import datetime
# ✅ 文字列を date オブジェクトに変換してから渡す
await conn.execute(
"INSERT INTO stock_prices (date, close) VALUES ($1, $2)",
datetime.strptime(row["Date"], "%Y-%m-%d").date(),
row["Close"],
)
まとめて処理するなら変換をパイプラインの手前に入れる方がすっきりする。
def parse_quote(row: dict) -> dict:
return {
"date": datetime.strptime(row["Date"], "%Y-%m-%d").date(),
"close": row["Close"],
# 他のフィールド...
}
quotes = [parse_quote(r) for r in raw_data]
無料プランのデータ範囲について
2026年時点の目安として、無料プランで取得できるのは「2024年3月〜2026年3月の範囲(直近12週除外)」程度だ。この範囲は時間とともに動く。
実装する前に to_date に今日の日付を入れて実際にリクエストを送り、400が返るかどうかを確認しておくと後の混乱を防げる。
まとめ
raise_for_status()を必ず入れる。無言で空リストになるバグは原因特定が難しい- asyncpg に日付を渡すときは
datetime.dateオブジェクトに変換する - 無料プランの取得可能範囲は事前に確認する
※ 本記事にはアフィリエイトリンクが含まれます。