# iPAT自動購入 システム設計書

## 概要

iPAT自動購入は、AI予想に基づいてJRAのIPATサイトで馬券を自動購入する機能。  
Laravel（PHP）がオーケストレーション、Python（Selenium）がブラウザ操作を担当する。

## アーキテクチャ

```
┌─────────────┐     ┌────────────────────────┐     ┌────────────────────────┐     ┌──────────────┐
│  設定画面     │     │  HorseRacingService    │     │  IpatPurchaseService   │     │  Python      │
│  (Blade+JS)  │────▶│  (購入判定・予算管理)    │────▶│  (HTTP API呼出)        │────▶│  Selenium    │
│              │     │                        │     │                        │     │  (IPAT操作)  │
└─────────────┘     └────────────────────────┘     └────────────────────────┘     └──────────────┘
                              │                              │                            │
                              ▼                              ▼                            ▼
                    ┌────────────────────┐       ┌─────────────────────┐      ┌──────────────────┐
                    │  DB                │       │  IPATサーバー         │      │  JRA IPAT        │
                    │  - settings        │       │  (ipat_server.py /   │      │  (Webサイト)      │
                    │  - bets            │       │   ipat_local_server) │      └──────────────────┘
                    │  - predictions     │       └─────────────────────┘
                    └────────────────────┘
```

**投票照会の通信フロー（syncBetStatuses）:**

```
Laravel (IpatPurchaseService)
  → HTTP POST /check-votes → ipat_server.py / ipat_local_server.py
    → subprocess: ipat_check_votes.py（Selenium操作）
      → JRA IPAT 投票照会画面
    ← JSON結果を返却
  ← HTTPレスポンスとして受信、DBと照合
```

## Pythonスクリプト構成

### スクリプト一覧

| スクリプト | 役割 | 呼出元 |
|-----------|------|--------|
| `ipat_purchase.py` | 低レベル：SeleniumでIPATを操作し馬券を購入 | Docker環境のLaravel / auto_purchase.py |
| `auto_purchase.py` | 高レベル：DB連携・予算管理・検証を含むオーケストレーション | ローカル環境のタスクスケジューラ / ipat_local_server.py |
| `ipat_check_votes.py` | IPAT投票照会で購入済み馬券を検証 | ipat_server.py / ipat_local_server.py / auto_purchase.py |
| `ipat_common.py` | 共通ユーティリティ（ログ設定、Discord通知等） | 各スクリプト |
| `ipat_server.py` | Docker用HTTPサーバー（APIリクエストを受けてスクリプト実行） | Dockerコンテナ |
| `ipat_local_server.py` | ローカル用HTTPサーバー（UIからauto_purchase.pyを実行） | ローカルPC |

### IPATサーバー APIエンドポイント一覧

#### ipat_server.py（Docker環境）

| メソッド | パス | 説明 |
|---------|------|------|
| POST | `/purchase` | 馬券購入（JSON body） |
| POST | `/dry-run` | ドライラン |
| POST | `/check-refund` | 払い戻しチェック（非同期、SSE進捗） |
| POST | `/check-votes` | 投票照会（同期、JSONで結果を直接返す） |
| GET | `/health` | ヘルスチェック |
| GET | `/status` | 現在の状態（JSON） |
| GET | `/progress` | SSEで進捗をリアルタイム配信 |

#### ipat_local_server.py���ローカル環境）

| メソッド | パス | 説明 |
|---------|------|------|
| POST | `/run` | auto_purchase.py を実行（本番） |
| POST | `/run?dry_run=1` | ドライラン |
| POST | `/check-refund` | 払い戻しチェック（非同期、SSE進捗） |
| POST | `/check-votes` | 投票照会（同期、JSONで結果を直接返す） |
| GET | `/progress` | SSEで進捗をリアルタイム配信 |
| GET | `/status` | 現在の状態（JSON） |
| GET | `/health` | ヘルスチェック |

### 環境別の実行フロー

#### Docker環境（本番・NAS）

```
Laravel (IpatPurchaseService)
  → HTTP API → ipat_server.py
    → ipat_purchase.py（Selenium操作）
      → JRA IPAT
```

- Laravelが購入判定・予算管理を担当
- `ipat_purchase.py` はJSON引数で賭け情報を受け取り、ブラウザ操作のみ行う
- 結果JSONをLaravelに返し、DBへの反映はLaravel側で実施

#### ローカル環境（Windows）

```
タスクスケジューラ / purchase-monitor画面
  → ipat_local_server.py
    → auto_purchase.py（オーケストレーション）
      → Laravel DB（Vagrant経由でPHP実行し戦略・設定を取得）
      → ipat_purchase.py（Selenium操作）
      → ipat_check_votes.py（投票照会で検証）
      → Laravel DB（購入結果を登録）
```

- `auto_purchase.py` がDB連携・予算管理・購入検証をすべて担当
- Vagrant SSH経由でLaravelのPHPコードを実行し、暗号化された認証情報を復号・戦略ベースの賭け対象を取得
- 購入後に `ipat_check_votes.py` で実際の投票照会を行い検証
- 重複購入防止、週間予算管理、1レースあたり上限の制御を内蔵

### ipat_purchase.py と auto_purchase.py の使い分け

| 観点 | ipat_purchase.py | auto_purchase.py |
|------|------------------|------------------|
| 責務 | ブラウザ操作のみ | DB連携＋予算管理＋検証＋操作 |
| 入力 | JSONコマンドライン引数 | Laravel DB（.env + Vagrant PHP） |
| 出力 | JSON（標準出力） | Discord通知 + DB登録 |
| DB連携 | なし | あり（PHP経由で読み書き） |
| 予算管理 | なし（呼出元が管理） | 内蔵（週間予算・レース上限） |
| 購入検証 | なし | あり（ipat_check_votes.py呼出） |
| 重複防止 | なし | あり（購入済みレースを除外） |
| 使用環境 | Docker（本番） | ローカル（Windows） |

## 処理フロー

### 1. ユーザー設定（事前準備）

**画面:** `/horse-racing/settings`  
**エンドポイント:** `PUT /horse-racing/settings`

ユーザーが以下を設定する：

| 設定項目 | 説明 | 例 |
|---------|------|-----|
| `auto_purchase_enabled` | 自動購入ON/OFF | `true` |
| `weekly_budget` | 週の上限予算（円） | `10000` |
| `max_bet_per_race` | 1レースあたり最大金額 | `1000` |
| `min_expected_value` | AI予想の最低期待値 | `1.50`（= 150%） |
| `min_confidence` | AI予想の最低信頼度 | `60`（%） |
| `target_grades` | 対象グレード | `["G1", "G2", "G3"]` |
| `target_bet_types` | 対象馬券種別 | `["win", "place"]` |
| `active_strategy` | 購入戦略 | `favorite_win` |
| `ipat_id` | IPAT加入者番号 | `12345678` |
| `ipat_password` | IPAT暗証番号（暗号化保存） | `****` |
| `ipat_pars` | P-ARS番号（暗号化保存） | `****` |

IPAT認証情報はLaravelの `encrypt()` (AES-256-GCM) で暗号化してDBに保存。

### 2. AI予想生成

**エンドポイント:** `POST /horse-racing/predict-all`  
**ジョブ:** `HorseRacingPredictJob`

```
HorseRacingService::runWeeklyPredictions($userId)
  ├── 今週の未確定レースを取得
  ├── 各レースに対して AiPredictionService::predictMultiple() を実行
  └── 予想結果をDBに保存（horse_race_predictions テーブル）
```

各予想には以下が含まれる：
- `expected_value` — 期待回収率
- `confidence` — AI信頼度（0-100%）
- `recommended_amount` — 推奨賭け金
- `selections` — 馬番の配列
- `bet_type` — 馬券種別

### 3. 自動購入実行

**エンドポイント:** `POST /horse-racing/purchase`  
**メソッド:** `HorseRacingService::executeAutoPurchase($userId)`

```
executeAutoPurchase($userId)
│
├── 1. 設定チェック
│   └── auto_purchase_enabled が true か確認
│
├── 2. 予算計算
│   ├── 今週のpurchased/pending合計を集計
│   └── weekly_budget - 使用済み = 残り予算
│
├── 3. 対象予想の抽出
│   ├── 未確定レースの予想のみ
│   ├── expected_value >= min_expected_value
│   ├── confidence >= min_confidence
│   ├── まだ馬券が作られていないもの
│   └── expected_value 降順でソート（期待値が高い順）
│
├── 4. 馬券作成ループ（予算がなくなるまで）
│   ├── 賭け金 = min(推奨金額, 1レース上限, 残り予算)
│   ├── 100円未満なら終了
│   ├── 100円単位に切り捨て
│   ├── HorseRaceBet を status='pending' で作成
│   ├── IpatPurchaseService::purchase() を呼出
│   └── 残り予算を減算
│
└── 5. 結果を返却
```

### 4. IPAT操作（Python Selenium）

**スクリプト:** `python/ipat-purchase/ipat_purchase.py`  
**呼出元:** `IpatPurchaseService::purchaseMultiple()`

#### Laravelからの呼出

```php
$result = Process::timeout(180)
    ->env(['STORAGE_PATH' => $storagePath])
    ->run("python3 {$scriptPath} " . escapeshellarg($params));
```

JSONパラメータをコマンドライン引数としてPythonスクリプトに渡す。タイムアウトは180秒。

#### Python側の処理フロー

```
main()
├── 引数のJSON解析
├── Chrome WebDriver起動（ヘッドレスモード）
│
├── IPATログイン
│   ├── https://www.ipat.jra.go.jp/ にアクセス
│   ├── フレーム構造を検出して切り替え
│   ├── 加入者番号を入力
│   ├── 暗証番号を入力
│   ├── P-ARS番号を入力
│   ├── ログインボタンをクリック
│   └── ログイン成功を判定
│
├── 各馬券のセット（place_bet）
│   ├── 通常投票ページへ遷移
│   ├── 競馬場を選択（会場コードで指定）
│   ├── レース番号を選択
│   ├── 馬券種別を選択（単勝/複勝/馬連 等）
│   ├── 馬番をクリックして選択
│   ├── 金額を入力（100円単位）
│   └── 「セット」ボタンをクリック
│
├── 購入確定（confirm_purchase）※dry_runでなければ
│   ├── 「投票」ボタンをクリック
│   ├── 暗証番号の再入力（必要な場合）
│   ├── 最終確定ボタンをクリック
│   └── 結果判定（「受付」「完了」の文字を検出）
│
└── 結果JSONを標準出力に出力
```

#### 競馬場コード

| 競馬場 | コード | 競馬場 | コード |
|--------|--------|--------|--------|
| 札幌 | 01 | 東京 | 05 |
| 函館 | 02 | 中山 | 06 |
| 福島 | 03 | 中京 | 07 |
| 新潟 | 04 | 京都 | 08 |
|  |  | 阪神 | 09 |
|  |  | 小倉 | 10 |

#### 馬券種別マッピング

| システム内部名 | IPAT表示名 |
|--------------|-----------|
| `win` | 単勝 |
| `place` | 複勝 |
| `quinella` | 馬連 |
| `exacta` | 馬単 |
| `wide` | ワイド |
| `trio` | 三連複 |
| `trifecta` | 三連単 |

### 5. 結果反映

Pythonスクリプトが返すJSON:

```json
{
  "success": true,
  "dry_run": false,
  "results": [
    {"bet_id": 1, "success": true, "error": null},
    {"bet_id": 2, "success": false, "error": "馬番要素が見つかりません"}
  ]
}
```

Laravel側で各BetのステータスをDBに反映：
- 成功 → `status = 'purchased'`, `purchased_at = now()`
- 失敗 → `status = 'error'`

### 6. レース結果の精算

**エンドポイント:** `POST /horse-racing/update-results`

レース終了後に結果データを取得し、各Betのステータスを更新：
- 的中 → `status = 'won'`, `payout` に払戻金を記録
- 不的中 → `status = 'lost'`

## Betステータスのライフサイクル

```
pending（Bet作成時）
  ├── purchased（IPAT購入成功）
  │     ├── won（的中）
  │     └── lost（不的中）
  ├── error（購入失敗）
  └── canceled（ユーザーがキャンセル）
```

## 購入戦略一覧

| キー | 名称 | 説明 |
|------|------|------|
| `favorite_win` | 1番人気 単勝 | 1番人気の単勝 |
| `favorite_place` | 1番人気 複勝 | 1番人気の複勝 |
| `favorite_quinella` | 1-2番人気 馬連 | 上位2頭の馬連 |
| `favorite_wide` | 1-2番人気 ワイド | 上位2頭のワイド |
| `favorite_trio` | 1-2-3番人気 三連複 | 上位3頭の三連複 |
| `mid_odds_win` | 中穴狙い 単勝 | 中オッズ帯の単勝 |
| `big_odds_win` | 大穴狙い 単勝 | 高オッズの単勝 |
| `horse_ev` | 馬期待値 単勝 | 馬の期待値ベース |
| `jockey_ev` | 騎手期待値 単勝 | 騎手の期待値ベース |
| `combined_ev` | 複合期待値 単勝 | 馬+騎手の複合期待値 |
| `smart_ev` | Smart EV | 直近走+通算+コース適性の重み付き勝率ベース |
| `ml_ev` | ML EV | LightGBM予測勝率ベース |

### 馬券作成時の重複防止

`HorseRaceBet::createUnique()` により、同一の (user/race/bet_type/selections/amount) の組合せは重複作成されない。`normalizeSelections()` が先頭ゼロ（"09" → 9）を正規化し、IPAT表記とDB表記の不一致を解消する。

## スケジュール実行（現在コメントアウト中）

`routes/console.php` に定義済みだが未有効化：

```php
// 毎週金曜 21:00 — レース同期 + AI予想 + 自動購入
Schedule::command('horse-racing:predict --sync --purchase')->weeklyOn(5, '21:00');

// 毎週月曜 09:00 — レース結果の取得・精算
Schedule::command('horse-racing:update-results')->weeklyOn(1, '09:00');
```

有効化すれば完全自動の週次ワークフローになる。

## 関連ファイル一覧

| パス | 役割 |
|------|------|
| `app/Services/Entertainment/HorseRacing/IpatPurchaseService.php` | IPAT購入のLaravel側サービス |
| `app/Services/Entertainment/HorseRacing/HorseRacingService.php` | 予算管理・購入判定のメインサービス |
| `app/Services/Entertainment/HorseRacing/AiPredictionService.php` | AI予想生成 |
| `python/ipat-purchase/ipat_purchase.py` | Seleniumによるブラウザ自動操作 |
| `python/ipat-purchase/auto_purchase.py` | ローカル用オーケストレーション（DB連携・予算管理・検証） |
| `python/ipat-purchase/ipat_check_votes.py` | IPAT投票照会による購入検証 |
| `python/ipat-purchase/ipat_common.py` | 共通ユーティリティ（ログ・Discord通知等） |
| `python/ipat-purchase/ipat_server.py` | Docker用HTTPサーバー |
| `python/ipat-purchase/ipat_local_server.py` | ローカル用HTTPサーバー（UIからauto_purchase.py実行） |
| `app/Models/Entertainment/Horse/HorseRacingSetting.php` | 設定モデル（IPAT認証情報含む） |
| `app/Models/Entertainment/Horse/HorseRaceBet.php` | 馬券モデル |
| `app/Models/Entertainment/Horse/HorseRacePrediction.php` | AI予想モデル |
| `app/Jobs/Entertainment/HorseRacingPredictJob.php` | 予想+購入のキュージョブ |
| `app/Console/Commands/Entertainment/HorseRacingPredictCommand.php` | Artisanコマンド |
| `resources/js/entertainment/horse-racing-settings.js` | 設定画面のJS |
| `resources/views/entertainment/horse_racing/settings.blade.php` | 設定画面のBlade |

## セキュリティ

- IPAT認証情報（暗証番号・P-ARS）はLaravelの `encrypt()`/`decrypt()` で暗号化保存
- モデルの `$hidden` で認証情報フィールドをAPI応答から除外
- 購入処理: PythonスクリプトにはJSONコマンドライン引数で渡す（`escapeshellarg` でエスケープ）
- 投票照会: HTTP POST body でIPATサーバーに送信（Cloudflare Access Service Token による認証保護対応）
- 全操作をスクリーンショット付きでログ記録（`storage/app/ipat/screenshots/`）

## ログ・デバッグ

- **Laravelログ:** `horse-racing-report` チャンネル → `storage/logs/horse-racing-report-*.log`
- **Pythonログ:** `storage/app/ipat/ipat_purchase.log`
- **スクリーンショット:** `storage/app/ipat/screenshots/ipat_{stage}_{timestamp}.png`
  - ステージ: `01_top`, `02_frame`, `03_filled`, `04_after_login`, `05_vote_top`, `06_race_selected`, `07_bet_type_*`, `08_bet_filled_*`, `09_bet_set_*`, `10_before_confirm`, `11_confirm_dialog`, `12_after_purchase`

## ドライランモード

`IpatPurchaseService::dryRun()` で実際の購入をせずに画面操作まで確認可能。  
Pythonスクリプトに `dry_run: true` を渡すと、最終確定ボタンのクリックをスキップする。  
ドライラン時はヘッドレスモード OFF（`headless: false`）で実行し、ブラウザを目視確認できる。
