Brettapps's picture
Upload folder using huggingface_hub (part 21)
e23172f verified
Raw History Blame Contribute Delete
7.02 kB
"""Backtesting harness for trifecta model optimization.
Evaluates any weight profile against historical results with:
- Leave-one-day-out cross-validation
- Grid search over weight space
- Statistical significance testing
"""
from __future__ import annotations
import itertools
import json
import logging
from pathlib import Path
from typing import Optional
logger = logging.getLogger("trifecta_bro.backtest")
def evaluate_prediction(pred_nums: list[int], actual_nums: list[int]) -> dict:
"""Compare a prediction against actual results."""
if not pred_nums or not actual_nums:
return {"exact": False, "box": False, "top1": False, "top3": 0}
return {
"exact": pred_nums == actual_nums,
"box": set(pred_nums) == set(actual_nums),
"top1": pred_nums[0] == actual_nums[0],
"top3": len(set(pred_nums) & set(actual_nums)),
}
def evaluate_day(
predictions: list[dict],
results: list[dict],
) -> dict:
"""Evaluate all predictions for a single day."""
# Build result lookup
result_lookup = {}
for r in results:
key = (r.get("track", ""), r.get("race", 0))
actual = r.get("trifecta", [])
if actual:
result_lookup[key] = actual
stats = {
"total": 0,
"exact": 0,
"box": 0,
"top1": 0,
"top3_total": 0,
}
for pred in predictions:
track = pred.get("track", "")
race = pred.get("race", 0)
pred_nums = pred.get("trifecta", [])
actual = result_lookup.get((track, race))
if not actual or pred.get("skipped"):
continue
stats["total"] += 1
result = evaluate_prediction(pred_nums, actual)
stats["exact"] += result["exact"]
stats["box"] += result["box"]
stats["top1"] += result["top1"]
stats["top3_total"] += result["top3"]
if stats["total"] > 0:
stats["top1_pct"] = stats["top1"] / stats["total"] * 100
stats["top3_pct"] = stats["top3_total"] / (stats["total"] * 3) * 100
return stats
def load_day_data(date: str, data_dir: str = "/home/brettanthonysjoberg179/trifecta-bro-hf-space/data") -> tuple:
"""Load predictions and results for a single day."""
# Load predictions
pred_path = Path(data_dir) / "reports" / f"predictions-{date}.json"
if not pred_path.exists():
return [], []
with open(pred_path) as f:
pred_data = json.load(f)
predictions = pred_data.get("predictions", [])
# Load results
results = []
# Helper to parse runner numbers (handle "13e" emergency runners)
def parse_num(s):
return int(str(s).replace("e", "").replace("E", ""))
# Try multiple result file formats
result_path = Path(data_dir) / "results" / f"{date}.json"
if result_path.exists():
with open(result_path) as f:
rdata = json.load(f)
if "tracks" in rdata:
for track, races in rdata["tracks"].items():
if isinstance(races, list):
for r in races:
if "trifecta" in r:
results.append({
"track": track,
"race": r.get("race", r.get("race_number", 0)),
"trifecta": [parse_num(x) for x in r["trifecta"]],
})
elif isinstance(races, dict):
for rn, r in races.items():
if "runners" in r:
runners = r.get("runners", [])
winners = []
for runner in runners:
pos = runner.get("position")
if pos and pos <= 3:
winners.append((pos, runner["number"]))
winners.sort()
actual = [w[1] for w in winners]
if actual:
results.append({
"track": track,
"race": int(rn),
"trifecta": actual,
})
# Try alternate result file
alt_path = Path(data_dir) / "results" / f"{date}-ra.json"
if alt_path.exists() and not results:
with open(alt_path) as f:
rdata = json.load(f)
for track, races in rdata.items():
for r in races:
if "trifecta" in r:
results.append({
"track": track,
"race": r["race"],
"trifecta": [parse_num(x) for x in r["trifecta"]],
})
return predictions, results
def cross_validate(
dates: list[str],
weight_profile: dict,
) -> dict:
"""Run leave-one-day-out cross-validation."""
all_predictions = {}
all_results = {}
for date in dates:
predictions, results = load_day_data(date)
if predictions and results:
all_predictions[date] = predictions
all_results[date] = results
if not all_predictions:
return {"error": "No data loaded"}
# Evaluate each day
day_stats = {}
for date in sorted(all_predictions.keys()):
stats = evaluate_day(all_predictions[date], all_results[date])
day_stats[date] = stats
# Aggregate
total_races = sum(s["total"] for s in day_stats.values())
total_top1 = sum(s["top1"] for s in day_stats.values())
total_exact = sum(s["exact"] for s in day_stats.values())
return {
"dates": list(day_stats.keys()),
"day_stats": day_stats,
"total_races": total_races,
"total_top1": total_top1,
"total_exact": total_exact,
"top1_pct": total_top1 / total_races * 100 if total_races > 0 else 0,
"profile": weight_profile,
}
def grid_search(
dates: list[str],
param_grid: dict,
base_profile: dict,
) -> list[dict]:
"""Grid search over weight combinations."""
results = []
# Generate all combinations
param_names = list(param_grid.keys())
param_values = [param_grid[name] for name in param_names]
for combo in itertools.product(*param_values):
profile = dict(base_profile)
for name, value in zip(param_names, combo):
profile[name] = value
# Normalize weights to sum to 1.0
total_w = sum(profile.values())
if total_w > 0:
profile = {k: v / total_w for k, v in profile.items()}
# Evaluate
cv_result = cross_validate(dates, profile)
cv_result["params"] = dict(zip(param_names, combo))
results.append(cv_result)
# Sort by top1_pct
results.sort(key=lambda x: x.get("top1_pct", 0), reverse=True)
return results