"""Comprehensive feature engineering from existing FormFav data.""" from __future__ import annotations import json from pathlib import Path from typing import Optional from trifecta_bro.data.models import RunnerModel, RaceModel def extract_features(runner: RunnerModel, race: RaceModel) -> dict: """Extract all possible features from runner data for ML model.""" features = {} # Basic features features['number'] = runner.number features['barrier'] = runner.barrier or 0 features['weight'] = runner.weight or 57.0 features['age'] = runner.age or 4 # Sex encoding sex = (runner.sex or '').lower() features['is_gelding'] = 1 if 'gelding' in sex else 0 features['is_mare'] = 1 if 'mare' in sex else 0 features['is_filly'] = 1 if 'filly' in sex else 0 features['is_colt'] = 1 if 'colt' in sex else 0 # Career stats overall = runner.stats.get('overall', {}) features['career_starts'] = overall.get('starts', 0) or 0 features['career_wins'] = overall.get('wins', 0) or 0 features['career_places'] = overall.get('places', 0) or 0 features['career_win_pct'] = overall.get('winPercent', 0) or 0 features['career_place_pct'] = overall.get('placePercent', 0) or 0 # Track stats track = runner.stats.get('track', {}) features['track_starts'] = track.get('starts', 0) or 0 features['track_wins'] = track.get('wins', 0) or 0 features['track_win_pct'] = track.get('winPercent', 0) or 0 features['track_place_pct'] = track.get('placePercent', 0) or 0 # Distance stats distance = runner.stats.get('distance', {}) features['distance_starts'] = distance.get('starts', 0) or 0 features['distance_wins'] = distance.get('wins', 0) or 0 features['distance_win_pct'] = distance.get('winPercent', 0) or 0 features['distance_place_pct'] = distance.get('placePercent', 0) or 0 # Track + Distance combo track_dist = runner.stats.get('trackDistance', {}) features['track_distance_starts'] = track_dist.get('starts', 0) or 0 features['track_distance_wins'] = track_dist.get('wins', 0) or 0 features['track_distance_win_pct'] = track_dist.get('winPercent', 0) or 0 features['track_distance_place_pct'] = track_dist.get('placePercent', 0) or 0 # Condition stats (match by race condition) cond = (race.condition or '').lower() cond_stats = runner.stats.get('conditions', {}) cond_key = None for key in cond_stats: if cond and cond[:4] in key.lower(): cond_key = key break if cond_key: cs = cond_stats[cond_key] features['cond_starts'] = cs.get('starts', 0) or 0 features['cond_wins'] = cs.get('wins', 0) or 0 features['cond_win_pct'] = cs.get('winPercent', 0) or 0 features['cond_place_pct'] = cs.get('placePercent', 0) or 0 else: features['cond_starts'] = 0 features['cond_wins'] = 0 features['cond_win_pct'] = 0 features['cond_place_pct'] = 0 # First-up / Second-up stats first_up = runner.stats.get('firstUp', {}) second_up = runner.stats.get('secondUp', {}) features['first_up_starts'] = first_up.get('starts', 0) or 0 features['first_up_wins'] = first_up.get('wins', 0) or 0 features['first_up_win_pct'] = first_up.get('winPercent', 0) or 0 features['second_up_starts'] = second_up.get('starts', 0) or 0 features['second_up_wins'] = second_up.get('wins', 0) or 0 features['second_up_win_pct'] = second_up.get('winPercent', 0) or 0 # Form analysis form = runner.form or '' features['form_length'] = len(form) # Recent form (last 5) recent = form[-5:] if len(form) >= 5 else form features['recent_wins'] = sum(1 for c in recent if c == '1') features['recent_places'] = sum(1 for c in recent if c in ['1', '2', '3']) features['recent_win_pct'] = features['recent_wins'] / len(recent) if recent else 0 features['recent_place_pct'] = features['recent_places'] / len(recent) if recent else 0 # Very recent (last 3) very_recent = form[-3:] if len(form) >= 3 else form features['very_recent_wins'] = sum(1 for c in very_recent if c == '1') features['very_recent_win_pct'] = features['very_recent_wins'] / len(very_recent) if very_recent else 0 # Last start result features['last_start_pos'] = _parse_last_position(form[-1]) if form else 0 # Career prize money prize = runner.career_prize_money if prize: try: features['career_prize'] = float(str(prize).replace('$', '').replace(',', '')) except: features['career_prize'] = 0 else: features['career_prize'] = 0 # Prize money per start features['prize_per_start'] = features['career_prize'] / features['career_starts'] if features['career_starts'] > 0 else 0 # Race features features['num_runners'] = race.number_of_runners or 10 # Distance in metres distance_str = race.distance or '1200m' try: features['distance_metres'] = int(''.join(filter(str.isdigit, str(distance_str)))) except: features['distance_metres'] = 1200 # Barrier advantage (relative to field) features['barrier_central'] = abs(features['barrier'] - features['num_runners'] / 2) # Weight relative to field features['weight_relative'] = features['weight'] - 57.0 # 57kg is average return features def _parse_last_position(char: str) -> int: """Parse a form character to a finishing position.""" if char == '1': return 1 elif char == '2': return 2 elif char == '3': return 3 elif char == '4': return 4 elif char == '5': return 5 elif char == '6': return 6 elif char == '7': return 7 elif char == '8': return 8 elif char == '9': return 9 elif char == '0': return 10 # spell elif char in ['x', 'X']: return 0 # spell elif char in ['f', 'F']: return 0 # fell elif char in ['p', 'P']: return 0 # pulled up else: return 0