"""Pace / race-shape estimation. Where FormFav supplies a pace scenario we use it; otherwise we derive a proxy from the field's historical speed profiles. The engine then classifies SLOW / MODERATE / FAST / VERY FAST and tags each runner's likely role (leader / on-speed / midfield / backmarker) using barrier + historical front-running tendency when available. All adjustments are evidence-based and never universal rules. """ from __future__ import annotations from typing import Optional from ..data.models import RaceModel, RunnerModel def classify_pace(race: RaceModel, runners: list[RunnerModel]) -> dict: """Estimate race shape from available evidence. Uses: - FormFav raceClass / distance as coarse tempo priors (short races + lower classes tend to be faster) - a derived 'speed_pressure' from how many runners have strong front-running or high win% (proxy when no explicit pace field) """ n = len(runners) or 1 pressure = 0.0 leaders = [] for r in runners: wp = r.win_percent # high win% + low barrier => more likely to press forward barrier = r.barrier or 5 fwd = max(0.0, (wp / 100.0) - 0.05) + (0.5 if barrier <= 3 else 0.0) pressure += fwd if fwd > 0.12: leaders.append(r.number) pressure_norm = pressure / n # ~0..0.5 if pressure_norm >= 0.30: label = "VERY FAST" elif pressure_norm >= 0.20: label = "FAST" elif pressure_norm >= 0.11: label = "MODERATE" else: label = "SLOW" roles = {} for r in runners: barrier = r.barrier or 5 wp = r.win_percent / 100.0 if barrier <= 3 and wp >= 0.10: roles[r.number] = "leader/on-speed" elif barrier <= 6: roles[r.number] = "midfield" else: roles[r.number] = "backmarker" return { "label": label, "pressure": round(pressure_norm, 3), "likely_leaders": leaders, "roles": roles, } def pace_adjustment(runner: RunnerModel, pace: dict) -> float: """Return an additive 0..100 sub-score adjustment (can be negative). Logic: - SLOW race: a confirmed leader/on-speed runner is advantaged (can control). - FAST/VERY FAST: a leader is penalised (likely to fold); midfield/strong finishers advantaged. - Backmarkers get a small negative in SLOW (hard to make ground). """ role = pace["roles"].get(runner.number, "midfield") label = pace["label"] if label == "SLOW": if role == "leader/on-speed": return 6.0 if role == "backmarker": return -4.0 return 1.0 if label in ("FAST", "VERY FAST"): if role == "leader/on-speed": return -4.0 if role == "backmarker": return 3.0 return 2.0 return 0.0