概要
Phase 2で完成した認証基盤の上に、V2 APIの全18エンドポイントを実装する。
TDD(テスト駆動開発)で進め、V2ネイティブカラム名をそのまま使用する。
親Issue: #8
設計方針
1. レスポンス形式
V2 APIは全エンドポイントで統一されたレスポンス形式:
{
"data": [...],
"pagination_key": "optional_key"
}
V1との違い:
| V1 |
V2 |
d["info"], d["daily_quotes"] 等 |
全て d["data"] |
2. V2ネイティブカラム名
V2のカラム名をそのまま使用(V1形式への変換なし):
| V2 (採用) |
V1 (参考) |
説明 |
O |
Open |
始値 |
H |
High |
高値 |
L |
Low |
安値 |
C |
Close |
終値 |
Vo |
Volume |
出来高 |
Va |
TurnoverValue |
売買代金 |
CoName |
CompanyName |
会社名 |
S17 |
Sector17Code |
17業種コード |
S33 |
Sector33Code |
33業種コード |
3. エラーハンドリング
V2 APIのエラーレスポンス形式:
| Status |
意味 |
処理 |
| 400 |
Bad Request |
JQuantsAPIError を raise |
| 403 |
Forbidden |
JQuantsForbiddenError を raise |
| 429 |
Too Many Requests |
5分10秒待ってリトライ(設定可能) |
| 500 |
Internal Server Error |
urllib3.Retry で自動リトライ |
注: V2 APIは401を返さない(無効キーは403)
4. プラン別実装優先度
開発環境はStandardプランのため、テスト可能なエンドポイントを優先実装。
| プラン |
エンドポイント数 |
結合テスト |
| Standard |
12 |
✅ 実行可能 |
| Premium |
6 |
作成のみ(実行スキップ) |
5. レートリミット対応 ⚠️ 重要
V2 APIは厳格なレートリミットが適用される(V1ではほぼ発動しなかった)。
| プラン |
上限 |
実効レート |
| Free |
5 req/min |
0.08 req/sec (12秒に1回) |
| Light |
60 req/min |
1 req/sec |
| Standard |
120 req/min |
2 req/sec (0.5秒に1回) |
| Premium |
500 req/min |
8.3 req/sec |
V1との違い:
- V1: レートリミットほぼ発動せず、
ThreadPoolExecutor(max_workers=5) で並列取得可能だった
- V2: 大幅超過で5分間遮断 のペナルティあり。従来の並列取得は即BAN
設計原則(安全第一):
-
Leaky Bucket (Pacer) 方式を採用
Token Bucket(バースト許容)は危険。アイドル後の一斉送信で即BAN
- 常に一定間隔を守る「整流化」 が必須(Standardなら0.5秒間隔)
-
429エラーは5分10秒待ってリトライ
- 公式仕様に「大幅超過で5分間遮断」とあるため、遮断解除を待つ
- リトライ待機時間・回数は設定可能(デフォルト: 310秒、最大3回)
retry_on_429=False で即例外送出も可能
-
並列化は設定可能
- デフォルトは直列(
max_workers=1)で安全側に倒す
- 環境に応じてユーザーが調整可能(高レイテンシ環境では並列化が有効)
- Pacer制御は並列時も必須
-
デフォルトは最も安全なプラン
rate_limit=None → Free(5 req/min) または明示的指定を要求
- Freeユーザーが設定忘れで即BANされるリスクを回避
参考: V2 レートリミット仕様
ファイル構成
jquants/
├── client_v2.py # エンドポイント追加
├── constants_v2.py # V2カラム定義
├── exceptions.py # カスタム例外
└── pacer.py # Leaky Bucket レートリミッター(新規)
実装タスク
Sub-Phase 3.1: 基本インフラ ✅
完了: PR #14 でマージ済み(単体148件、結合44件、カバレッジ97%)
Sub-Phase 3.1.5: レートリミッター基盤 ✅
V2の厳格なレートリミットに対応するため、エンドポイント実装前に基盤を整備。
完了: PR #16 でマージ済み(Issue #15)
Phase A: Standard プラン(12エンドポイント)
Sub-Phase 3.2: Equities-Standard (3件) ✅
| エンドポイント |
メソッド名 |
プラン |
/v2/equities/master |
get_listed_info() |
Standard |
/v2/equities/bars/daily |
get_prices_daily_quotes() |
Standard |
/v2/equities/earnings-calendar |
get_fins_announcement() |
Standard |
完了: PR #18 でマージ済み(Issue #17、単体43件追加)
結合テスト: PR #22 でマージ済み
Sub-Phase 3.3: Markets (5件 Standard + 1件 Premium) ✅
| エンドポイント |
メソッド名 |
プラン |
/v2/markets/calendar |
get_markets_trading_calendar() |
Standard |
/v2/markets/margin-interest |
get_markets_weekly_margin_interest() |
Standard |
/v2/markets/short-ratio |
get_markets_short_selling() |
Standard |
/v2/markets/breakdown |
get_markets_breakdown() |
Premium |
/v2/markets/short-sale-report |
get_markets_short_selling_positions() |
Standard |
/v2/markets/margin-alert |
get_markets_daily_margin_interest() |
Standard |
完了: PR #23 でマージ済み(Issue #19)
Sub-Phase 3.4: Indices (2件) ✅
| エンドポイント |
メソッド名 |
プラン |
/v2/indices/bars/daily |
get_indices() |
Standard |
/v2/indices/bars/daily/topix |
get_indices_topix() |
Standard |
完了: PR #27 でマージ済み(Issue #20)
Sub-Phase 3.5: Financials-Standard (1件) ✅ レビュー待ち
| エンドポイント |
メソッド名 |
プラン |
/v2/fins/summary |
get_fins_summary() |
Standard |
完了: PR #26 (Issue #21)
Sub-Phase 3.6: Derivatives-Standard (1件)
| エンドポイント |
メソッド名 |
プラン |
/v2/derivatives/bars/daily/options/225 |
get_options_225_daily(), get_options_225_daily_range() |
Standard |
完了: PR #28 (Issue #25)
Phase B: Premium プラン(6エンドポイント)
注: 結合テストは作成するが、実行は @pytest.mark.premium でスキップ
Sub-Phase 3.7: Equities-Premium (2件)
| エンドポイント |
メソッド名 |
プラン |
/v2/equities/bars/daily/am |
get_prices_prices_am() |
Premium |
/v2/equities/investor-types |
get_markets_trades_spec() |
Premium |
Sub-Phase 3.8: Financials-Premium (2件)
| エンドポイント |
メソッド名 |
プラン |
/v2/fins/details |
get_fins_fs_details() |
Premium |
/v2/fins/dividend |
get_fins_dividend() |
Premium |
Sub-Phase 3.9: Derivatives-Premium (2件)
| エンドポイント |
メソッド名 |
プラン |
/v2/derivatives/bars/daily/futures |
get_derivatives_futures() |
Premium |
/v2/derivatives/bars/daily/options |
get_derivatives_options() |
Premium |
結合テスト設計(実API使用)
概要
| 項目 |
内容 |
| 対象 |
ClientV2 全機能 |
| 前提 |
有効な J-Quants API キー |
| 実行条件 |
JQUANTS_API_KEY 環境変数 or jquants-api.toml |
| マーカー |
@pytest.mark.integration |
| ファイル |
tests/test_integration/ |
ファイル構成
tests/test_integration/
├── conftest.py # フィクスチャ、スキップ条件
├── test_auth.py # AUTH-001〜011
├── test_request.py # REQ-001〜009
├── test_pagination.py # PAGE-001〜006
├── test_error_handling.py # ERR-001〜013
├── test_dataframe.py # DF-001〜008
├── test_retry.py # RETRY-001〜004
├── test_rate_limiter.py # RATE-001〜008
├── test_equities.py # Equities エンドポイント
├── test_markets.py # Markets エンドポイント
├── test_indices.py # Indices エンドポイント
├── test_fins.py # Financials エンドポイント
└── test_*.py # 追加エンドポイント
テストケース一覧
AUTH: 認証・設定読み込み
| ID |
テストケース |
検証観点 |
レア度 |
| AUTH-001 |
有効なAPIキーで初期化成功 |
例外が発生しない |
通常 |
| AUTH-002 |
環境変数からAPIキー読み込み |
JQUANTS_API_KEY 優先 |
通常 |
| AUTH-003 |
TOMLファイルからAPIキー読み込み |
jquants-api.toml パース |
通常 |
| AUTH-004 |
無効なAPIキーで403エラー |
JQuantsForbiddenError 発生 |
通常 |
| AUTH-005 |
空文字APIキーで初期化失敗 |
ValueError 発生 |
通常 |
| AUTH-006 |
前後空白付きAPIキーが正規化される |
.strip() 動作 |
🔸レア |
| AUTH-007 |
改行付きAPIキーが正規化される |
TOML末尾改行対策 |
🔸レア |
| AUTH-008 |
環境変数がTOMLより優先される |
優先順位確認 |
🔸レア |
| AUTH-009 |
明示パス指定時にファイル不在でエラー |
JQUANTS_API_CLIENT_CONFIG_FILE |
🔸レア |
| AUTH-010 |
TOML構文エラーで警告+フォールバック |
暗黙パスの場合 |
🔸レア |
| AUTH-011 |
api_keyが非文字列型で警告+無視 |
api_key = 12345 |
🔸レア |
REQ: 基本リクエスト
| ID |
テストケース |
検証観点 |
レア度 |
| REQ-001 |
_request("GET", path) 成功 |
requests.Response 返却 |
通常 |
| REQ-002 |
_get_raw(path) がJSON文字列を返す |
UTF-8デコード済み文字列 |
通常 |
| REQ-003 |
User-Agent ヘッダが正しい |
jqapi-python/VERSION/v2 |
通常 |
| REQ-004 |
x-api-key ヘッダが設定される |
APIキー含む |
通常 |
| REQ-005 |
タイムアウト設定(30秒) |
応答確認 |
通常 |
| REQ-006 |
セッションが再利用される |
2回目以降同一Session |
🔸レア |
| REQ-007 |
パラメータに日本語含む |
URLエンコード |
🔸レア |
| REQ-008 |
パラメータに特殊文字含む |
&, =, % 等 |
🔸レア |
| REQ-009 |
存在しないパスで404 |
JQuantsAPIError(404) |
🔸レア |
PAGE: ページネーション
| ID |
テストケース |
検証観点 |
レア度 |
| PAGE-001 |
単一ページレスポンス処理 |
data キー抽出 |
通常 |
| PAGE-002 |
複数ページを自動結合 |
全データ取得 |
通常 |
| PAGE-003 |
pagination_key なしで正常終了 |
最終ページ判定 |
通常 |
| PAGE-004 |
空データレスポンス {"data": []} |
[] 返却 |
🔸レア |
| PAGE-005 |
1件のみのレスポンス |
境界値 |
🔸レア |
| PAGE-006 |
大量ページ取得(10+ページ) |
メモリ・時間 |
🔸レア |
ERR: エラーハンドリング
| ID |
テストケース |
検証観点 |
レア度 |
| ERR-001 |
403 → JQuantsForbiddenError |
認証失敗/プラン制限 |
通常 |
| ERR-002 |
403 → JQuantsForbiddenError |
プラン制限時 |
通常 |
| ERR-003 |
404 → JQuantsAPIError |
存在しないパス |
通常 |
| ERR-004 |
400 → JQuantsAPIError |
不正パラメータ |
通常 |
| ERR-005 |
エラーメッセージにAPI応答含む |
response_body 設定 |
通常 |
| ERR-006 |
例外継承が機能する |
except JQuantsAPIError |
通常 |
| ERR-007 |
JSONエラーレスポンスからmessage抽出 |
{"message": "..."} |
通常 |
| ERR-008 |
非JSONエラーレスポンス処理 |
HTML等でもクラッシュしない |
🔸レア |
| ERR-009 |
巨大エラーレスポンスの切り詰め |
2048文字制限 |
🔸レア |
| ERR-010 |
messageフィールドが巨大 |
切り詰め動作 |
🔸レア |
| ERR-011 |
messageフィールドがdict/list |
json.dumps変換 |
🔸レア |
| ERR-012 |
response_bodyが空文字 |
フォールバック動作 |
🔸レア |
| ERR-013 |
複数エラーの連続発生 |
セッション状態維持 |
🔸レア |
DF: DataFrame変換
| ID |
テストケース |
検証観点 |
レア度 |
| DF-001 |
実APIレスポンスからDataFrame生成 |
型が pd.DataFrame |
通常 |
| DF-002 |
カラム順序が定義通り |
constants_v2.py 準拠 |
通常 |
| DF-003 |
日付カラムが pd.Timestamp |
型変換成功 |
通常 |
| DF-004 |
ソートが正しく適用される |
Date昇順等 |
通常 |
| DF-005 |
APIに存在しないカラムは無視 |
Free vs Premium |
🔸レア |
| DF-006 |
空リストで空DataFrame |
columns設定維持 |
🔸レア |
| DF-007 |
日付形式が不正な場合 |
pandas例外素通し |
🔸レア |
| DF-008 |
NaN/null値の処理 |
欠損値保持 |
🔸レア |
RETRY: リトライ/レジリエンス
| ID |
テストケース |
検証観点 |
レア度 |
| RETRY-001 |
セッション再利用確認 |
コネクションプール |
通常 |
| RETRY-002 |
Retry設定値が正しい |
total=3, backoff_factor=0.5 |
通常 |
| RETRY-003 |
ネットワークエラーは素通し |
requests.ConnectionError |
🔸レア |
| RETRY-004 |
不正ホストで接続エラー |
DNS解決失敗 |
🔸レア |
RATE: レートリミット
| ID |
テストケース |
検証観点 |
レア度 |
| RATE-001 |
デフォルトレート設定 |
rate_limit=None時の挙動 |
通常 |
| RATE-002 |
カスタムレート設定 |
rate_limit=60等 |
通常 |
| RATE-003 |
リクエスト間隔が制御される |
Pacerによる整流化 |
通常 |
| RATE-004 |
429で5分10秒待ってリトライ |
デフォルト動作 |
通常 |
| RATE-005 |
retry_on_429=Falseで即例外 |
リトライ無効化 |
通常 |
| RATE-006 |
max_workers=1で直列取得 |
デフォルト動作 |
通常 |
| RATE-007 |
max_workers>1で並列取得 |
Pacer制御付き |
🔸レア |
| RATE-008 |
スレッドセーフ性 |
並列アクセスで競合なし |
🔸レア |
CONC: 並行性/スレッドセーフ
| ID |
テストケース |
検証観点 |
レア度 |
| CONC-001 |
複数リクエストの並行実行 |
ThreadPoolExecutor |
🔸レア |
| CONC-002 |
セッション共有時のスレッドセーフ |
競合なし |
🔸レア |
テスト数サマリー
| カテゴリ |
通常 |
レア |
合計 |
Sub-Phase |
| AUTH |
5 |
6 |
11 |
3.1 ✅ |
| REQ |
5 |
4 |
9 |
3.1 ✅ |
| PAGE |
3 |
3 |
6 |
3.1 ✅ |
| ERR |
7 |
6 |
13 |
3.1 ✅ |
| DF |
4 |
4 |
8 |
3.1 ✅ |
| RETRY |
2 |
2 |
4 |
3.1 ✅ |
| RATE |
6 |
2 |
8 |
3.1.5 ✅ |
| CONC |
0 |
2 |
2 |
3.2+ |
| EP-* |
- |
- |
TBD |
3.2+ |
| 合計 |
32 |
29 |
61+ |
- |
実装優先度
| フェーズ |
カテゴリ |
テスト数 |
理由 |
| 1st |
AUTH (通常) |
5 |
認証失敗は全機能に影響 |
| 1st |
ERR (通常) |
7 |
エラーハンドリング確認必須 |
| 2nd |
REQ, PAGE, DF (通常) |
12 |
基本動作確認 |
| 2.5th |
RATE (通常) |
6 |
V2必須、並列取得の前提 |
| 3rd |
全レアケース |
29 |
回帰テスト用 |
| 4th |
CONC |
2 |
並列取得実装後 |
結合テストマーカー設計
Premium マーカー
# conftest.py
def pytest_configure(config):
config.addinivalue_line("markers", "premium: Premium plan required")
# 各テスト
@pytest.mark.premium
@pytest.mark.skipif(
os.getenv("JQUANTS_PLAN") != "premium",
reason="Premium plan required"
)
def test_get_fins_dividend():
...
実行方法
# 結合テストのみ実行(Standardのみ)
.venv/bin/poetry run pytest -m "integration and not premium"
# 結合テスト(Premium含む、Premium契約者向け)
JQUANTS_PLAN=premium .venv/bin/poetry run pytest -m integration
# 通常テスト(結合テスト除外)
.venv/bin/poetry run pytest -m "not integration"
# 全テスト
.venv/bin/poetry run pytest
完了条件
見積もり
| Sub-Phase |
内容 |
プラン |
単体テスト |
結合テスト |
状態 |
| 3.1 基本インフラ |
- |
- |
148 ✅ |
44 ✅ |
✅ 完了 |
| 3.1.5 レートリミッター |
基盤 |
- |
- |
+8 ✅ |
✅ 完了 |
| 3.2 Equities-Std |
3 EP + 1 ヘルパー |
Standard |
43 ✅ |
✅ |
✅ 完了 |
| 3.3 Markets |
6 EP |
Standard |
✅ |
✅ |
✅ 完了 |
| 3.4 Indices |
2 EP |
Standard |
24 ✅ |
8 ✅ |
✅ 完了 |
| 3.5 Financials-Std |
1 EP + 1 ヘルパー |
Standard |
36 ✅ |
13 ✅ |
✅ 完了 (PR #26) |
| 3.6 Derivatives-Std |
1 EP + 1 ヘルパー |
Standard |
24 ✅ |
5 ✅ |
✅ 完了 (PR #28) |
| 3.7 Equities-Prm |
2 EP |
Premium |
🔲 |
スキップ |
🔲 未着手 |
| 3.8 Financials-Prm |
2 EP |
Premium |
🔲 |
スキップ |
🔲 未着手 |
| 3.9 Derivatives-Prm |
2 EP |
Premium |
🔲 |
スキップ |
🔲 未着手 |
| 合計 |
18 EP |
- |
- |
- |
- |
参考資料
🤖 Generated with Claude Code
概要
Phase 2で完成した認証基盤の上に、V2 APIの全18エンドポイントを実装する。
TDD(テスト駆動開発)で進め、V2ネイティブカラム名をそのまま使用する。
親Issue: #8
設計方針
1. レスポンス形式
V2 APIは全エンドポイントで統一されたレスポンス形式:
{ "data": [...], "pagination_key": "optional_key" }V1との違い:
d["info"],d["daily_quotes"]等d["data"]2. V2ネイティブカラム名
V2のカラム名をそのまま使用(V1形式への変換なし):
OOpenHHighLLowCCloseVoVolumeVaTurnoverValueCoNameCompanyNameS17Sector17CodeS33Sector33Code3. エラーハンドリング
V2 APIのエラーレスポンス形式:
{"message": "エラー詳細"}JQuantsAPIErrorを raiseJQuantsForbiddenErrorを raise4. プラン別実装優先度
開発環境はStandardプランのため、テスト可能なエンドポイントを優先実装。
5. レートリミット対応⚠️ 重要
V2 APIは厳格なレートリミットが適用される(V1ではほぼ発動しなかった)。
V1との違い:
ThreadPoolExecutor(max_workers=5)で並列取得可能だった設計原則(安全第一):
Leaky Bucket (Pacer) 方式を採用
Token Bucket(バースト許容)は危険。アイドル後の一斉送信で即BAN429エラーは5分10秒待ってリトライ
retry_on_429=Falseで即例外送出も可能並列化は設定可能
max_workers=1)で安全側に倒すデフォルトは最も安全なプラン
rate_limit=None→ Free(5 req/min) または明示的指定を要求参考: V2 レートリミット仕様
ファイル構成
実装タスク
Sub-Phase 3.1: 基本インフラ ✅
exceptions.py作成(カスタム例外クラス)constants_v2.py作成(V2カラム定義)_request()拡張(エラーハンドリング強化)Sub-Phase 3.1.5: レートリミッター基盤 ✅
V2の厳格なレートリミットに対応するため、エンドポイント実装前に基盤を整備。
pacer.py作成(Leaky Bucket方式)ClientV2にPacer統合rate_limit,max_workers,retry_*)Phase A: Standard プラン(12エンドポイント)
Sub-Phase 3.2: Equities-Standard (3件) ✅
/v2/equities/masterget_listed_info()/v2/equities/bars/dailyget_prices_daily_quotes()/v2/equities/earnings-calendarget_fins_announcement()get_listed_info()get_prices_daily_quotes()get_fins_announcement()get_price_range()並列取得(レートリミット対応)Sub-Phase 3.3: Markets (5件 Standard + 1件 Premium) ✅
/v2/markets/calendarget_markets_trading_calendar()/v2/markets/margin-interestget_markets_weekly_margin_interest()/v2/markets/short-ratioget_markets_short_selling()/v2/markets/breakdownget_markets_breakdown()/v2/markets/short-sale-reportget_markets_short_selling_positions()/v2/markets/margin-alertget_markets_daily_margin_interest()Sub-Phase 3.4: Indices (2件) ✅
/v2/indices/bars/dailyget_indices()/v2/indices/bars/daily/topixget_indices_topix()Sub-Phase 3.5: Financials-Standard (1件) ✅ レビュー待ち
/v2/fins/summaryget_fins_summary()get_fins_summary()(36件)get_fins_summary()+get_summary_range()_to_dataframe拡張:ensure_all_columnsパラメータ追加Sub-Phase 3.6: Derivatives-Standard (1件)
/v2/derivatives/bars/daily/options/225get_options_225_daily(),get_options_225_daily_range()get_options_225_daily(),get_options_225_daily_range()(24件)Phase B: Premium プラン(6エンドポイント)
Sub-Phase 3.7: Equities-Premium (2件)
/v2/equities/bars/daily/amget_prices_prices_am()/v2/equities/investor-typesget_markets_trades_spec()Sub-Phase 3.8: Financials-Premium (2件)
/v2/fins/detailsget_fins_fs_details()/v2/fins/dividendget_fins_dividend()Sub-Phase 3.9: Derivatives-Premium (2件)
/v2/derivatives/bars/daily/futuresget_derivatives_futures()/v2/derivatives/bars/daily/optionsget_derivatives_options()結合テスト設計(実API使用)
概要
JQUANTS_API_KEY環境変数 orjquants-api.toml@pytest.mark.integrationtests/test_integration/ファイル構成
テストケース一覧
AUTH: 認証・設定読み込み
JQUANTS_API_KEY優先jquants-api.tomlパースJQuantsForbiddenError発生ValueError発生.strip()動作JQUANTS_API_CLIENT_CONFIG_FILEapi_key = 12345REQ: 基本リクエスト
_request("GET", path)成功requests.Response返却_get_raw(path)がJSON文字列を返すjqapi-python/VERSION/v2x-api-keyヘッダが設定される&,=,%等JQuantsAPIError(404)PAGE: ページネーション
dataキー抽出pagination_keyなしで正常終了{"data": []}[]返却ERR: エラーハンドリング
JQuantsForbiddenErrorJQuantsForbiddenErrorJQuantsAPIErrorJQuantsAPIErrorresponse_body設定except JQuantsAPIError{"message": "..."}DF: DataFrame変換
pd.DataFramepd.TimestampRETRY: リトライ/レジリエンス
requests.ConnectionErrorRATE: レートリミット
CONC: 並行性/スレッドセーフ
テスト数サマリー
実装優先度
結合テストマーカー設計
Premium マーカー
実行方法
完了条件
exceptions.py作成完了constants_v2.py作成完了make lintパス見積もり
参考資料
🤖 Generated with Claude Code