File size: 2,874 Bytes
e23172f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
"""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