from __future__ import annotations import os import sys import unittest import pandas as pd sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) from analytics.props_view_model import ( build_best_on_slate_df, build_best_on_slate_summary, build_featured_hr_props_df, build_game_player_props_map, build_games_summary_df, build_hr_props_view_model, build_player_prop_detail_map, select_best_lines_per_prop, ) class TestPropsViewModel(unittest.TestCase): def _sample_mapped_df(self) -> pd.DataFrame: return pd.DataFrame( [ { "event_id": "evt-1", "away_team": "Yankees", "home_team": "Giants", "commence_time": "2026-03-25T00:05:00Z", "player_name": "aaron judge", "player_name_raw": "Aaron Judge", "sportsbook": "Caesars", "market": "hr", "market_family": "hr", "threshold": 1, "display_label": "1+ HR", "is_modeled": True, "has_model_probability": True, "odds_american": 245, "model_hr_prob": 0.19, "bet_ev": 0.041, "confidence_score": 84.0, "edge": 0.035, "final_recommendation_score": 0.91, "player_event_market_key": "evt-1|aaron judge|hr|1", }, { "event_id": "evt-1", "away_team": "Yankees", "home_team": "Giants", "commence_time": "2026-03-25T00:05:00Z", "player_name": "aaron judge", "player_name_raw": "Aaron Judge", "sportsbook": "FanDuel", "market": "hr", "market_family": "hr", "threshold": 1, "display_label": "1+ HR", "is_modeled": True, "has_model_probability": True, "odds_american": 230, "model_hr_prob": 0.19, "bet_ev": 0.036, "confidence_score": 80.0, "edge": 0.029, "final_recommendation_score": 0.83, "player_event_market_key": "evt-1|aaron judge|hr|1", }, { "event_id": "evt-1", "away_team": "Yankees", "home_team": "Giants", "commence_time": "2026-03-25T00:05:00Z", "player_name": "aaron judge", "player_name_raw": "Aaron Judge", "sportsbook": "Caesars", "market": "hr", "market_family": "hr", "threshold": 2, "display_label": "2+ HR", "is_modeled": False, "has_model_probability": False, "odds_american": 1750, "model_hr_prob": None, "edge": None, "player_event_market_key": "evt-1|aaron judge|hr|2", }, { "event_id": "evt-1", "away_team": "Yankees", "home_team": "Giants", "commence_time": "2026-03-25T00:05:00Z", "player_name": "giancarlo stanton", "player_name_raw": "Giancarlo Stanton", "sportsbook": "Caesars", "market": "hr", "market_family": "hr", "threshold": 1, "display_label": "1+ HR", "is_modeled": True, "has_model_probability": True, "odds_american": 390, "model_hr_prob": 0.16, "bet_ev": 0.068, "confidence_score": 77.0, "edge": 0.041, "final_recommendation_score": 0.95, "player_event_market_key": "evt-1|giancarlo stanton|hr|1", }, { "event_id": "evt-2", "away_team": "Mets", "home_team": "Cubs", "commence_time": "2026-03-25T01:10:00Z", "player_name": "pete alonso", "player_name_raw": "Pete Alonso", "sportsbook": "Caesars", "market": "hr", "market_family": "hr", "threshold": 1, "display_label": "1+ HR", "is_modeled": True, "has_model_probability": True, "odds_american": 310, "model_hr_prob": 0.15, "bet_ev": 0.022, "confidence_score": 73.0, "edge": 0.024, "final_recommendation_score": 0.78, "player_event_market_key": "evt-2|pete alonso|hr|1", }, ] ) def _sample_mapped_df_with_missing_primary_prob(self) -> pd.DataFrame: df = self._sample_mapped_df() mask = df["player_name"] == "pete alonso" df.loc[mask, "has_model_probability"] = False df.loc[mask, "model_hr_prob"] = None df.loc[mask, "edge"] = None df.loc[mask, "bet_ev"] = None df.loc[mask, "model_probability_status"] = "missing_baseline" return df def _sample_cross_market_df(self) -> pd.DataFrame: base = self._sample_mapped_df() strikeout_rows = pd.DataFrame( [ { "event_id": "evt-3", "away_team": "Pirates", "home_team": "Mets", "commence_time": "2026-03-25T17:15:00Z", "player_name": "paul skenes", "player_name_raw": "Paul Skenes", "sportsbook": "DraftKings", "market": "k", "market_family": "k", "display_label": "Over 7.5 Ks", "selection_side": "over", "line": 7.5, "is_modeled": True, "odds_american": 110, "fair_prob": 0.58, "bet_ev": 0.081, "confidence_score": 76.0, "edge": 0.034, "final_recommendation_score": 0.88, "player_event_market_key": "evt-3|paul skenes|k|7.5|over", }, { "event_id": "evt-3", "away_team": "Pirates", "home_team": "Mets", "commence_time": "2026-03-25T17:15:00Z", "player_name": "paul skenes", "player_name_raw": "Paul Skenes", "sportsbook": "FanDuel", "market": "k", "market_family": "k", "display_label": "Over 7.5 Ks", "selection_side": "over", "line": 7.5, "is_modeled": True, "odds_american": 102, "fair_prob": 0.58, "bet_ev": 0.063, "confidence_score": 74.0, "edge": 0.027, "final_recommendation_score": 0.80, "player_event_market_key": "evt-3|paul skenes|k|7.5|over", }, ] ) return pd.concat([base, strikeout_rows], ignore_index=True, sort=False) def test_featured_props_only_use_modeled_primary_rows(self) -> None: featured = build_featured_hr_props_df(self._sample_mapped_df(), limit=5) self.assertEqual(len(featured), 3) self.assertEqual(featured.iloc[0]["player_name"], "giancarlo stanton") self.assertTrue((featured["threshold"] == 1).all()) self.assertTrue(featured["is_modeled"].all()) self.assertNotIn("2+ HR", featured["display_label"].tolist()) judge_row = featured[featured["player_name"] == "aaron judge"].iloc[0] self.assertEqual(judge_row["sportsbook"], "Caesars") self.assertEqual(int(judge_row["odds_american"]), 245) def test_featured_props_excludes_primary_rows_missing_probability(self) -> None: featured = build_featured_hr_props_df(self._sample_mapped_df_with_missing_primary_prob(), limit=5) self.assertNotIn("pete alonso", featured["player_name"].tolist()) def test_games_summary_tracks_modeled_props_and_top_edge(self) -> None: summary = build_games_summary_df(self._sample_mapped_df()) self.assertEqual(len(summary), 2) first_game = summary.iloc[0] self.assertEqual(first_game["event_id"], "evt-1") self.assertEqual(int(first_game["modeled_props_count"]), 2) self.assertEqual(first_game["top_player_name"], "giancarlo stanton") self.assertAlmostEqual(float(first_game["best_edge"]), 0.041, places=6) def test_games_summary_excludes_primary_rows_without_probability(self) -> None: summary = build_games_summary_df(self._sample_mapped_df_with_missing_primary_prob()) self.assertNotIn("evt-2", summary["event_id"].tolist()) def test_player_detail_map_separates_primary_and_alt_rows(self) -> None: detail_map = build_player_prop_detail_map(self._sample_mapped_df()) judge = detail_map["evt-1|aaron judge"] self.assertTrue(judge["has_modeled_row"]) self.assertTrue(judge["has_alt_ladders"]) self.assertEqual(len(judge["primary_rows"]), 2) self.assertEqual(len(judge["alt_rows"]), 1) self.assertEqual(judge["best_primary_row"]["display_label"], "1+ HR") self.assertEqual(judge["best_book"], "Caesars") self.assertAlmostEqual(float(judge["best_bet_ev"]), 0.041, places=6) def test_game_player_map_groups_players_under_each_game(self) -> None: game_map = build_game_player_props_map(self._sample_mapped_df()) self.assertEqual(set(game_map.keys()), {"evt-1", "evt-2"}) yankees_giants = game_map["evt-1"] self.assertEqual(yankees_giants["top_player_name"], "giancarlo stanton") self.assertEqual(len(yankees_giants["players"]), 2) first_player = yankees_giants["players"][0] self.assertEqual(first_player["player_name"], "giancarlo stanton") self.assertFalse(first_player["has_alt_ladders"]) def test_full_view_model_returns_all_sections(self) -> None: vm = build_hr_props_view_model(self._sample_mapped_df(), featured_limit=2) self.assertEqual(set(vm.keys()), { "featured_props_df", "best_on_slate_df", "best_on_slate_summary", "games_summary_df", "game_player_props_map", "player_prop_detail_map", }) self.assertEqual(len(vm["featured_props_df"]), 2) self.assertEqual(len(vm["games_summary_df"]), 2) self.assertIn("evt-1", vm["game_player_props_map"]) def test_select_best_lines_per_prop_is_threshold_aware(self) -> None: best = select_best_lines_per_prop(self._sample_mapped_df()) self.assertEqual(len(best), 4) judge_primary = best[(best["player_name"] == "aaron judge") & (best["threshold"] == 1)].iloc[0] judge_alt = best[(best["player_name"] == "aaron judge") & (best["threshold"] == 2)].iloc[0] self.assertEqual(judge_primary["sportsbook"], "Caesars") self.assertEqual(int(judge_primary["odds_american"]), 245) self.assertEqual(judge_alt["sportsbook"], "Caesars") self.assertEqual(int(judge_alt["odds_american"]), 1750) def test_best_on_slate_includes_hr_and_strikeouts_after_best_line_reduction(self) -> None: best = build_best_on_slate_df(self._sample_cross_market_df(), limit=10) self.assertEqual(set(best["market_family"].tolist()), {"hr", "k"}) skenes = best[best["player_name"] == "paul skenes"].iloc[0] self.assertEqual(skenes["sportsbook"], "DraftKings") self.assertEqual(float(best.iloc[0]["bet_ev"]), 0.081) def test_best_on_slate_summary_counts_modes_and_books(self) -> None: summary = build_best_on_slate_summary(self._sample_cross_market_df()) self.assertEqual(int(summary["modeled_props_count"]), 4) self.assertEqual(int(summary["sportsbooks_count"]), 2) self.assertEqual(int(summary["markets_count"]), 2) self.assertAlmostEqual(float(summary["best_ev"]), 0.081, places=6) def test_full_view_model_preserves_full_slate_summary_when_featured_is_limited(self) -> None: vm = build_hr_props_view_model(self._sample_cross_market_df(), featured_limit=2) self.assertEqual(len(vm["best_on_slate_df"]), 2) self.assertEqual(int(vm["best_on_slate_summary"]["modeled_props_count"]), 4) self.assertEqual(int(vm["best_on_slate_summary"]["markets_count"]), 2) def test_game_player_map_excludes_games_with_zero_modeled_primary_props(self) -> None: game_map = build_game_player_props_map(self._sample_mapped_df_with_missing_primary_prob()) self.assertNotIn("evt-2", game_map) if __name__ == "__main__": unittest.main()