"""球員道具盤(Props)蒙地卡羅模擬模組。""" from __future__ import annotations from dataclasses import dataclass import numpy as np @dataclass(frozen=True) class PlayerPropsDistribution: shots: np.ndarray shots_on_target: np.ndarray passes: np.ndarray def simulate_player_stats( player_metrics: dict, opponent_defense_metrics: dict, iterations: int = 10_000, ) -> PlayerPropsDistribution: """快速模擬球員事件次數分佈。""" if iterations <= 0: raise ValueError('iterations 必須大於 0') avg_touches = float(player_metrics.get('avg_touches', 45) or 0.0) base_shot_rate = float(player_metrics.get('shots_per_touch', 0.08) or 0.0) base_target_rate = float(player_metrics.get('shot_on_target_rate', 0.35) or 0.0) base_pass_rate = float(player_metrics.get('passes_per_touch', 0.65) or 0.0) opp_pressure = float(opponent_defense_metrics.get('pressing_index', 1.0) or 1.0) opp_tackling = float(opponent_defense_metrics.get('marking_index', 1.0) or 1.0) adj_touches = max(1.0, avg_touches * max(0.6, 1.0 / max(0.5, opp_pressure))) shot_lambda = adj_touches * base_shot_rate pass_lambda = adj_touches * base_pass_rate rng = np.random.default_rng() shots = rng.poisson(lam=shot_lambda, size=iterations) passes = rng.poisson(lam=pass_lambda, size=iterations) # 對方壓迫會降低射正率 effective_target_rate = max(0.02, base_target_rate / max(opp_tackling, 0.3)) shots_on_target = rng.binomial(shots, p=min(effective_target_rate, 0.99), size=iterations) return PlayerPropsDistribution(shots=shots.astype(int), shots_on_target=shots_on_target.astype(int), passes=passes.astype(int)) def evaluate_prop_bet( simulated_distribution: PlayerPropsDistribution, line: float, odds: float, ) -> dict[str, float | bool]: """從 10,000 次模擬結果計算超過盤口機率與 EV。""" if odds <= 1: raise ValueError('odds 必須大於 1') if line < 0: raise ValueError('line 必須大於等於 0') shots = simulated_distribution.shots if shots.size == 0: raise ValueError('distribution 為空') probability_over = float((shots > line).mean()) from .ev_calculator import calculate_expected_value ev = calculate_expected_value(probability_over, odds) return { 'metric': 'shots', 'line': line, 'over_probability': round(probability_over, 6), 'under_probability': round(1.0 - probability_over, 6), 'implied_ev': ev['ev_value'], 'ev_percentage': ev['ev_percentage'], 'is_value_bet': bool(ev['is_value_bet']), }