J-Quants APIの無料プランで詰まった2つのバグ:日付範囲とasyncpgの型変換
範囲外の日付で400が空リストになる・asyncpgがDATE型の文字列を変換しない。どちらも公式ドキュメントには明記がなかった。
J-Quants API(日本株の株価データAPI)の無料プランでデータ取得を実装したときに踏んだ2つのバグ。どちらも挙動が直感に反する。
バグ1:範囲外の日付で400が「空リスト」として処理される
事象
無料プランで取得できるデータには範囲制限がある(2026年時点で「直近12週を除いた過去2年分程度」)。この範囲外を to_date に指定するとAPIは400を返すが、エラーに気づかずデータ0件として処理が正常終了してしまう。
# ❌ 問題のあるコード
response = requests.get(url, headers=headers, params=params)
data = response.json().get("daily_quotes", [])
# 400が返っても .get() のデフォルト値 [] が使われ、0件扱いになる
原因
レスポンスのステータスコードを確認せず、ボディを直接 .get("daily_quotes", []) で読んでいるため。400のとき daily_quotes キーが無く、デフォルトの空リストにフォールバックする。データが取れていないのに例外も立たないので、原因特定に時間がかかる。
解決策
ステータスコードを先に検証する。
# ✅ 4xx/5xx で例外を投げる
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
data = response.json().get("daily_quotes", [])
raise_for_status() を入れれば400が例外として浮上する。取得可能な日付範囲は実装前に確認しておく。
バグ2:asyncpg が DATE 型の文字列を自動変換しない
事象
J-Quantsが返す日付("2024-03-15" のような文字列)を、asyncpg 経由でPostgreSQLの DATE 型カラムに保存しようとすると失敗する。
# ❌ 動かないコード
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 は暗黙変換するため、そこから移行してきた場合に気づきにくい。
解決策
渡す前に datetime.date へ変換する。
from datetime import datetime
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が返るか確認しておくと混乱を防げる。