| """Trifecta-Bro v1 — open-source multi-factor trifecta predictor. |
| |
| Model id: Brettapps/trifecta-bro/v1 (HF repo: Brettapps/trifecta-bro-v1) |
| |
| This is the standalone inference module for the HuggingFace model repo. It is a |
| self-contained, dependency-light implementation of the trifecta prediction logic |
| (rule-based multi-factor scoring). It mirrors `src/prediction_model.py` in the |
| Trifecta-Bro Space so the published model is directly usable without cloning the |
| whole Space. |
| |
| Load it: |
| from trifecta_bro_v1.predictor import TrifectaPredictor, Race, Runner |
| pred = TrifectaPredictor().predict(race) |
| """ |
|
|
| 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. |
| |
| Scores each runner on a 0-100 scale using recent form, career/overall |
| win & place percentages, track/distance/condition strike rates, barrier |
| draw, and career prize money. The top three by score form the primary |
| trifecta; secondary and value bets are derived from the next-best runners. |
| """ |
|
|
| MODEL_ID = "Brettapps/trifecta-bro/v1" |
|
|
| 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 { |
| "model_id": self.MODEL_ID, |
| "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", {}) |
| 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) |
|
|