File size: 4,534 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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
"""Lightweight open-source trifecta prediction model."""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any


@dataclass
class Runner:
    number: int
    name: str
    jockey: str = ""
    trainer: str = ""
    weight: float | None = None
    barrier: int | None = None
    form: str = ""
    last20Starts: str = ""
    careerPrizeMoney: str = "$0"
    scratched: bool = False
    stats: dict[str, Any] = field(default_factory=dict)


@dataclass
class Race:
    date: str
    track: str
    track_slug: str
    race_number: str
    race_name: str
    distance: str
    condition: str
    weather: str
    race_class: str
    start_time: str
    prize_money: str
    number_of_runners: int
    runners: list[Runner] = field(default_factory=list)


class TrifectaPredictor:
    """Open-source trifecta prediction model."""

    def predict(self, race: Race) -> dict[str, Any]:
        runners = [r for r in race.runners if not r.scratched]
        if len(runners) < 3:
            return {"error": "Insufficient runners"}

        scored: list[dict[str, Any]] = []
        for runner in runners:
            scored.append({
                "number": runner.number,
                "name": runner.name,
                "score": self._score_runner(runner),
                "win_prob": self._win_probability(runner),
                "place_prob": self._place_probability(runner),
            })

        scored.sort(key=lambda x: x["score"], reverse=True)
        top = scored[:3]
        primary = f"{top[0]['number']}-{top[1]['number']}-{top[2]['number']}"

        secondary = None
        value = None
        if len(scored) > 3:
            secondary = f"{top[0]['number']}-{top[2]['number']}-{scored[3]['number']}"
            outsiders = [s for s in scored[3:] if s["score"] > 30]
            if outsiders:
                value = f"{scored[1]['number']}-{top[0]['number']}-{outsiders[0]['number']}"
            else:
                value = f"{scored[1]['number']}-{top[0]['number']}-{top[2]['number']}"

        return {
            "date": race.date,
            "track": race.track,
            "race_number": race.race_number,
            "race_name": race.race_name,
            "primary": primary,
            "secondary": secondary,
            "value": value,
            "top3": top,
            "confidence": "MEDIUM",
        }

    def _score_runner(self, runner: Runner) -> float:
        score = 0.0
        form = str(runner.form or runner.last20Starts or "")
        recent = form[-5:] if len(form) > 5 else form
        score += min((recent.count("1") * 8 + recent.count("2") * 4 + recent.count("3") * 4), 25)

        overall = runner.stats.get("overall", {})
        starts = overall.get("starts", 0) or 0
        win_pct = overall.get("winPercent", 0) or 0
        place_pct = overall.get("placePercent", 0) or 0
        score += win_pct * 20
        score += place_pct * 10

        track_stats = runner.stats.get("track", {})
        track_starts = track_stats.get("starts", 0) or 0
        track_places = track_stats.get("places", 0) or 0
        score += min((track_places / max(track_starts, 1)) * 10, 10)

        dist_stats = runner.stats.get("distance", {})
        dist_starts = dist_stats.get("starts", 0) or 0
        dist_places = dist_stats.get("places", 0) or 0
        score += min((dist_places / max(dist_starts, 1)) * 8, 8)

        cond_stats = runner.stats.get("conditions", {})
        for key, data in cond_stats.items():
            c_starts = data.get("starts", 0) or 0
            c_places = data.get("places", 0) or 0
            score += min((c_places / max(c_starts, 1)) * 8, 8)

        try:
            barrier = int(runner.barrier) if runner.barrier else 5
            score += max(0, 5 - abs(barrier - 5))
        except Exception:
            score += 3

        try:
            prize = float(str(runner.careerPrizeMoney).replace("$", "").replace(",", ""))
            score += min(prize / 20000, 5)
        except Exception:
            pass

        return min(round(score, 1), 100)

    def _win_probability(self, runner: Runner) -> float:
        overall = runner.stats.get("overall", {})
        win_pct = overall.get("winPercent", 0) or 0
        return min(max(round(win_pct * 100, 1), 0), 100)

    def _place_probability(self, runner: Runner) -> float:
        overall = runner.stats.get("overall", {})
        place_pct = overall.get("placePercent", 0) or 0
        return min(max(round(place_pct * 100, 1), 0), 100)