競馬AIを過去データではなくこれから先のレースで検証するには、「締切前に本当に手に入ったオッズ」を取得時刻と一緒に集め続ける必要があります。確定後のオッズで検証すると、実際より良い結果に見えてしまうためです。

この記事では、シリーズ第5回として、締切前オッズを時点付きで定期収集し、紙上シミュレーションに使う仕組みを、VPS・Raspberry Pi・Macの3台に役割分担して作った方法をまとめます。収集対象は公開Webページで、購入処理は一切含みません。

この記事の結論

  • VPS=定期収集、Raspberry Pi=紙上シミュレーション、Mac=長期保管と学習に役割を分ける。
  • オッズの行には必ず odds_timestamp・deadline_at・snapshot_minutes_before を保存する。
  • 判断時刻のスナップショットは「一番近いもの」ではなく、目標時刻以前で一番新しいものを選ぶ(未来のデータを見ないため)。
  • 常駐プロセスをやめ、systemd timer(5分間隔)+ oneshotサービス + flock に変更した。
  • VPSからPiへの受け渡しは、強制コマンド付きSSH鍵で決まったデータしか渡せないようにした。
  • テストは成功したが、JRA公式ページへのアクセスで HTTP 403 が返ったため、本番timerは有効化していない。

構成:VPS・Raspberry Pi・Macの役割分担

マシンやることやらないこと
VPS定期収集。取得したHTMLをそのまま保存し、整えたデータも作る推論
Raspberry Pi整えたデータを受け取り、紙上シミュレーションと仮想台帳の精算生データや履歴の保管
Mac生データの長期保管、開発と学習定期収集
  • VPS:常時起動でネットワークが安定しているので、決まった時刻の収集に向く。ただし計算能力は高くない。
  • Mac:ノートPCなので閉じている間は動かない。保管と学習に使う。
  • Raspberry Pi:小型・低消費電力なので推論だけを任せる。生データを置かないことで、SDカード故障時の被害を小さくする。

オッズの時点を記録するデータ設計

整えたオッズの行には次の列を必ず入れます。

列内容
odds_timestamp取得した時刻
deadline_at締切の時刻
snapshot_minutes_before締切の何分前に取ったか
取得元取得元のページ

出走表・単勝複勝オッズ・払戻は別ファイルにし、払戻は精算専用として推論側から見えないようにしています。

収集スケジュールと失敗時の扱い

  • 各レースの締切60分前から3分前まで、5分おきに取得
  • 通信失敗時は60秒後と120秒後の2回だけ再試行
  • それでも単勝と複勝がそろわなければ、そのスナップショットは見送る(古い値で埋めない)

注意点:「一番近いデータ」を選ぶと未来を見てしまう

Piの紙上シミュレーションは締切30分前(T-30)と10分前(T-10)を判断時刻にしています。「目標時刻に一番近いスナップショット」を選ぶと、T-10の判断に締切8分前のデータが選ばれることがあります。本番なら10分前の時点ではまだ存在しないデータです。

そこで、目標時刻以前で、許容範囲内に収まる一番新しいものを選ぶようにしました。

# 考え方を示す例
def pick_snapshot(snapshots, target_time, tolerance):
    candidates = [
        s for s in snapshots
        if s.odds_timestamp <= target_time                  # 目標時刻より後は選ばない
        and target_time - s.odds_timestamp <= tolerance     # 古すぎるものも選ばない
        and s.is_complete                                   # 単勝・複勝がそろっている
    ]
    return max(candidates, key=lambda s: s.odds_timestamp, default=None)

取得の遅れ、締切の接近、モデルや特徴量の欠けで判断できなかったときは、見送り理由としてCSVに残します。

常駐プロセスからsystemd timer+flockへ変更

症状:以前の収集プロセスは常駐型で、前日から残り続けているのに気づきました。常駐型は止まったときに気づきにくく、メモリの状態も持ち越します。

対処:5分ごとにtimerがサービスを起動し、サービスは1回分の収集をして終了する形に変えました。前回が終わっていないときに重ねて動かないよう flock -n で排他します。Pi側は1分ごとのtimerで新着データを確認します。

# 構成の例
[Service]
Type=oneshot
ExecStart=/usr/bin/flock -n <state-dir>/collector.lock <project-dir>/venv/bin/python -m collector

[Timer]
OnCalendar=*:0/5

古いプロセスは停止しましたが、それまでに保存されたファイルは削除していません。

SSHの強制コマンドでデータ受け渡しを制限する

VPSとPiのやりとりにはSSHを使いますが、何でもできる鍵にはせず、Pi側の専用鍵に強制コマンド(command=)を設定しました。この鍵でできるのは「日付とカーソルを指定して、整えたイベントを受け取る」ことだけです。

# authorized_keys の例
command="<feed-script>",no-pty,no-port-forwarding <public-key>
  • Piの仮想台帳は、結果イベントが届いたときだけ精算する。
  • VPSの生データは、SHA-256で検証してからMacへ送る既存の仕組みに乗せ、転送を確認できるまで削除しない。

検証結果と現在の状態

確認できたこと

保存済みHTMLを使ったテストで、次を確認しました。関連するユニットテスト11件はすべて成功しています。

  • レースID・出走表・単勝複勝オッズ・複数の複勝払戻の解析
  • 受け渡しカーソルの順序
  • オッズが欠けたときに拒否すること
  • 目標時刻より後のデータを選ばないこと

未完了:JRA公式ページでHTTP 403

このVPSからJRA公式の当日ページに1回だけアクセスしたところ、HTTP 403が返りました。制限を回避する実装はせず、本番のtimerは有効にしていません。利用条件と許可されたアクセス方法を確認してから進める方針です。

あわせて直したこと

古いスクリプトに直接書かれていた認証情報を、環境変数から読む形に修正しました。一度でも使われた可能性のある認証情報は、サービス側で変更・失効させる必要があります。

まとめ

  • データ収集で一番大事なのは、量より「いつ取ったか」を失わないこと。
  • 判断時刻のデータは「目標時刻以前で最新」を選び、未来のデータを混ぜない。
  • 定期処理は常駐より、systemd timer+oneshot+flockのほうが追跡しやすい。
  • マシン間の受け渡しは強制コマンド付きSSH鍵で最小権限にする。
  • アクセスが拒否されたら回避せず、利用条件を確認する。

これはデータ収集と固定額の紙上シミュレーションの実装記録です。利益や的中を保証するものではなく、馬券の購入を勧めるものでもありません。