Brettapps commited on
Commit
bbddeaa
·
verified ·
1 Parent(s): 057d499

Add Brettapps/trifecta-bro/v1 v1.0.0 trifecta predictor (rule-based)

Browse files
README.md ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Trifecta-Bro v1 — Australian Gallops Trifecta Predictor
3
+ emoji: 🐎
4
+ colorFrom: green
5
+ colorTo: blue
6
+ sdk: "false"
7
+ tags:
8
+ - racing
9
+ - horse-racing
10
+ - australian-gallops
11
+ - trifecta
12
+ - prediction
13
+ - sports-betting
14
+ license: mit
15
+ library_name: other
16
+ ---
17
+
18
+ # Brettapps/trifecta-bro-v1
19
+
20
+ **Model id:** `Brettapps/trifecta-bro/v1`
21
+ **Version:** 1.0.0
22
+ **Task:** Australian Gallops **trifecta prediction** (pick the top-3 finishers, in order).
23
+
24
+ Trifecta-Bro v1 is an open-source, multi-factor trifecta scorer for Australian
25
+ gallops. Given a race's field + form, it assigns each runner a 0–100 score and
26
+ emits a **primary**, **secondary**, and **value** trifecta combination, plus the
27
+ top-3 ranked runners with win/place probabilities.
28
+
29
+ > **Status:** v1 is a deterministic rule-based scorer. No supervised training
30
+ > was performed because the project has no historical race **results** (labels)
31
+ > yet. v2 will train a gradient-boosted / logistic model on observed outcomes
32
+ > once `data/results/` is populated.
33
+
34
+ ## Method
35
+
36
+ For each runner, a weighted 0–100 score is computed from:
37
+
38
+ | Factor | Max weight |
39
+ |--------|-----------|
40
+ | Recent form (last 5 starts: 1/2/3 finishes) | 25 |
41
+ | Career overall win % | 20 |
42
+ | Career overall place % | 10 |
43
+ | Track strike rate (places/starts) | 10 |
44
+ | Distance strike rate | 8 |
45
+ | Condition strike rate (per going) | 8 |
46
+ | Barrier draw | 5 |
47
+ | Career prize money | 5 |
48
+
49
+ The three highest-scoring runners form the **primary** trifecta. A **secondary**
50
+ and **value** combination are derived from the next-best runners (with an
51
+ outsider angle when a score > 30 exists further down the field).
52
+
53
+ ## Usage
54
+
55
+ ```python
56
+ # Install from the Hub
57
+ # pip install huggingface_hub
58
+ from huggingface_hub import snapshot_download
59
+ path = snapshot_download("Brettapps/trifecta-bro-v1")
60
+ import sys; sys.path.insert(0, path)
61
+
62
+ from trifecta_bro_v1 import TrifectaPredictor, race_from_payload
63
+
64
+ payload = {...} # Trifecta-Bro race payload
65
+ race = race_from_payload(payload)
66
+ prediction = TrifectaPredictor().predict(race)
67
+ print(prediction["primary"], prediction["secondary"], prediction["value"])
68
+ ```
69
+
70
+ Or run the bundled CLI:
71
+
72
+ ```bash
73
+ python -m trifecta_bro_v1.main --data predictions-2026-08-10.json
74
+ ```
75
+
76
+ ## Artifact
77
+
78
+ `model_artifacts/model_artifact.json` documents the model method, feature
79
+ weights, and version — making the published model interpretable and reproducible.
80
+
81
+ ## Backend note
82
+
83
+ This model is also wired as the `Brettapps/trifecta-bro/v1` identity in the
84
+ Trifecta-Bro LM Studio / Obsidian-vault backend. The HF-published code is the
85
+ canonical, dependency-light inference implementation.
config.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_id": "Brettapps/trifecta-bro/v1",
3
+ "version": "1.0.0",
4
+ "task": "trifecta-prediction",
5
+ "domain": "australian-gallops",
6
+ "method": "multi-factor-rule-based-scoring",
7
+ "library_name": "other",
8
+ "license": "mit",
9
+ "dependencies": [],
10
+ "inference_entrypoints": [
11
+ "trifecta_bro_v1.predictor.TrifectaPredictor",
12
+ "trifecta_bro_v1.artifact.predict_race"
13
+ ],
14
+ "artifact": "model_artifacts/model_artifact.json"
15
+ }
model_artifacts/model_artifact.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_id": "Brettapps/trifecta-bro/v1",
3
+ "version": "1.0.0",
4
+ "method": "multi-factor rule-based scoring (0-100)",
5
+ "feature_weights": {
6
+ "recent_form": 25.0,
7
+ "overall_win_pct": 20.0,
8
+ "overall_place_pct": 10.0,
9
+ "track_strike": 10.0,
10
+ "distance_strike": 8.0,
11
+ "condition_strike": 8.0,
12
+ "barrier_draw": 5.0,
13
+ "career_prize": 5.0
14
+ },
15
+ "notes": "Open-source multi-factor trifecta scorer. No supervised training (no historical race results available in data/results/). Scores are deterministic given runner form/stat inputs. Future v2 will train a gradient-boosted model on observed outcomes."
16
+ }
push.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Publish Brettapps/trifecta-bro-v1 to the HuggingFace Hub.
3
+
4
+ Requires a VALID HuggingFace token, supplied via:
5
+ - HF_ACCESS_TOKEN / HF_TOKEN env var, or
6
+ - the cached `huggingface_hub` credentials (hf auth login)
7
+
8
+ This script ONLY uploads; it does not build the package (build is done via
9
+ save_artifact + the committed source files). Run after `hf auth login` succeeds.
10
+
11
+ Usage:
12
+ python push.py # uses cached/ENV token
13
+ HF_ACCESS_TOKEN=hf_xxx python push.py
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import os
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ from huggingface_hub import HfApi, create_repo
23
+
24
+ REPO_ID = "Brettapps/trifecta-bro-v1"
25
+ REPO_TYPE = "model"
26
+ LOCAL_DIR = Path(__file__).resolve().parents[0]
27
+ ARTIFACT_DIR = LOCAL_DIR / "model_artifacts"
28
+
29
+
30
+ def main() -> int:
31
+ token = os.environ.get("HF_ACCESS_TOKEN") or os.environ.get("HF_TOKEN")
32
+ api = HfApi(token=token)
33
+
34
+ # Fail fast with a clear message if the token is invalid/expired.
35
+ try:
36
+ me = api.whoami()
37
+ print(f"Authenticated as: {me.get('name') or me.get('id')}")
38
+ except Exception as exc: # noqa: BLE001
39
+ print(f"ERROR: HuggingFace authentication failed: {exc}")
40
+ print("Fix: run `hf auth login` with a valid token, or set HF_ACCESS_TOKEN.")
41
+ return 1
42
+
43
+ # Ensure the repo exists under the authenticated namespace.
44
+ create_repo(
45
+ repo_id=REPO_ID,
46
+ repo_type=REPO_TYPE,
47
+ token=token,
48
+ exist_ok=True,
49
+ )
50
+
51
+ print(f"Uploading {LOCAL_DIR} -> {REPO_ID}")
52
+ api.upload_folder(
53
+ folder_path=str(LOCAL_DIR),
54
+ repo_id=REPO_ID,
55
+ repo_type=REPO_TYPE,
56
+ commit_message="Add Brettapps/trifecta-bro/v1 v1.0.0 predictor",
57
+ token=token,
58
+ )
59
+ print(f"DONE: https://huggingface.co/{REPO_ID}")
60
+ return 0
61
+
62
+
63
+ if __name__ == "__main__":
64
+ raise SystemExit(main())
pyproject.toml ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "trifecta-bro-v1"
7
+ version = "1.0.0"
8
+ description = "Australian Gallops trifecta predictor (Brettapps/trifecta-bro/v1)."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.9"
12
+ keywords = ["horse-racing", "trifecta", "australian-gallops", "prediction"]
13
+
14
+ [tool.setuptools.packages.find]
15
+ where = ["."]
16
+ include = ["trifecta_bro_v1*"]
trifecta_bro_v1/__init__.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Brettapps/trifecta-bro-v1 — Australian Gallops trifecta predictor (v1)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .artifact import (
6
+ ARTIFACT,
7
+ MODEL_ID,
8
+ ModelArtifact,
9
+ load_artifact,
10
+ predict_race,
11
+ race_from_payload,
12
+ save_artifact,
13
+ )
14
+ from .predictor import Race, Runner, TrifectaPredictor
15
+
16
+ __version__ = "1.0.0"
17
+
18
+ __all__ = [
19
+ "TrifectaPredictor",
20
+ "Race",
21
+ "Runner",
22
+ "MODEL_ID",
23
+ "ARTIFACT",
24
+ "ModelArtifact",
25
+ "save_artifact",
26
+ "load_artifact",
27
+ "predict_race",
28
+ "race_from_payload",
29
+ "__version__",
30
+ ]
trifecta_bro_v1/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (732 Bytes). View file
 
