① パイプライン全体像
✅ Pipeline v2 稼働中 — 展示データ完全パイプライン + oriten バックフィル
exhibition_scheduler.py が発走-10分に展示3ファイルを自動取得 → vote 時に再フェッチ + LightGBM 再推論。
23:15 backfill_sweep.py が行欠損 + oriten欠損(turn_time全NULL)の2段階差分でDB補完。
SKIP_VENUE_IDS: {39}(江戸川のみ) — oriten 配信なし(403 Forbidden)。他23会場は全て配信確認済み。
実精度は evening_race_results.py の 7日MA を正とする(バックテスト CSV は参考値)。
pnl_daily パイプライン
-
1a
07:30 —
scrape_today.py— 当日番組表 + 初期オッズ取得 -
2
23:30(Kファイル ETL 完了後)—
backtest_live.py --margin 0.35 -
3
generate_pnl_report.py→track_record_all.csv -
4
HTML 生成 → commit/push →
teinou.pages.dev/pnl.html
リアルタイム通知パイプライン
-
1
09:30 —
morning_race_summary.py -
2
07:45 —
exhibition_scheduler.py -
3
発走 -5分(1回のみ)— vote シングルショット
-
4
発走 +20分 —
result_notifier.py(新設) -
5
23:30 — 今日の結果レポート
② レース当日タイムライン
morning_race_summary.py✅ 実装済み(2026-05-22)
exhibition_scheduler.py 起動(Pipeline v3)getHoldingList2_{date}.json で全会場発走時刻取得 → 各レースに 3 ジョブ登録(-10分 / -5分 / +20分)✅ Pipeline v3 実装済み(2026-05-24)launchd 07:45 起動 / APScheduler デーモン / 最終発走 +30分で自動終了
fetch_boatcast_prerace(継続)bc_j_tkz(展示タイム・チルト)/ bc_j_stt(コース・展示ST)/ bc_oriten(まわり足・直線)→ exhibition_runs MERGE✅ 全会場・全レース対象(getHoldingList2 掲載分)/ 取得失敗は 23:15 バックフィルで補完(継続)
exhibition_runs 再フェッチ → LightGBM 再推論(展示特徴量あり)→ Telegram 正式通知(1R/1回 dedup)→ 通知成功後:
logs/bet_signal_YYYYMMDD.csv に買い目詳細を自動記録記録フィールド: 会場 / R / top1〜4 / bet_unit / add_win / win_unit / invest / margin12 / win_odds / notified_at
✅ bet_signal 実装済み(2026-05-24)
result_notifier.py(新設)①
bet_signal CSV から買い目読み取り(対象外レースは自動スキップ)② boatrace.jp
raceresult ページから 1〜3着着順・払戻をスクレイプ(Primary — 発走+5〜10分で公開)③ Fallback: Oracle
payout_records(Kファイル ETL 23:10 以降)④ bet_signal と照合 → 三連単A/B/C/D的中判定 + 単勝的中判定 → P&L計算 → Telegram 即時通知
⑤
logs/bet_result_YYYYMMDD.csv に結果追記⚠️ Kファイル ETL(23:10)・backfill_sweep(23:15)・展示フェッチは継続。本スクリプトは発走+20分専用
✅ 実装済み(2026-05-24)
payout_records / races 完成全レース終了後。以降のジョブの前提条件
backfill_sweep.py① 行欠損:
getHoldingList2(当日全レース)vs exhibition_runs(取得済み)の差分② oriten欠損: 行はあるが
turn_time 全NULL(tkz/sttは取れたがoritenだけタイミングミスマッチ)→ 欠損レースを boatcast.jp から再フェッチ → UPSERT(NVL で既存値を上書きしない)
✅ 実装済み — 5/21: 122レース補完(行29 + oriten93)OK=122 NG=0
江戸川(39)は oriten 配信なし → _ORITEN_UNAVAILABLE_VENUES で除外
evening_race_results.py + pnl_daily①
evening_race_results.py — pending_vote_signals × 実結果 → 予測精度集計 → Telegram 配信(7日MA Top-1 的中率)②
pnl_daily — backtest → track_record_all.csv 追記 → teinou.pages.dev/pnl.html 更新 → push← ① が実際の予測精度の正。② は参考値(backtest)
③ モデル・ベット戦略・精度の読み方
単勝1点 + 三連単(top1-top2-top3 / top1-top2-top4 / top1-top3-top4)の3点流し
①
margin12 ≥ 0.35 — top1とtop2の予測確率差(自信度フィルタ)②
min-proba ≥ 0.50 — top1の生確率が50%以上③
value_edge ≥ 0.10 — EV-implied_prob差(期待値フィルタ)
※ Calibrated 確率ベース(raw確率の0.10下方シフト版。5/22〜)
v2026_34no の設計(現行モデル・2026-05-24〜)
オッズ除外設計: win_odds / implied_prob をモデル特徴量から除外(v2026_27以降継続)。
展示特徴量込み: --with-exhibition フラグ有効 — exhibition_runs から展示タイム・コース・STを特徴量に組み込み。
LightGBM: lr=0.01 × n=12,000(best_iter=12,000で上限到達・未収束 → v2026_35no で n=20,000 に延長中)。
SKIP_VENUE_IDS: {39}(江戸川のみ) — oriten 配信なし(403 Forbidden)。
次世代: v2026_35no 学習中(lr=0.01 × n=20,000 — v2026_34no の継続延長)
⚠️ バックテスト ROI と本番の乖離について(重要)
バックテスト件数(193件/5日)≠ 実通知件数(62件/5日):約3倍の差がある。
理由①「特徴量の計算方法が違う」:バックテストはDB全データから一括でfeature再計算するため、ローリング特徴量が微妙にズレる。本番はrolling_cache(当日リアルタイム)を使用。同じmargin閾値でも選ばれるレースが変わる。
理由②「完全なデータしか処理できない」:バックテストはデータが不完全なレースを自動除外 → 自然と「きれいなレース」だけになりROIが高く見える。
→ バックテストROIは「モデルの理論的な実力の目安」。本番ROIは3〜4割引で見るのが現実的。
本当の成績はシャドーラン(5/21〜)100bet蓄積後に確定する。
④ バグ一覧と対応状況
| 優先度 | バグ | 影響 | ステータス | 対処 |
|---|---|---|---|---|
| P0 | race_odds 0.0 格納 → モデル誤判定 | バックテスト的中率が実態と大幅乖離。精度低下の主因の可能性 | ✅ 修正済 | parse_odds.py / feature_engineering.py 修正。再学習で本来精度に回復見込み |
| P0 | vote stage Telegram 重複送信(1R に 2-4通) | 有料ユーザーに同一通知が複数届く→UX 最悪 | ✅ 修正済 | today_tg_voted_{date}.json で 1R/1回に制限 |
| P1 | predict stage "dead" 通知(vote stage を通過しない予告) | 「通知来たのに何も賭けない?」と混乱 | ✅ 修正済 | 「〔予告・30分前〕オッズ確認後に正式通知」ラベル付与 |
| P1 | live 推論で展示特徴量がゼロ(boatcast.jp は発走10-12分前に公開) | predict stage(20-40分前)はbeforeinfo未公開で空取得。vote stage も再フェッチなし → 展示タイム・コース取り・展示STが全て欠損のまま推論 | ✅ 修正済 | ① exhibition_scheduler.py — -10分に3ファイル並列取得 → exhibition_runs MERGE(launchd 07:45起動済み)② race_notifier.py vote stage — exhibition_runs 再フェッチ + ML再推論実装済み(+2.88pp 実測 2026-05-21)詳細 → ⑦ |
| P1 | backtest CSV が実態と乖離(race_odds の残存 0.0) | website の損益レポートが信頼できない | ⏳ 残タスク | pnl_daily_launchd.sh で backtest 前に odds 再取得(fetch_live_odds.py --date YESTERDAY の実装が必要) |
| P2 | 精度低下(7日MA 51.9% vs 78% baseline) | 三連単的中率が著しく低い | ✅ 解決済 | v2026_27_lr02(オッズ除外設計)で根本解決。AUC=0.9073 / Top-1=70.0% |
| P2 | oriten 欠損(行はあるが turn_time 全NULL) | 23会場中一部で oriten 配信タイミングミスマッチ → 展示特徴量欠損 | ✅ 解決済 | backfill_sweep.py が行欠損 + oriten欠損の2段階差分で23:15に一括補完。江戸川(39)のみ配信なし→SKIP |
| P0 | 予測確率 99%超の出力(統計的にあり得ない値) | EV計算が歪む / 信頼度ラベルS帯が偽陽性で乱発 / ユーザー信頼失墜 | ✅ 修正済 | MAX_WIN_PROB = 0.80 で clip(4ファイル適用: predict.py / predict_daily.py / race_notifier.py / backtest_live.py)。競艇の実勝率上限は〜72%のため80%は十分な余裕値。 |
| P1 | EV計算の歪み(モデルの生確率が実際の勝率より過大) | 期待値フィルタ(value_edge)が機能しない / 偽陽性推薦が増える | ✅ 修正済 | Isotonic Regression で calibration 導入。役割分離アーキテクチャ: margin12 = raw clip値で計算 / top_proba のみ calibration 適用(全艇適用だと margin12 が崩壊するため)。訓練データ: 2026-01-01〜05-18 / 10,683件。 |
⑤ 精度監視 + サーキットブレーカー
margin12=0.35(5/22変更: BT 5/2-5/20 p≥0.50/m≥0.35 → 85.5%的中/234%ROI/日45件)
江戸川: BoatCast oriten 配信なし(403 Forbidden)→ 24特徴量欠損で予測精度劣化
戸田(9)・多摩川(55): v2026_27_lr02切替によりリセット済み(1-2週間ライブデータ蓄積後に再評価)
.betting_paused フラグ作成 → 全通知停止回復: 7日MA ≥ 65% で自動解除 / 手動解除は
rm logs/.betting_paused
retrain_monthly_launchd.sh → train_monthly.py自動バージョン採番(v{year}_{NN}_lr02)/ Telegram にメトリクス通知 / モデル自動切替なし(人間がレビューして手動切替)
次回: 6/1 → v2026_28_lr02 生成予定
monthly_filter_optimize.sh前月全レースでバックテスト → threshold_scan → prob×margin グリッドスキャン → Telegram レポート通知
パラメータ変更は人間判断(レポートのみ自動)
✅ 実装済み(2026-05-22)
⑥ 今後のロードマップ
完了済み(Phase 1-2)
完了済み(Phase 3: 5/21-22)
morning_race_summary.py 朝注目レース一括通知(ランク別S/A/B)進行中
長期(月次)
⑦ exhibition_scheduler — 展示データ収集アーキテクチャ(✅ 実装済み 2026-05-21)
アーキテクチャ方針
朝の番組表(getHoldingList2_{date}.json)を元に各レースを動的スケジューリング。
直前データ取得ソースを race.boatcast.jp に一元化。exhibition_scheduler が -10分・-5分の 2 ジョブを管理し、run_vote_stage() を -5分に直接呼び出す。
📡 race.boatcast.jp — 利用可能ファイル一覧(調査済み 2026-05-21)
| パスパターン | コンテンツ | フォーマット(主要カラム) | HTTP | 利用タイミング |
|---|---|---|---|---|
| /api_txt/getHoldingList2_{YYYYMMDD}.json | 開催場一覧・発走時刻 | JSON: RaceStudiumNo, DeadlineTimeAll[] | 200 | 07:45 スケジュール構築 |
| /hp_txt/{jcd}/bc_j_tkz_{date}_{jcd}_{rno}.txt | 直前情報(展示タイム・体重・チルト) | TSV: name / exhibition_time / weight_adj_flag / weight_adj_g / weight / tilt_flag / tilt_amount | 200 | 発走 -10分 |
| /hp_txt/{jcd}/bc_j_stt_{date}_{jcd}_{rno}.txt | スタート展示(コース取り・展示ST) | TSV: position / lane / name / st_practice / st_actual / f_flag / distance | 200 | 発走 -10分 |
| /txt/{jcd}/bc_oriten_{date}_{jcd}_{rno}.txt | オリジナル展示(まわり足・直線・ラップ) | TSV: lane / name / lap_time / turn_time / straight_time | 200 | 発走 -10分(既存 fetch_oriten_to_adw.py) |
| /txt/{jcd}/bc_smt_od1_{date}_{jcd}_{rno}.txt | 単勝オッズ(リアルタイム) | TSV: 6枠分オッズグリッド + 名前行 | 200 | 30分毎 odds_load の代替候補 |
| /txt/{jcd}/bc_smt_od3_{date}_{jcd}_{rno}.txt | 3連単オッズ(リアルタイム) | TSV: 6行×25列(1着×2着3着組合せ) | 200 | 30分毎 odds_load の代替候補 |
| /txt/{jcd}/bc_smt_od2_{date}_{jcd}_{rno}.txt | 2連単オッズ(リアルタイム) | TSV: 6×6行列 | 200 | 将来活用候補 |
| /txt/{jcd}/bc_kakutei_od3_{date}_{jcd}_{rno}.txt | 3連単確定オッズ | レース確定後に200 | 403→200 | レース後結果検証 |
| /m_txt/{jcd}/bc_rs1_2_{date}_{jcd}_{rno}.txt | 競走成績(着順・ST・決まり手) | TSV: ST行 + 着順・選手名・タイム行 + 気象行 | 200 | レース後 結果検証 |
| /hp_txt/{jcd}/bc_j_str3_{date}_{jcd}_{rno}.txt | 出走表(選手情報・成績統計) | TSV: 登録番号 / 名前 / 期別 / 支部 / …統計列 | 200 | 07:30 scrape_today(出走表) |
⚠️ boatcast.jp 制約メモ
- 展示データ(bc_j_tkz / bc_j_stt / bc_oriten)は発走 10〜12分前 に公開。それ以前は
data=\n{rno}\nだけ(空) - 確定オッズ(bc_kakutei_*)はレース確定前 403、確定後 200 に切替わる
- bc_rs1_2(競走成績)は 完了済みレース のみ 200。未完了は 403
- User-Agent 必須:
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36
スケジュール構築
-
1
07:30 —
scrape_today.py -
2
07:45 —
exhibition_scheduler.py起動
展示スクレイプ + vote トリガー(動的)
-
A
発走 -10分 —
fetch_boatcast_prerace.py -
B2
発走 -5分 — vote シングルショット ✅ 実装済み(2026-05-21)
欠損リトライ・バックフィル
-
C
23:15 —
backfill_sweep.py✅ 実装済み
scripts/exhibition_scheduler.py — APScheduler デーモン(launchd で 07:45 起動)etl/fetch_boatcast_prerace.py — boatcast.jp 直前3ファイル並列取得 (bc_j_tkz + bc_j_stt + bc_oriten) → exhibition_runs UPSERTscripts/backfill_sweep.py — 23:15 展示データ2段階バックフィル(行欠損 + oriten欠損)
scripts/race_notifier.py — vote stage に展示再フェッチ + ML再推論 追加 ✅ commit 93ab4a3scripts/scrape_today.py — 展示スクレイプは exhibition_scheduler が一元管理
apscheduler==3.11.2(インストール済み)変更履歴
| バージョン | 日付 | 変更内容 |
|---|---|---|
| v8 | 2026-05-22 | Calibration 導入 + 確率上限設定 + バックテスト乖離の明文化 ① MAX_WIN_PROB=0.80 clip 全4ファイル適用(predict.py / predict_daily.py / race_notifier.py / backtest_live.py)— 統計的上限 99%問題を根絶 ② Isotonic Regression Calibration: models/lgbm_v2026_27_lr02_calibration.pkl(10,683件・2026-01-01〜05-18訓練) ③ 役割分離アーキテクチャ: margin12=raw clip値 / top_proba=calibrated(全艇適用するとmargin12が崩壊するため分離) ④ 信頼度ラベル閾値 0.10 下方シフト(S≥0.75/A≥0.65/B≥0.55) ⑤ バックテスト≠本番の乖離をドキュメント化(3倍差の構造的理由) |
| v7 | 2026-05-22 | ベット戦略・フィルタ条件の明文化 — yoko戦略(単勝+三連単1着軸流し3点)/ margin12≥0.35 / min-proba≥0.50 / value_edge≥0.10 をセクション③に集約。margin≥0.35変更による推薦件数変化を分析(5/17-20: BT 193件 vs 実通知62件) |
| v6 | 2026-05-22 | モデル切替 + oriten完全カバレッジ — v2026_27_lr02(AUC=0.9073 / Top-1=70.0% / ROI=200.4%・オッズ除外設計)。SKIP_VENUE_IDS: {9,39,55}→{39}(江戸川のみ)。backfill_sweep: 行欠損+oriten欠損の2段階差分補完実装。サーキットブレーカー(.betting_paused / WARN65% / ALERT58%)+月次リトレイン自動化(launchd毎月1日02:00)。シャドーモード〜5/22 → 5/23ライブ化 |
| v5 | 2026-05-21 | パイプライン簡素化 — predict stage・30分毎 odds_load・vote ポーリング launchd を廃止。07:30 を注目レース初期推論通知に変更。exhibition_scheduler に -5分 vote シングルショットジョブ追加。+20分ジョブ廃止 + 23:15 バックフィルスイープ設計 |
| v4 | 2026-05-21 | P1完全解決 — exhibition_scheduler.py 発走 -10分スクレイプ実装 + vote stage 展示再フェッチ・LightGBM 再推論実装(児島R9: +2.88pp 実測)。commit 93ab4a3 |
| v3 | 2026-05-21 | race_odds 0.0→NaN 修正(P0)/ Telegram 重複送信修正(P0)/ モデル v2026_25_lr02(AUC=0.9139)ライブ化 |
| v1-v2 | 2026-05-19〜20 | 初版 / predict stage + vote stage ポーリング構成 / 展示特徴量ゼロ(P1)が未解決の状態 |