Initial commit with 2026 World Cup Quant Platform core modules and CI/CD

This commit is contained in:
QuantBot
2026-06-13 23:18:18 +08:00
commit 073abf98c1
155 changed files with 19539 additions and 0 deletions

View File

@@ -0,0 +1,79 @@
"""球員道具盤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']),
}