Brettapps's picture
Add Brettapps/trifecta-bro/v1 v1.0.0 trifecta predictor (rule-based)
bbddeaa verified
Raw
History Blame Contribute Delete
5.43 kB
"""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)