Skip to content

Phase 3 詳細設計: ClientV2 エンドポイント実装 #10

Description

@apokamo

概要

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のエラーレスポンス形式:

{"message": "エラー詳細"}
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

設計原則(安全第一):

  1. Leaky Bucket (Pacer) 方式を採用

    • Token Bucket(バースト許容)は危険。アイドル後の一斉送信で即BAN
    • 常に一定間隔を守る「整流化」 が必須(Standardなら0.5秒間隔)
  2. 429エラーは5分10秒待ってリトライ

    • 公式仕様に「大幅超過で5分間遮断」とあるため、遮断解除を待つ
    • リトライ待機時間・回数は設定可能(デフォルト: 310秒、最大3回)
    • retry_on_429=False で即例外送出も可能
  3. 並列化は設定可能

    • デフォルトは直列(max_workers=1)で安全側に倒す
    • 環境に応じてユーザーが調整可能(高レイテンシ環境では並列化が有効)
    • Pacer制御は並列時も必須
  4. デフォルトは最も安全なプラン

    • 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: 基本インフラ ✅

  • exceptions.py 作成(カスタム例外クラス)
  • constants_v2.py 作成(V2カラム定義)
  • _request() 拡張(エラーハンドリング強化)
  • 共通ヘルパーメソッド追加
  • 単体テスト作成
  • 結合テスト作成(実API疎通確認)

完了: PR #14 でマージ済み(単体148件、結合44件、カバレッジ97%)

Sub-Phase 3.1.5: レートリミッター基盤 ✅

V2の厳格なレートリミットに対応するため、エンドポイント実装前に基盤を整備。

  • pacer.py 作成(Leaky Bucket方式)
  • ClientV2 にPacer統合
  • 429エラー時の5分10秒リトライ実装
  • パラメータ設定(rate_limit, max_workers, retry_*
  • 単体テスト作成
  • 結合テスト作成

完了: 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
  • 単体テスト: get_listed_info()
  • 単体テスト: get_prices_daily_quotes()
  • 単体テスト: get_fins_announcement()
  • 実装: 上記3メソッド
  • 実装: get_price_range() 並列取得(レートリミット対応)
  • 結合テスト: 上記メソッド群

完了: 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
  • 単体テスト: 上記6メソッド
  • 実装: 上記6メソッド
  • 結合テスト: 上記6メソッド(Premium含む、スキップ機構付き)

完了: 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
  • 単体テスト: 上記2メソッド(24件)
  • 実装: 上記2メソッド
  • 結合テスト: 上記2メソッド(8件)

完了: PR #27 でマージ済み(Issue #20

Sub-Phase 3.5: Financials-Standard (1件) ✅ レビュー待ち

エンドポイント メソッド名 プラン
/v2/fins/summary get_fins_summary() Standard
  • 単体テスト: get_fins_summary()(36件)
  • 実装: get_fins_summary() + get_summary_range()
  • 結合テスト: 上記メソッド群(13件)
  • _to_dataframe 拡張: ensure_all_columns パラメータ追加

完了: 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
  • 単体テスト: get_options_225_daily(), get_options_225_daily_range() (24件)
  • 実装: 上記2メソッド
  • 結合テスト: 上記メソッド群 (5件)

完了: 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
  • 単体テスト: 上記2メソッド
  • 実装: 上記2メソッド
  • 結合テスト: 上記2メソッド(実行スキップ)

Sub-Phase 3.8: Financials-Premium (2件)

エンドポイント メソッド名 プラン
/v2/fins/details get_fins_fs_details() Premium
/v2/fins/dividend get_fins_dividend() Premium
  • 単体テスト: 上記2メソッド
  • 実装: 上記2メソッド
  • 結合テスト: 上記2メソッド(実行スキップ)

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
  • 単体テスト: 上記2メソッド
  • 実装: 上記2メソッド
  • 結合テスト: 上記2メソッド(実行スキップ)

結合テスト設計(実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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions