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

## 概要

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

## アーキテクチャ

```
┌─────────────┐     ┌────────────────────────┐     ┌────────────────────────┐     ┌──────────────┐
│  設定画面     │     │  HorseRacingService    │     │  IpatPurchaseService   │     │  Python      │
│  (Blade+JS)  │────▶│  (購入判定・予算管理)    │────▶│  (スクリプト呼出)       │────▶│  Selenium    │
│              │     │                        │     │                        │     │  (IPAT操作)  │
└─────────────┘     └────────────────────────┘     └────────────────────────┘     └──────────────┘
                              │                                                          │
                              ▼                                                          ▼
                    ┌────────────────────┐                                    ┌──────────────────┐
                    │  DB                │                                    │  JRA IPAT        │
                    │  - settings        │                                    │  (Webサイト)      │
                    │  - bets            │                                    └──────────────────┘
                    │  - predictions     │
                    └────────────────────┘
```

## 処理フロー

### 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` | 複合期待値 単勝 | 馬+騎手の複合期待値 |

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

`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によるブラウザ自動操作 |
| `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` でエスケープ）
- 全操作をスクリーンショット付きでログ記録（`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`）で実行し、ブラウザを目視確認できる。
