from __future__ import annotations import os import sys import unittest from unittest.mock import patch import pandas as pd sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) from analytics.props_mapper import map_hr_props_to_model, map_props_to_models, map_strikeout_props_to_model class TestPropsMapper(unittest.TestCase): def test_props_mapper_exposes_shared_engine_fields(self) -> None: props_df = pd.DataFrame( [ { "market": "hr", "player_name": "slugger sam", "player_name_raw": "Slugger Sam", "odds_american": 425, "sportsbook": "DraftKings", "away_team": "Away", "home_team": "Home", "commence_time": "2026-03-25T00:10:00Z", } ] ) statcast_df = pd.DataFrame( [ { "player_name": "Slugger Sam", "baseline_mode": "blended", "prior_sample_size": 260, "season_2026_sample_size": 24, "prior_weight": 0.64, "season_2026_weight": 0.36, "baseline_driver": "prior_led", "rolling_overlay_active": True, } ] ) with patch( "analytics.props_mapper.build_hr_probability_result", return_value={ "adjusted_hr_prob": 0.18, "raw_hr_prob": 0.18, "calibrated_hr_prob": 0.17, "baseline_hr_prob": 0.14, "pregame_hr_prob": 0.17, "mode": "pregame", "applied_layers": "pitcher|environment|opportunity", "skipped_layers": "live_pitch_telemetry|bullpen_transition", "confidence_score": 82.0, "confidence_bucket": "high", "confidence_reasons": ["Good batter sample"], "opportunity_hr_adjustment": 0.004, "expected_pa": 4.45, "pa_multiplier": 1.04, "lineup_slot_used": 3, "lineup_slot_source": "projected", "team_total_used": 4.8, "team_total_source": "projected", "opportunity_mode": "slot_and_total", "opportunity_reason": "pitcher_quality_missing", "pregame_pitcher_context_adj": 0.01, "pregame_park_context_adj": 0.01, "pregame_weather_context_adj": 0.0, "pregame_context_applied": True, "pitcher_hr_adjustment": 0.01, "trend_hr_adjustment": 0.0, "zone_hr_adjustment": 0.0, "family_zone_hr_adjustment": 0.0, "arsenal_hr_adjustment": 0.0, "pulled_contact_hr_adjustment": 0.0, "env_hr_adjustment": 0.01, "park_hr_adjustment": 0.01, "weather_hr_adjustment": 0.0, "platoon_hr_adjustment": 0.0, "trajectory_hr_adjustment": 0.0, "rolling_hr_adjustment": 0.0, "pitcher_reliability": 0.75, "trend_reliability": 0.60, "zone_reliability": 0.55, "family_zone_reliability": 0.55, "arsenal_reliability": 0.55, "pulled_contact_reliability": 0.65, "environment_reliability": 0.82, "trajectory_reliability": 0.70, "rolling_reliability": 0.66, "opportunity_reliability": 0.82, "matchup_platoon_multiplier": 1.0, "matchup_platoon_reason": "unknown", }, ): result = map_hr_props_to_model(props_df, statcast_df) self.assertEqual(len(result), 1) row = result.iloc[0] self.assertEqual(row["model_hr_prob_source"], "shared_pregame_engine") self.assertEqual(row["probability_mode"], "pregame") self.assertAlmostEqual(float(row["baseline_hr_prob"]), 0.14, places=6) self.assertAlmostEqual(float(row["pregame_hr_prob"]), 0.17, places=6) self.assertAlmostEqual(float(row["raw_hr_prob"]), 0.18, places=6) self.assertAlmostEqual(float(row["model_hr_prob"]), 0.17, places=6) self.assertTrue(pd.notna(row["bet_ev"])) self.assertEqual(row["confidence_bucket"], "high") self.assertTrue(str(row["model_voice"]).strip()) self.assertTrue(str(row["model_voice_primary_reason"]).strip()) self.assertIn("final_recommendation_score", result.columns) self.assertTrue(pd.notna(row["edge"])) self.assertEqual(row["baseline_mode"], "blended") self.assertEqual(int(row["prior_sample_size"]), 260) self.assertEqual(int(row["season_2026_sample_size"]), 24) self.assertAlmostEqual(float(row["prior_weight"]), 0.64, places=6) self.assertAlmostEqual(float(row["season_2026_weight"]), 0.36, places=6) self.assertEqual(row["baseline_driver"], "prior_led") self.assertTrue(bool(row["rolling_overlay_active"])) self.assertTrue(bool(row["expected_modeled_hr_row"])) self.assertTrue(bool(row["has_model_probability"])) self.assertTrue(bool(row["has_modeled_edge"])) self.assertEqual(row["model_probability_status"], "modeled_ok") self.assertFalse(bool(row["projected_starter_available"])) self.assertEqual(row["projected_starter_match_status"], "projected_starter_unavailable") def test_props_mapper_marks_missing_probability_reason_for_modeled_1plus_hr(self) -> None: props_df = pd.DataFrame( [ { "market": "hr", "market_family": "hr", "threshold": 1, "is_modeled": True, "player_name": "slugger sam", "player_name_raw": "Slugger Sam", "odds_american": 425, "sportsbook": "DraftKings", "away_team": "Away", "home_team": "Home", "commence_time": "2026-03-25T00:10:00Z", } ] ) statcast_df = pd.DataFrame( [ { "player_name": "Slugger Sam", "baseline_mode": "blended", } ] ) with patch( "analytics.props_mapper.build_hr_probability_result", return_value={ "baseline_hr_prob": 0.12, "raw_hr_prob": None, "calibrated_hr_prob": None, "pregame_hr_prob": None, "mode": "pregame", "applied_layers": "", "skipped_layers": "", "confidence_score": None, "confidence_bucket": None, "confidence_reasons": [], "pitcher_resolution_status": "pitcher_missing", "model_voice_reason_candidates": [], "model_voice_tags": [], }, ): result = map_hr_props_to_model(props_df, statcast_df) self.assertEqual(len(result), 1) row = result.iloc[0] self.assertTrue(bool(row["expected_modeled_hr_row"])) self.assertFalse(bool(row["has_model_probability"])) self.assertFalse(bool(row["has_modeled_edge"])) self.assertEqual(row["model_probability_status"], "missing_pitcher_context") def test_props_mapper_uses_probable_starters_when_pitcher_missing(self) -> None: props_df = pd.DataFrame( [ { "market": "hr", "player_name": "slugger sam", "player_name_raw": "Slugger Sam", "odds_american": 425, "sportsbook": "DraftKings", "away_team": "Away Team", "home_team": "Home Team", "commence_time": "2026-03-25T00:10:00Z", } ] ) statcast_df = pd.DataFrame( [ { "player_name": "Slugger Sam", "inning_topbot": "Top", "away_team": "Away Team", "home_team": "Home Team", } ] ) probable_starters = { ("away team", "home team"): { "home_pitcher": "Home Starter", "away_pitcher": "Away Starter", "home_pitcher_source": "oddsapi_pitcher_strikeouts_two_candidate_match", "away_pitcher_source": "statsapi_probable_pitcher", "starter_cache_source": "statsapi_plus_oddsapi_fallback", "fallback_used": True, } } with patch( "analytics.props_mapper.build_hr_probability_result", return_value={ "adjusted_hr_prob": 0.18, "raw_hr_prob": 0.18, "calibrated_hr_prob": 0.18, "baseline_hr_prob": 0.14, "pregame_hr_prob": 0.18, "mode": "pregame", "applied_layers": "pitcher", "skipped_layers": "", "confidence_score": 78.0, "confidence_bucket": "medium", "confidence_reasons": [], "opportunity_hr_adjustment": 0.0, "expected_pa": 4.3, "pa_multiplier": 1.0, "lineup_slot_used": None, "lineup_slot_source": "unknown", "team_total_used": None, "team_total_source": "unknown", "opportunity_mode": "baseline_only", "opportunity_reason": "pitcher_quality_missing", "pregame_pitcher_context_adj": 0.01, "pregame_park_context_adj": 0.0, "pregame_weather_context_adj": 0.0, "pregame_context_applied": True, "pitcher_hr_adjustment": 0.01, "trend_hr_adjustment": 0.0, "zone_hr_adjustment": 0.0, "family_zone_hr_adjustment": 0.0, "arsenal_hr_adjustment": 0.0, "pulled_contact_hr_adjustment": 0.0, "env_hr_adjustment": 0.0, "park_hr_adjustment": 0.0, "weather_hr_adjustment": 0.0, "platoon_hr_adjustment": 0.0, "trajectory_hr_adjustment": 0.0, "rolling_hr_adjustment": 0.0, "pitcher_reliability": 0.75, "trend_reliability": 0.60, "zone_reliability": 0.55, "family_zone_reliability": 0.55, "arsenal_reliability": 0.55, "pulled_contact_reliability": 0.65, "environment_reliability": 0.82, "trajectory_reliability": 0.70, "rolling_reliability": 0.66, "opportunity_reliability": 0.0, "matchup_platoon_multiplier": 1.0, "matchup_platoon_reason": "unknown", }, ) as mocked_build: result = map_hr_props_to_model( props_df, statcast_df, probable_starters=probable_starters, ) self.assertEqual(len(result), 1) self.assertEqual(result.iloc[0]["resolved_pitcher_name"], "Home Starter") self.assertEqual(result.iloc[0]["projected_home_pitcher"], "Home Starter") self.assertEqual(result.iloc[0]["projected_away_pitcher"], "Away Starter") self.assertTrue(bool(result.iloc[0]["projected_starter_available"])) self.assertEqual(result.iloc[0]["projected_home_pitcher_source"], "oddsapi_pitcher_strikeouts_two_candidate_match") self.assertEqual(result.iloc[0]["projected_away_pitcher_source"], "statsapi_probable_pitcher") self.assertEqual(result.iloc[0]["starter_cache_source"], "statsapi_plus_oddsapi_fallback") self.assertTrue(bool(result.iloc[0]["fallback_used"])) self.assertEqual(result.iloc[0]["projected_starter_match_status"], "matched_projected_home") self.assertEqual(mocked_build.call_args.kwargs["pitcher_name"], "Home Starter") def test_props_mapper_uses_statcast_inferred_lineup_slot_when_available(self) -> None: props_df = pd.DataFrame( [ { "market": "hr", "player_name": "aaron judge", "player_name_raw": "Aaron Judge", "odds_american": 245, "sportsbook": "Caesars", "away_team": "New York Yankees", "home_team": "Boston Red Sox", "commence_time": "2026-03-25T00:10:00Z", "pitcher_name": "Lefty Lou", } ] ) statcast_df = pd.DataFrame( [ { "player_name": "Aaron Judge", "source_season": 2026, "inning_topbot": "Top", "away_team": "New York Yankees", "home_team": "Boston Red Sox", "lineup_slot": 3, } ] ) pitcher_statcast_df = pd.DataFrame( [ { "player_name": "Lefty Lou", "p_throws": "L", } ] ) with patch( "analytics.props_mapper.build_hr_probability_result", return_value={ "adjusted_hr_prob": 0.18, "raw_hr_prob": 0.18, "calibrated_hr_prob": 0.18, "baseline_hr_prob": 0.14, "pregame_hr_prob": 0.18, "mode": "pregame", "applied_layers": "opportunity", "skipped_layers": "", "confidence_score": 75.0, "confidence_bucket": "medium", "confidence_reasons": [], "opportunity_hr_adjustment": 0.003, "expected_pa": 4.5, "pa_multiplier": 1.03, "lineup_slot_used": 3, "lineup_slot_source": "projected", "team_total_used": None, "team_total_source": "unknown", "opportunity_mode": "slot_only", "opportunity_reason": "pitcher_quality_missing", "pregame_pitcher_context_adj": 0.0, "pregame_park_context_adj": 0.0, "pregame_weather_context_adj": 0.0, "pregame_context_applied": True, "pitcher_hr_adjustment": 0.0, "trend_hr_adjustment": 0.0, "zone_hr_adjustment": 0.0, "family_zone_hr_adjustment": 0.0, "arsenal_hr_adjustment": 0.0, "pulled_contact_hr_adjustment": 0.0, "env_hr_adjustment": 0.0, "park_hr_adjustment": 0.0, "weather_hr_adjustment": 0.0, "platoon_hr_adjustment": 0.0, "trajectory_hr_adjustment": 0.0, "rolling_hr_adjustment": 0.0, "pitcher_reliability": 0.0, "trend_reliability": 0.0, "zone_reliability": 0.0, "family_zone_reliability": 0.0, "arsenal_reliability": 0.0, "pulled_contact_reliability": 0.0, "environment_reliability": 0.0, "trajectory_reliability": 0.0, "rolling_reliability": 0.0, "opportunity_reliability": 0.82, "matchup_platoon_multiplier": 1.0, "matchup_platoon_reason": "unknown", }, ) as mocked_build: result = map_hr_props_to_model( props_df, statcast_df, pitcher_statcast_df=pitcher_statcast_df, ) self.assertEqual(len(result), 1) self.assertEqual(result.iloc[0]["lineup_slot_source"], "projected") self.assertEqual(mocked_build.call_args.kwargs["game_row"]["lineup_slot"], 3) def test_props_mapper_prefers_row_matchup_context_for_team_and_pitcher_resolution(self) -> None: props_df = pd.DataFrame( [ { "market": "hr", "player_name": "luis arraez", "player_name_raw": "Luis Arraez", "odds_american": 1300, "sportsbook": "Caesars", "away_team": "Away Club", "home_team": "Home Club", "team": "Away Club", "commence_time": "2026-03-25T00:10:00Z", } ] ) statcast_df = pd.DataFrame( [ { "player_name": "Luis Arraez", "inning_topbot": "Bottom", "away_team": "Old Away", "home_team": "Old Home", } ] ) probable_starters = { ("away club", "home club"): { "home_pitcher": "Home Starter", "away_pitcher": "Away Starter", } } with patch( "analytics.props_mapper.build_hr_probability_result", return_value={ "adjusted_hr_prob": 0.03, "raw_hr_prob": 0.03, "calibrated_hr_prob": 0.03, "baseline_hr_prob": 0.02, "pregame_hr_prob": 0.03, "mode": "pregame", "applied_layers": "pitcher", "skipped_layers": "", "confidence_score": 65.0, "confidence_bucket": "medium", "confidence_reasons": [], "opportunity_hr_adjustment": 0.0, "expected_pa": 4.4, "pa_multiplier": 1.0, "lineup_slot_used": 1, "lineup_slot_source": "unknown", "team_total_used": None, "team_total_source": "unknown", "opportunity_mode": "slot_only", "opportunity_reason": None, "pregame_pitcher_context_adj": 0.01, "pregame_park_context_adj": 0.0, "pregame_weather_context_adj": 0.0, "pregame_context_applied": True, "pitcher_hr_adjustment": 0.01, "trend_hr_adjustment": 0.0, "zone_hr_adjustment": 0.0, "family_zone_hr_adjustment": 0.0, "arsenal_hr_adjustment": 0.0, "pulled_contact_hr_adjustment": 0.0, "env_hr_adjustment": 0.0, "park_hr_adjustment": 0.0, "weather_hr_adjustment": 0.0, "platoon_hr_adjustment": 0.0, "trajectory_hr_adjustment": 0.0, "rolling_hr_adjustment": 0.0, "pitcher_reliability": 0.80, "pitcher_resolution_status": "resolved", "trend_reliability": 0.0, "zone_reliability": 0.0, "zone_status": "missing_batter_zone_profile", "zone_store_sample_size": 0, "family_zone_reliability": 0.0, "family_zone_status": "missing_batter_family_zone_profile", "family_zone_batter_sample_size": 0, "family_zone_pitcher_sample_size": 0, "arsenal_reliability": 0.0, "arsenal_status": "missing_batter_arsenal_profile", "arsenal_batter_sample_size": 0, "arsenal_pitcher_sample_size": 0, "pulled_contact_reliability": 0.0, "environment_reliability": 0.0, "trajectory_reliability": 0.0, "rolling_reliability": 0.0, "opportunity_reliability": 0.0, "matchup_platoon_multiplier": 1.0, "matchup_platoon_reason": "unknown", "model_voice_reason_candidates": [], "model_voice_tags": [], "reason_candidate_count": 0, }, ) as mocked_build: result = map_hr_props_to_model( props_df, statcast_df, probable_starters=probable_starters, ) self.assertEqual(len(result), 1) row = result.iloc[0] self.assertEqual(row["batter_team"], "Away Club") self.assertEqual(row["batter_team_source"], "row_team") self.assertEqual(row["resolved_pitcher_name"], "Home Starter") self.assertEqual(row["resolved_pitcher_source"], "probable_starters_matchup") self.assertEqual(mocked_build.call_args.kwargs["pitcher_name"], "Home Starter") def test_props_mapper_passes_model_voice_candidates_through(self) -> None: props_df = pd.DataFrame( [ { "market": "hr", "player_name": "slugger sam", "player_name_raw": "Slugger Sam", "odds_american": 425, "sportsbook": "DraftKings", "away_team": "Away", "home_team": "Home", "commence_time": "2026-03-25T00:10:00Z", } ] ) statcast_df = pd.DataFrame([{"player_name": "Slugger Sam"}]) with patch( "analytics.props_mapper.build_hr_probability_result", return_value={ "adjusted_hr_prob": 0.18, "raw_hr_prob": 0.18, "calibrated_hr_prob": 0.17, "baseline_hr_prob": 0.14, "pregame_hr_prob": 0.17, "mode": "pregame", "applied_layers": "arsenal|pitcher", "skipped_layers": "", "confidence_score": 82.0, "confidence_bucket": "high", "confidence_reasons": ["Good batter sample"], "opportunity_hr_adjustment": 0.0, "expected_pa": 4.4, "pa_multiplier": 1.0, "lineup_slot_used": None, "lineup_slot_source": "unknown", "team_total_used": None, "team_total_source": "unknown", "opportunity_mode": None, "opportunity_reason": None, "pregame_pitcher_context_adj": 0.01, "pregame_park_context_adj": 0.0, "pregame_weather_context_adj": 0.0, "pregame_context_applied": True, "pitcher_hr_adjustment": 0.01, "trend_hr_adjustment": 0.0, "zone_hr_adjustment": 0.0, "family_zone_hr_adjustment": 0.0, "arsenal_hr_adjustment": 0.012, "pulled_contact_hr_adjustment": 0.0, "env_hr_adjustment": 0.0, "park_hr_adjustment": 0.0, "weather_hr_adjustment": 0.0, "platoon_hr_adjustment": 0.0, "trajectory_hr_adjustment": 0.0, "rolling_hr_adjustment": 0.0, "pitcher_reliability": 0.75, "pitcher_resolution_status": "resolved", "trend_reliability": 0.60, "zone_reliability": 0.55, "zone_status": "available_zero_effect", "zone_store_sample_size": 15, "family_zone_reliability": 0.55, "family_zone_status": "available_zero_effect", "family_zone_batter_sample_size": 20, "family_zone_pitcher_sample_size": 18, "arsenal_reliability": 0.55, "arsenal_status": "applied", "arsenal_batter_sample_size": 40, "arsenal_pitcher_sample_size": 55, "pulled_contact_reliability": 0.65, "environment_reliability": 0.82, "trajectory_reliability": 0.70, "rolling_reliability": 0.66, "opportunity_reliability": 0.82, "matchup_platoon_multiplier": 1.0, "matchup_platoon_reason": "unknown", "model_voice_reason_candidates": [ { "category": "arsenal", "direction": "supportive", "magnitude": 0.012, "signed_magnitude": 0.012, "template_key": "arsenal_favorable", "template_inputs": {}, } ], "model_voice_tags": ["arsenal_favorable"], "reason_candidate_count": 1, }, ): result = map_hr_props_to_model(props_df, statcast_df) self.assertEqual(len(result), 1) row = result.iloc[0] self.assertEqual(row["reason_candidate_count"], 1) self.assertEqual(row["model_voice_tags"], ["arsenal_favorable"]) self.assertIn("arsenal", str(row["model_voice"]).lower()) def test_props_mapper_matches_pitcher_aliases_across_starters_and_baseline(self) -> None: props_df = pd.DataFrame( [ { "market": "hr", "player_name": "alek thomas", "player_name_raw": "Alek Thomas", "odds_american": 450, "sportsbook": "Caesars", "away_team": "Arizona Diamondbacks", "home_team": "Los Angeles Dodgers", "commence_time": "2026-03-25T00:10:00Z", } ] ) statcast_df = pd.DataFrame( [ { "player_name": "Alek Thomas", "inning_topbot": "Top", "away_team": "Arizona Diamondbacks", "home_team": "Los Angeles Dodgers", "baseline_mode": "blended", } ] ) pitcher_statcast_df = pd.DataFrame( [ { "player_name": "Rodriguez, Eduardo", "baseline_mode": "prior_only", "prior_sample_size": 5000, "season_2026_sample_size": 0, "prior_weight": 1.0, "season_2026_weight": 0.0, "baseline_driver": "prior_led", "rolling_overlay_active": False, "p_throws": "L", } ] ) probable_starters = { ("arizona diamondbacks", "los angeles dodgers"): { "home_pitcher": "Eduardo Rodriguez", "away_pitcher": "Someone Else", } } with patch( "analytics.props_mapper.build_hr_probability_result", return_value={ "adjusted_hr_prob": 0.04, "raw_hr_prob": 0.04, "calibrated_hr_prob": 0.04, "baseline_hr_prob": 0.03, "pregame_hr_prob": 0.04, "mode": "pregame", "applied_layers": "pitcher", "skipped_layers": "", "confidence_score": 70.0, "confidence_bucket": "medium", "confidence_reasons": [], "opportunity_hr_adjustment": 0.0, "expected_pa": 4.3, "pa_multiplier": 1.0, "lineup_slot_used": None, "lineup_slot_source": "unknown", "team_total_used": None, "team_total_source": "unknown", "opportunity_mode": None, "opportunity_reason": None, "pregame_pitcher_context_adj": 0.0, "pregame_park_context_adj": 0.0, "pregame_weather_context_adj": 0.0, "pregame_context_applied": True, "pitcher_hr_adjustment": 0.0, "trend_hr_adjustment": 0.0, "zone_hr_adjustment": 0.0, "family_zone_hr_adjustment": 0.0, "arsenal_hr_adjustment": 0.0, "pulled_contact_hr_adjustment": 0.0, "env_hr_adjustment": 0.0, "park_hr_adjustment": 0.0, "weather_hr_adjustment": 0.0, "platoon_hr_adjustment": 0.0, "trajectory_hr_adjustment": 0.0, "rolling_hr_adjustment": 0.0, "pitcher_reliability": 0.8, "pitcher_resolution_status": "resolved", "trend_reliability": 0.0, "zone_reliability": 0.0, "family_zone_reliability": 0.0, "arsenal_reliability": 0.0, "pulled_contact_reliability": 0.0, "environment_reliability": 0.0, "trajectory_reliability": 0.0, "rolling_reliability": 0.0, "opportunity_reliability": 0.0, "matchup_platoon_multiplier": 1.0, "matchup_platoon_reason": "unknown", }, ): result = map_hr_props_to_model( props_df, statcast_df, pitcher_statcast_df=pitcher_statcast_df, probable_starters=probable_starters, ) row = result.iloc[0] self.assertEqual(row["resolved_pitcher_name"], "Eduardo Rodriguez") self.assertEqual(row["pitcher_baseline_mode"], "prior_only") self.assertEqual(row["projected_starter_match_status"], "matched_projected_home") def test_props_mapper_uses_current_season_statcast_team_when_row_team_is_missing(self) -> None: props_df = pd.DataFrame( [ { "market": "hr", "player_name": "andrew benintendi", "player_name_raw": "Andrew Benintendi", "odds_american": 500, "sportsbook": "BetMGM", "away_team": "Chicago White Sox", "home_team": "Toronto Blue Jays", "commence_time": "2026-03-25T00:10:00Z", } ] ) statcast_df = pd.DataFrame( [ { "player_name": "Andrew Benintendi", "inning_topbot": "Top", "away_team": "Chicago White Sox", "home_team": "Toronto Blue Jays", "source_season": 2026, } ] ) probable_starters = { ("chicago white sox", "toronto blue jays"): { "home_pitcher": "Anthony Kay", "away_pitcher": "Brandon Sproat", } } with patch( "analytics.props_mapper.build_hr_probability_result", return_value={ "adjusted_hr_prob": 0.03, "raw_hr_prob": 0.03, "calibrated_hr_prob": 0.03, "baseline_hr_prob": 0.02, "pregame_hr_prob": 0.03, "mode": "pregame", "applied_layers": "pitcher", "skipped_layers": "", "confidence_score": 68.0, "confidence_bucket": "medium", "confidence_reasons": [], "opportunity_hr_adjustment": 0.0, "expected_pa": 4.2, "pa_multiplier": 1.0, "lineup_slot_used": None, "lineup_slot_source": "unknown", "team_total_used": None, "team_total_source": "unknown", "opportunity_mode": None, "opportunity_reason": None, "pregame_pitcher_context_adj": 0.0, "pregame_park_context_adj": 0.0, "pregame_weather_context_adj": 0.0, "pregame_context_applied": True, "pitcher_hr_adjustment": 0.0, "trend_hr_adjustment": 0.0, "zone_hr_adjustment": 0.0, "family_zone_hr_adjustment": 0.0, "arsenal_hr_adjustment": 0.0, "pulled_contact_hr_adjustment": 0.0, "env_hr_adjustment": 0.0, "park_hr_adjustment": 0.0, "weather_hr_adjustment": 0.0, "platoon_hr_adjustment": 0.0, "trajectory_hr_adjustment": 0.0, "rolling_hr_adjustment": 0.0, "pitcher_reliability": 0.8, "pitcher_resolution_status": "resolved", "trend_reliability": 0.0, "zone_reliability": 0.0, "family_zone_reliability": 0.0, "arsenal_reliability": 0.0, "pulled_contact_reliability": 0.0, "environment_reliability": 0.0, "trajectory_reliability": 0.0, "rolling_reliability": 0.0, "opportunity_reliability": 0.0, "matchup_platoon_multiplier": 1.0, "matchup_platoon_reason": "unknown", }, ): result = map_hr_props_to_model( props_df, statcast_df, probable_starters=probable_starters, ) row = result.iloc[0] self.assertEqual(row["batter_team"], "Chicago White Sox") self.assertEqual(row["batter_team_source"], "current_season_statcast") self.assertEqual(row["resolved_pitcher_name"], "Anthony Kay") def test_unmodeled_hr_ladders_do_not_get_model_probability(self) -> None: props_df = pd.DataFrame( [ { "market": "hr", "market_family": "hr", "threshold": 2, "display_label": "2+ HR", "is_modeled": False, "player_name": "slugger sam", "player_name_raw": "Slugger Sam", "odds_american": 4000, "sportsbook": "Caesars", "away_team": "Away", "home_team": "Home", "commence_time": "2026-03-25T00:10:00Z", } ] ) statcast_df = pd.DataFrame([{"player_name": "Slugger Sam"}]) with patch("analytics.props_mapper.build_hr_probability_result") as mocked_build: result = map_hr_props_to_model(props_df, statcast_df) self.assertEqual(len(result), 1) row = result.iloc[0] self.assertEqual(row["model_hr_prob_source"], "unmodeled_hr_ladder") self.assertTrue(pd.isna(row["model_hr_prob"])) self.assertTrue(pd.isna(row["bet_ev"])) self.assertTrue(pd.isna(row["edge"])) mocked_build.assert_not_called() def test_strikeout_props_are_mapped_with_verdict_and_model_voice(self) -> None: props_df = pd.DataFrame( [ { "market": "k", "market_family": "k", "selection_side": "over", "selection_scope": "pitcher", "display_label": "Over 6.5 K", "player_name": "ace pitcher", "player_name_raw": "Ace Pitcher", "odds_american": 105, "line": 6.5, "sportsbook": "Caesars", "away_team": "Away Team", "home_team": "Home Team", "commence_time": "2026-03-25T00:10:00Z", } ] ) statcast_df = pd.DataFrame([{"player_name": "Slugger Sam"}]) probable_starters = { ("away team", "home team"): { "home_pitcher": "Ace Pitcher", "away_pitcher": "Other Pitcher", } } with patch( "analytics.props_mapper.build_strikeout_probability_result_v2", return_value={ "formula_version": "strikeout_v2_live", "fair_prob": 0.61, "fair_prob_v2": 0.61, "expected_strikeouts": 7.0, "expected_strikeouts_v2": 7.0, "projected_pitch_count": 94.0, "projected_batters_faced": 25.0, "projected_innings": 5.7, "pitches_per_bf": 3.76, "opportunity_confidence": 0.74, "opportunity_reasons": ["stable_starter_workload"], "projected_k_rate": 0.28, "raw_k_prob": 0.61, "calibrated_k_prob": 0.61, "raw_k_prob_v2": 0.61, "calibrated_k_prob_v2": 0.61, "confidence_score": 64.0, "confidence_score_raw": 64.0, "confidence_score_display": 64.0, "confidence_source": "strikeout_v2_live", "confidence_bucket": "medium", "confidence_reasons": ["Projected opponent lineup is incomplete"], "confidence_component_bonuses": [{"label": "Strong pitcher sample", "value": 10, "direction": "bonus"}], "confidence_component_penalties": [{"label": "Projected opponent lineup is incomplete", "value": 5, "direction": "penalty"}], "confidence_primary_driver": {"label": "Projected opponent lineup is incomplete", "value": 5, "direction": "penalty"}, "confidence_summary_label": "Projected opponent lineup is incomplete", "confidence_score_v2": 64.0, "confidence_score_raw_v2": 64.0, "confidence_score_display_v2": 64.0, "confidence_source_v2": "strikeout_v2_live", "confidence_bucket_v2": "medium", "confidence_reasons_v2": ["Projected opponent lineup is incomplete"], "confidence_component_bonuses_v2": [{"label": "Strong pitcher sample", "value": 10, "direction": "bonus"}], "confidence_component_penalties_v2": [{"label": "Projected opponent lineup is incomplete", "value": 5, "direction": "penalty"}], "confidence_primary_driver_v2": {"label": "Projected opponent lineup is incomplete", "value": 5, "direction": "penalty"}, "confidence_summary_label_v2": "Projected opponent lineup is incomplete", "k_rate_pitch_signal": 0.28, "k_rate_anchor": 0.27, "bb_rate_anchor": 0.06, "command_efficiency_signal": 0.69, "swing_miss_subscore": 0.77, "called_strike_subscore": 0.73, "command_efficiency_subscore": 0.69, "lineup_whiff_subscore": 0.58, "zone_matchup_subscore": 0.19, "family_zone_matchup_subscore": 0.22, "arsenal_fit_subscore": 0.24, "tunneling_subscore": 0.64, "release_consistency_subscore": 0.62, "sequencing_subscore": 0.66, "count_leverage_subscore": 0.58, "leash_risk_subscore": 0.34, "role_certainty_score": 0.96, "times_through_order_penalty": 0.01, "telemetry_path_status": "full_telemetry", "model_tier": "full_telemetry", "variance_band_low": 6.0, "variance_band_high": 8.0, "matchup_coverage_confidence": 0.48, "component_source_map": {"shared_composer": "ok"}, "predicted_whiff_regions": ["shadow"], "predicted_attack_regions": ["shadow"], "predicted_damage_regions": ["heart"], "tunnel_pair_scores": [{"pair": "FF/CH", "score": 0.62}], "reason_tags_for": ["Misses bats consistently", "Projected workload supports deep strikeout opportunity"], "reason_tags_against": ["Projected opponent lineup is incomplete"], }, ): result = map_props_to_models( props_df, statcast_df=statcast_df, probable_starters=probable_starters, ) self.assertEqual(len(result), 1) row = result.iloc[0] self.assertEqual(row["market"], "k") self.assertAlmostEqual(float(row["fair_prob"]), 0.61, places=6) self.assertEqual(row["selection_scope"], "pitcher") self.assertIn(row["verdict"], {"bet", "watch", "pass"}) self.assertTrue(bool(str(row["model_voice_for"]))) self.assertEqual(row["confidence_source"], "strikeout_v2_live") self.assertGreater(float(row["confidence_score"]), 64.0) self.assertEqual(row["confidence_summary_label"], "Projected opponent lineup is incomplete") self.assertTrue(bool(row["confidence_component_bonuses"])) self.assertTrue(bool(row["confidence_component_penalties"])) self.assertEqual(float(row["projected_pitch_count"]), 94.0) self.assertEqual(row["formula_version"], "strikeout_v2_live") self.assertEqual(row["model_probability_status"], "modeled_ok") self.assertTrue(bool(row["has_model_probability"])) self.assertEqual(row["telemetry_path_status"], "full_telemetry") self.assertEqual(row["model_tier"], "full_telemetry") def test_strikeout_props_reuse_probability_results_for_duplicate_books(self) -> None: props_df = pd.DataFrame( [ { "market": "k", "market_family": "k", "selection_side": "over", "selection_scope": "pitcher", "display_label": "Over 6.5 K", "player_name": "ace pitcher", "player_name_raw": "Ace Pitcher", "odds_american": 105, "line": 6.5, "sportsbook": "Caesars", "away_team": "Away Team", "home_team": "Home Team", "commence_time": "2026-03-25T00:10:00Z", }, { "market": "k", "market_family": "k", "selection_side": "over", "selection_scope": "pitcher", "display_label": "Over 6.5 K", "player_name": "ace pitcher", "player_name_raw": "Ace Pitcher", "odds_american": 102, "line": 6.5, "sportsbook": "FanDuel", "away_team": "Away Team", "home_team": "Home Team", "commence_time": "2026-03-25T00:10:00Z", }, ] ) statcast_df = pd.DataFrame([{"player_name": "Slugger Sam"}]) probable_starters = { ("away team", "home team"): { "home_pitcher": "Ace Pitcher", "away_pitcher": "Other Pitcher", } } with patch( "analytics.props_mapper.build_strikeout_probability_result_v2", return_value={ "formula_version": "strikeout_v2_live", "fair_prob": 0.61, "fair_prob_v2": 0.61, "expected_strikeouts": 7.0, "expected_strikeouts_v2": 7.0, "projected_pitch_count": 94.0, "projected_batters_faced": 25.0, "projected_innings": 5.7, "pitches_per_bf": 3.76, "opportunity_confidence": 0.74, "opportunity_reasons": ["stable_starter_workload"], "projected_k_rate": 0.28, "raw_k_prob": 0.61, "calibrated_k_prob": 0.61, "raw_k_prob_v2": 0.61, "calibrated_k_prob_v2": 0.61, "confidence_score": 64.0, "confidence_score_raw": 64.0, "confidence_score_display": 64.0, "confidence_source": "strikeout_v2_live", "confidence_bucket": "medium", "confidence_reasons": [], "confidence_component_bonuses": [], "confidence_component_penalties": [], "confidence_primary_driver": None, "confidence_summary_label": None, "confidence_score_v2": 64.0, "confidence_score_raw_v2": 64.0, "confidence_score_display_v2": 64.0, "confidence_source_v2": "strikeout_v2_live", "confidence_bucket_v2": "medium", "confidence_reasons_v2": [], "confidence_component_bonuses_v2": [], "confidence_component_penalties_v2": [], "confidence_primary_driver_v2": None, "confidence_summary_label_v2": None, "k_rate_pitch_signal": 0.28, "k_rate_anchor": 0.27, "bb_rate_anchor": 0.06, "command_efficiency_signal": 0.69, "swing_miss_subscore": 0.77, "called_strike_subscore": 0.73, "command_efficiency_subscore": 0.69, "lineup_whiff_subscore": 0.58, "zone_matchup_subscore": 0.19, "family_zone_matchup_subscore": 0.22, "arsenal_fit_subscore": 0.24, "tunneling_subscore": 0.64, "release_consistency_subscore": 0.62, "sequencing_subscore": 0.66, "count_leverage_subscore": 0.58, "leash_risk_subscore": 0.34, "role_certainty_score": 0.96, "times_through_order_penalty": 0.01, "telemetry_path_status": "full_telemetry", "model_tier": "full_telemetry", "variance_band_low": 6.0, "variance_band_high": 8.0, "matchup_coverage_confidence": 0.48, "component_source_map": {"shared_composer": "ok"}, "predicted_whiff_regions": ["shadow"], "predicted_attack_regions": ["shadow"], "predicted_damage_regions": ["heart"], "tunnel_pair_scores": [{"pair": "FF/CH", "score": 0.62}], "reason_tags_for": ["Misses bats consistently"], "reason_tags_against": [], }, ) as mocked_build: result = map_props_to_models( props_df, statcast_df=statcast_df, probable_starters=probable_starters, ) self.assertEqual(len(result), 2) self.assertEqual(mocked_build.call_count, 1) if __name__ == "__main__": unittest.main()