trifecta_bro_v1/__pycache__/artifact.cpython-313.pyc ADDED
Binary file (4.82 kB). View file
 
trifecta_bro_v1/__pycache__/main.cpython-313.pyc ADDED
Binary file (3.12 kB). View file
 
trifecta_bro_v1/__pycache__/predictor.cpython-313.pyc ADDED
Binary file (8.94 kB). View file
 
trifecta_bro_v1/artifact.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Artifact save/load and model metadata for Brettapps/trifecta-bro-v1."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import asdict, dataclass
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from .predictor import Race, Runner, TrifectaPredictor
11
+
12
+
13
+ MODEL_ID = "Brettapps/trifecta-bro/v1"
14
+ MODEL_CARD_VERSION = "1.0.0"
15
+
16
+
17
+ @dataclass
18
+ class ModelArtifact:
19
+ model_id: str
20
+ version: str
21
+ method: str
22
+ feature_weights: dict[str, float]
23
+ notes: str
24
+
25
+
26
+ # The "learned" configuration of the rule-based model. Documented explicitly so
27
+ # the published artifact is interpretable and reproducible.
28
+ ARTIFACT = ModelArtifact(
29
+ model_id=MODEL_ID,
30
+ version=MODEL_CARD_VERSION,
31
+ method="multi-factor rule-based scoring (0-100)",
32
+ feature_weights={
33
+ "recent_form": 25.0,
34
+ "overall_win_pct": 20.0,
35
+ "overall_place_pct": 10.0,
36
+ "track_strike": 10.0,
37
+ "distance_strike": 8.0,
38
+ "condition_strike": 8.0,
39
+ "barrier_draw": 5.0,
40
+ "career_prize": 5.0,
41
+ },
42
+ notes=(
43
+ "Open-source multi-factor trifecta scorer. No supervised training "
44
+ "(no historical race results available in data/results/). Scores are "
45
+ "deterministic given runner form/stat inputs. Future v2 will train a "
46
+ "gradient-boosted model on observed outcomes."
47
+ ),
48
+ )
49
+
50
+
51
+ def save_artifact(out_dir: str | Path) -> Path:
52
+ out_dir = Path(out_dir)
53
+ out_dir.mkdir(parents=True, exist_ok=True)
54
+ path = out_dir / "model_artifact.json"
55
+ path.write_text(json.dumps(asdict(ARTIFACT), indent=2))
56
+ return path
57
+
58
+
59
+ def load_artifact(path: str | Path) -> ModelArtifact:
60
+ data = json.loads(Path(path).read_text())
61
+ return ModelArtifact(**data)
62
+
63
+
64
+ def predict_race(race: Race) -> dict[str, Any]:
65
+ """Convenience: run the packaged predictor on a Race."""
66
+ return TrifectaPredictor().predict(race)
67
+
68
+
69
+ def race_from_payload(payload: dict[str, Any]) -> Race:
70
+ """Build a Race from the Trifecta-Bro predictions JSON shape."""
71
+ form = payload.get("form", {})
72
+ runners = [
73
+ Runner(
74
+ number=r.get("number", 0),
75
+ name=r.get("name", ""),
76
+ jockey=r.get("jockey", ""),
77
+ trainer=r.get("trainer", ""),
78
+ weight=r.get("weight"),
79
+ barrier=r.get("barrier"),
80
+ form=r.get("form", ""),
81
+ last20Starts=r.get("last20Starts", ""),
82
+ careerPrizeMoney=r.get("careerPrizeMoney", "$0"),
83
+ scratched=bool(r.get("scratched", False)),
84
+ stats=r.get("stats", {}),
85
+ )
86
+ for r in form.get("runners", [])
87
+ ]
88
+ return Race(
89
+ date=payload.get("date", ""),
90
+ track=payload.get("track", ""),
91
+ track_slug=payload.get("track_slug", ""),
92
+ race_number=str(payload.get("race_number", "")),
93
+ race_name=payload.get("race_name", ""),
94
+ distance=payload.get("distance", ""),
95
+ condition=payload.get("condition", ""),
96
+ weather=payload.get("weather", ""),
97
+ race_class=payload.get("race_class", ""),
98
+ start_time=payload.get("start_time", ""),
99
+ prize_money=payload.get("prize_money", ""),
100
+ number_of_runners=int(payload.get("number_of_runners", 0) or 0),
101
+ runners=runners,
102
+ )
trifecta_bro_v1/main.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """CLI for Brettapps/trifecta-bro-v1.
3
+
4
+ Usage:
5
+ # Run on the bundled sample race
6
+ python -m trifecta_bro_v1.main
7
+
8
+ # Run on a Trifecta-Bro predictions JSON file
9
+ python -m trifecta_bro_v1.main --data /path/to/predictions-2026-08-10.json
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import json
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ from .artifact import predict_race, race_from_payload
20
+ from .predictor import TrifectaPredictor
21
+
22
+ # A tiny self-contained demo race so the package is runnable with no inputs.
23
+ SAMPLE = {
24
+ "date": "2026-08-10",
25
+ "track": "Dubbo",
26
+ "track_slug": "dubbo",
27
+ "race_number": "3",
28
+ "race_name": "Aqua West Country Boosted BM58 Handicap",
29
+ "distance": "1620m",
30
+ "condition": "Soft 5",
31
+ "weather": "Showers",
32
+ "race_class": "BM58",
33
+ "start_time": "2026-08-10T04:40:00Z",
34
+ "prize_money": "30000",
35
+ "number_of_runners": 3,
36
+ "form": {
37
+ "runners": [
38
+ {"number": 1, "name": "Boncapo", "form": "X1241", "stats": {"overall": {"winPercent": 0.25, "placePercent": 0.5}}},
39
+ {"number": 14, "name": "Bill Peyto", "form": "36412", "stats": {"overall": {"winPercent": 0.07, "placePercent": 0.35}}},
40
+ {"number": 7, "name": "Casterly Rock", "form": "23939", "stats": {"overall": {"winPercent": 0.06, "placePercent": 0.48}}},
41
+ ]
42
+ },
43
+ }
44
+
45
+
46
+ def main(argv: list[str] | None = None) -> int:
47
+ p = argparse.ArgumentParser(description="Brettapps/trifecta-bro/v1 predictor")
48
+ p.add_argument("--data", help="Path to a Trifecta-Bro predictions JSON file")
49
+ p.add_argument("--race", type=int, default=0, help="1-based race index when --data is given")
50
+ args = p.parse_args(argv)
51
+
52
+ if args.data:
53
+ payload = json.loads(Path(args.data).read_text())
54
+ races = payload.get("races", [payload])
55
+ idx = max(0, args.race - 1)
56
+ if idx >= len(races):
57
+ print(f"Race index {args.race} out of range (have {len(races)})", file=sys.stderr)
58
+ return 2
59
+ race = race_from_payload(races[idx])
60
+ else:
61
+ race = race_from_payload(SAMPLE)
62
+
63
+ pred = TrifectaPredictor().predict(race)
64
+ print(json.dumps(pred, indent=2))
65
+ return 0
66
+
67
+
68
+ if __name__ == "__main__":
69
+ raise SystemExit(main())
trifecta_bro_v1/predictor.py ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Trifecta-Bro v1 — open-source multi-factor trifecta predictor.
2
+
3
+ Model id: Brettapps/trifecta-bro/v1 (HF repo: Brettapps/trifecta-bro-v1)
4
+
5
+ This is the standalone inference module for the HuggingFace model repo. It is a
6
+ self-contained, dependency-light implementation of the trifecta prediction logic
7
+ (rule-based multi-factor scoring). It mirrors `src/prediction_model.py` in the
8
+ Trifecta-Bro Space so the published model is directly usable without cloning the
9
+ whole Space.
10
+
11
+ Load it:
12
+ from trifecta_bro_v1.predictor import TrifectaPredictor, Race, Runner
13
+ pred = TrifectaPredictor().predict(race)
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from dataclasses import dataclass, field
19
+ from typing import Any
20
+
21
+
22
+ @dataclass
23
+ class Runner:
24
+ number: int
25
+ name: str
26
+ jockey: str = ""
27
+ trainer: str = ""
28
+ weight: float | None = None
29
+ barrier: int | None = None
30
+ form: str = ""
31
+ last20Starts: str = ""
32
+ careerPrizeMoney: str = "$0"
33
+ scratched: bool = False
34
+ stats: dict[str, Any] = field(default_factory=dict)
35
+
36
+
37
+ @dataclass
38
+ class Race:
39
+ date: str
40
+ track: str
41
+ track_slug: str
42
+ race_number: str
43
+ race_name: str
44
+ distance: str
45
+ condition: str
46
+ weather: str
47
+ race_class: str
48
+ start_time: str
49
+ prize_money: str
50
+ number_of_runners: int
51
+ runners: list[Runner] = field(default_factory=list)
52
+
53
+
54
+ class TrifectaPredictor:
55
+ """Open-source trifecta prediction model.
56
+
57
+ Scores each runner on a 0-100 scale using recent form, career/overall
58
+ win & place percentages, track/distance/condition strike rates, barrier
59
+ draw, and career prize money. The top three by score form the primary
60
+ trifecta; secondary and value bets are derived from the next-best runners.
61
+ """
62
+
63
+ MODEL_ID = "Brettapps/trifecta-bro/v1"
64
+
65
+ def predict(self, race: Race) -> dict[str, Any]:
66
+ runners = [r for r in race.runners if not r.scratched]
67
+ if len(runners) < 3:
68
+ return {"error": "Insufficient runners"}
69
+
70
+ scored: list[dict[str, Any]] = []
71
+ for runner in runners:
72
+ scored.append({
73
+ "number": runner.number,
74
+ "name": runner.name,
75
+ "score": self._score_runner(runner),
76
+ "win_prob": self._win_probability(runner),
77
+ "place_prob": self._place_probability(runner),
78
+ })
79
+
80
+ scored.sort(key=lambda x: x["score"], reverse=True)
81
+ top = scored[:3]
82
+ primary = f"{top[0]['number']}-{top[1]['number']}-{top[2]['number']}"
83
+
84
+ secondary = None
85
+ value = None
86
+ if len(scored) > 3:
87
+ secondary = f"{top[0]['number']}-{top[2]['number']}-{scored[3]['number']}"
88
+ outsiders = [s for s in scored[3:] if s["score"] > 30]
89
+ if outsiders:
90
+ value = f"{scored[1]['number']}-{top[0]['number']}-{outsiders[0]['number']}"
91
+ else:
92
+ value = f"{scored[1]['number']}-{top[0]['number']}-{top[2]['number']}"
93
+
94
+ return {
95
+ "model_id": self.MODEL_ID,
96
+ "date": race.date,
97
+ "track": race.track,
98
+ "race_number": race.race_number,
99
+ "race_name": race.race_name,
100
+ "primary": primary,
101
+ "secondary": secondary,
102
+ "value": value,
103
+ "top3": top,
104
+ "confidence": "MEDIUM",
105
+ }
106
+
107
+ def _score_runner(self, runner: Runner) -> float:
108
+ score = 0.0
109
+ form = str(runner.form or runner.last20Starts or "")
110
+ recent = form[-5:] if len(form) > 5 else form
111
+ score += min((recent.count("1") * 8 + recent.count("2") * 4 + recent.count("3") * 4), 25)
112
+
113
+ overall = runner.stats.get("overall", {})
114
+ win_pct = overall.get("winPercent", 0) or 0
115
+ place_pct = overall.get("placePercent", 0) or 0
116
+ score += win_pct * 20
117
+ score += place_pct * 10
118
+
119
+ track_stats = runner.stats.get("track", {})
120
+ track_starts = track_stats.get("starts", 0) or 0
121
+ track_places = track_stats.get("places", 0) or 0
122
+ score += min((track_places / max(track_starts, 1)) * 10, 10)
123
+
124
+ dist_stats = runner.stats.get("distance", {})
125
+ dist_starts = dist_stats.get("starts", 0) or 0
126
+ dist_places = dist_stats.get("places", 0) or 0
127
+ score += min((dist_places / max(dist_starts, 1)) * 8, 8)
128
+
129
+ cond_stats = runner.stats.get("conditions", {})
130
+ for _key, data in cond_stats.items():
131
+ c_starts = data.get("starts", 0) or 0
132
+ c_places = data.get("places", 0) or 0
133
+ score += min((c_places / max(c_starts, 1)) * 8, 8)
134
+
135
+ try:
136
+ barrier = int(runner.barrier) if runner.barrier else 5
137
+ score += max(0, 5 - abs(barrier - 5))
138
+ except Exception:
139
+ score += 3
140
+
141
+ try:
142
+ prize = float(str(runner.careerPrizeMoney).replace("$", "").replace(",", ""))
143
+ score += min(prize / 20000, 5)
144
+ except Exception:
145
+ pass
146
+
147
+ return min(round(score, 1), 100)
148
+
149
+ def _win_probability(self, runner: Runner) -> float:
150
+ overall = runner.stats.get("overall", {})
151
+ win_pct = overall.get("winPercent", 0) or 0
152
+ return min(max(round(win_pct * 100, 1), 0), 100)
153
+
154
+ def _place_probability(self, runner: Runner) -> float:
155
+ overall = runner.stats.get("overall", {})
156
+ place_pct = overall.get("placePercent", 0) or 0
157
+ return min(max(round(place_pct * 100, 1), 0), 100)