Capstone Project 3: Season-Long Betting Simulation Challenge
Project Overview
This capstone project is fundamentally different from the first two. Rather than building software, you will manage a simulated $10,000 bankroll through a complete historical sports season, making real betting decisions week by week without access to future results. The simulation framework will present you with that week's available odds, accept your bet slips, grade your bets against actual outcomes, and track your bankroll and performance metrics over time.
This project tests whether you can apply the full stack of knowledge from this textbook under realistic conditions: incomplete information, time pressure, psychological challenges, variance, and the need for consistent discipline across months of betting. Building a model is necessary but not sufficient. You must also demonstrate sound bankroll management (Chapters 4 and 14), value betting discipline (Chapter 13), emotional control through inevitable drawdowns (Chapter 36), systematic record-keeping (Chapter 37), and honest self-assessment (Chapter 41).
The simulation uses one complete historical season of real game results and actual closing odds. You will not know which season until the simulation begins, preventing any advantage from memory of outcomes. Your task is to apply your models and strategies as if you were betting in real time, submitting your bet slips before each week's results are revealed.
Learning Objectives
Upon completing this project, you will be able to:
- Develop and document a comprehensive pre-season betting strategy covering multiple sports and market types.
- Make disciplined betting decisions under uncertainty, balancing model output with market information.
- Manage a bankroll through extended winning streaks and drawdowns without deviating from strategy.
- Track, analyze, and attribute betting performance using professional-grade metrics.
- Conduct a rigorous mid-season review and make evidence-based strategy adjustments.
- Write a professional post-season retrospective that honestly evaluates both process and results.
- Distinguish between skill and variance in your own performance.
Rules and Constraints
Bankroll Rules
- Starting bankroll: $10,000 exactly. No additional deposits permitted.
- Minimum bet size: $25 per wager.
- Maximum bet size: $500 per wager (5% of initial bankroll). This hard cap applies regardless of current bankroll level.
- Maximum daily exposure: $2,000 in total open bets on any single day.
- Maximum weekly exposure: $4,000 in total bets placed during a single simulation week.
- If bankroll reaches $0: The simulation is over. You must document what happened and analyze why.
- If bankroll exceeds $25,000: Congratulations, but continue betting -- the full season must be completed for grading.
Betting Requirements
- Minimum total bets: 200 bets over the full season. This ensures you are actively participating, not sitting on the sideline waiting for one perfect spot.
- Multi-sport requirement: You must place bets on at least 3 different sports during the season. At least 20% of your total bets must come from a sport other than your primary sport.
- Market type diversity: You must bet on at least 3 different market types (e.g., spreads, moneylines, totals, props, futures). At least 10% of bets must come from a non-primary market.
- Bet types available: Moneylines, point spreads, totals (over/under), first-half lines, futures, and selected player props as provided in the data.
- No parlays or teasers unless you specifically model the correlation between legs (as described in Chapter 14, Section 14.3). If you include parlays, you must document your correlation methodology.
- Line shopping: You will receive odds from three simulated sportsbooks. You must place each bet at the book offering the best available odds for that selection.
- No retroactive bets: All bets must be submitted before the simulation reveals that week's results. The framework enforces this constraint programmatically.
What You Receive Each Week
The simulation framework provides:
- Game schedule: All games for the upcoming week across NFL, NBA, MLB, NHL, and selected soccer leagues.
- Odds from three sportsbooks: Moneylines, spreads, and totals for every game. Lines may differ between books by 0.5--2 points on spreads and 5--15 cents on moneylines, simulating real market variation.
- Basic statistics: Team records, recent results (last 5 games), and season-level summary statistics for each team.
- Injury reports: Key injuries listed with status (Out, Doubtful, Questionable).
- Prior weeks' results: All results and your personal bet history are available for review.
You may use any external tools, models, or analysis to make your decisions, but you may not look up the actual historical results for the season being simulated. The honor system applies; additionally, the grading rubric rewards process quality and analysis depth, not raw results. A student who loses money but demonstrates excellent process, analysis, and self-awareness will outscore a student who profits through undocumented or incoherent methods.
Data Provided: Historical Season Package
The simulation framework generates a complete season of data drawn from actual historical results. The data package includes:
Provided Data Files
| File | Contents | Format |
|---|---|---|
schedule.csv |
Full season schedule across all sports | game_id, date, sport, home_team, away_team, week_number |
odds_book_a.csv |
Odds from Sportsbook A for all games | game_id, home_ml, away_ml, spread, home_spread_odds, away_spread_odds, total, over_odds, under_odds |
odds_book_b.csv |
Odds from Sportsbook B | Same format as Book A |
odds_book_c.csv |
Odds from Sportsbook C | Same format as Book A |
team_stats.csv |
Season-to-date team statistics | Updated weekly; team, sport, wins, losses, points_for, points_against, recent_form |
injuries.csv |
Weekly injury reports | week, team, player, position, status |
results.csv |
Game results (revealed week by week) | game_id, home_score, away_score, winner |
Data Generation Script
The following Python script generates the simulation data package from historical records. The instructor runs this script once to produce the sealed season data; students receive the data one week at a time through the simulation framework.
"""
simulation_data_generator.py
Generates a complete season simulation package from historical data.
The instructor runs this script to produce the season data.
Students do NOT run this script -- they interact with the simulation
framework (phase2_simulation.py) which reveals data week by week.
"""
import pandas as pd
import numpy as np
import sqlite3
import json
import hashlib
from pathlib import Path
from typing import Dict, List, Tuple, Optional
from datetime import datetime
import random
class SeasonDataGenerator:
"""
Generates a complete multi-sport season data package for simulation.
Design:
- Draws from actual historical results to ensure realism
- Packages data into weekly chunks for sequential revelation
- Adds simulated multi-book odds with realistic variation
- Includes injury data and team statistics
"""
def __init__(self, seed: int = None):
"""
Parameters:
- seed: Random seed for reproducibility. Different seeds produce
different season selections.
"""
self.rng = np.random.RandomState(seed)
self.season_data = {}
def generate_nfl_season(self, source_db: str,
season_year: int) -> pd.DataFrame:
"""
Extract one NFL season from the nfl_data_py database.
Returns DataFrame with: game_id, week, date, home_team, away_team,
home_score, away_score, spread_line, total_line, home_ml, away_ml
"""
import nfl_data_py as nfl
schedules = nfl.import_schedules([season_year])
games = schedules[schedules["game_type"] == "REG"][[
"game_id", "week", "gameday", "home_team", "away_team",
"home_score", "away_score", "spread_line", "total_line",
"home_moneyline", "away_moneyline"
]].copy()
games.rename(columns={
"gameday": "date",
"home_moneyline": "home_ml",
"away_moneyline": "away_ml"
}, inplace=True)
games["sport"] = "NFL"
games["home_margin"] = games["home_score"] - games["away_score"]
games["total_score"] = games["home_score"] + games["away_score"]
return games
def generate_nba_season(self, season_year: int) -> pd.DataFrame:
"""
Generate NBA season data. In production, this would pull from
basketball-reference or the NBA API. For the simulation, we
provide a pre-processed CSV.
NBA seasons run October-April with ~1230 regular season games.
"""
# Placeholder: load from pre-processed source
# In the actual implementation, this reads from a curated database
pass
def generate_mlb_season(self, season_year: int) -> pd.DataFrame:
"""Generate MLB season data (April-September, ~2430 games)."""
pass
def generate_multi_book_odds(self, true_odds: pd.DataFrame,
n_books: int = 3) -> Dict[str, pd.DataFrame]:
"""
Generate simulated odds from multiple sportsbooks by adding
realistic noise to the true closing lines.
The variation between books follows the patterns described in
Chapter 12 (Line Shopping):
- Spreads vary by 0 to 1 point across books
- Totals vary by 0 to 1 point
- Moneylines vary by 5 to 20 cents
- Juice varies from -105 to -115 on spreads
This simulates real market conditions where different books
have different positions and customer bases.
"""
books = {}
for i in range(n_books):
book_name = f"book_{chr(65 + i)}" # book_A, book_B, book_C
book_odds = true_odds.copy()
# Spread variation: +/- 0.5 with some probability
spread_adjustment = self.rng.choice(
[-0.5, 0, 0, 0, 0.5],
size=len(book_odds)
)
book_odds["spread"] = book_odds["spread_line"] + spread_adjustment
# Total variation
total_adjustment = self.rng.choice(
[-0.5, 0, 0, 0, 0.5],
size=len(book_odds)
)
book_odds["total"] = book_odds["total_line"] + total_adjustment
# Juice variation on spreads (-105 to -115)
juice_home = self.rng.choice(
[-105, -108, -110, -110, -110, -112, -115],
size=len(book_odds)
)
juice_away = self._complementary_juice(juice_home)
book_odds["home_spread_odds"] = juice_home
book_odds["away_spread_odds"] = juice_away
# Total juice variation
book_odds["over_odds"] = self.rng.choice(
[-105, -108, -110, -110, -110, -112, -115],
size=len(book_odds)
)
book_odds["under_odds"] = self._complementary_juice(
book_odds["over_odds"].values
)
# Moneyline variation (add noise proportional to odds magnitude)
ml_noise = self.rng.randint(-10, 11, size=len(book_odds))
book_odds["home_ml"] = book_odds["home_ml"] + ml_noise
book_odds["away_ml"] = book_odds["away_ml"] - ml_noise
# Ensure moneyline consistency (no +/+ or -/- for both sides)
# and that moneylines have appropriate vig
book_odds = self._fix_moneylines(book_odds)
books[book_name] = book_odds[[
"game_id", "home_ml", "away_ml", "spread",
"home_spread_odds", "away_spread_odds",
"total", "over_odds", "under_odds"
]]
return books
def _complementary_juice(self, juice_array: np.ndarray) -> np.ndarray:
"""
Given juice on one side, compute the complementary juice
so that the total vig is approximately 20 cents (the standard).
If one side is -110, the other is -110 (20 cents total vig).
If one side is -105, the other is approximately -115.
"""
result = np.zeros_like(juice_array)
for i, j in enumerate(juice_array):
# Total implied probability should be approximately 1.04-1.05
impl_this = abs(j) / (abs(j) + 100) if j < 0 else 100 / (j + 100)
target_total = 1.04 + self.rng.uniform(0, 0.02)
impl_other = target_total - impl_this
# Convert back to American odds
if impl_other > 0.5:
result[i] = int(-100 * impl_other / (1 - impl_other))
else:
result[i] = int(100 * (1 - impl_other) / impl_other)
return result.astype(int)
def _fix_moneylines(self, df: pd.DataFrame) -> pd.DataFrame:
"""Ensure moneyline odds are valid and have appropriate vig."""
for idx in df.index:
home_ml = df.loc[idx, "home_ml"]
away_ml = df.loc[idx, "away_ml"]
# Both sides should not be positive (no arb by default)
# At least one should be negative in most cases
if home_ml > 0 and away_ml > 0:
# Make the slight favorite negative
if home_ml < away_ml:
df.loc[idx, "home_ml"] = -home_ml
else:
df.loc[idx, "away_ml"] = -away_ml
# Ensure minimum vig (total implied prob >= 1.02)
h_prob = abs(home_ml) / (abs(home_ml) + 100) if home_ml < 0 else 100 / (home_ml + 100)
a_prob = abs(away_ml) / (abs(away_ml) + 100) if away_ml < 0 else 100 / (away_ml + 100)
if h_prob + a_prob < 1.02:
# Add vig by moving both lines slightly
df.loc[idx, "home_ml"] = int(df.loc[idx, "home_ml"] * 0.95)
df.loc[idx, "away_ml"] = int(df.loc[idx, "away_ml"] * 0.95)
return df
def generate_weekly_team_stats(self, results: pd.DataFrame,
week: int) -> pd.DataFrame:
"""
Generate team statistics as they would be known through the
specified week. Only includes results from completed weeks.
This prevents any look-ahead information from leaking into
the data (critical constraint, see Chapter 30 on backtesting).
"""
completed = results[results["week"] <= week].copy()
stats = []
teams = set(completed["home_team"]).union(set(completed["away_team"]))
for team in teams:
home_games = completed[completed["home_team"] == team]
away_games = completed[completed["away_team"] == team]
wins = (
(home_games["home_score"] > home_games["away_score"]).sum() +
(away_games["away_score"] > away_games["home_score"]).sum()
)
losses = (
(home_games["home_score"] < home_games["away_score"]).sum() +
(away_games["away_score"] < away_games["home_score"]).sum()
)
ties = (
(home_games["home_score"] == home_games["away_score"]).sum() +
(away_games["away_score"] == away_games["home_score"]).sum()
)
points_for = (
home_games["home_score"].sum() +
away_games["away_score"].sum()
)
points_against = (
home_games["away_score"].sum() +
away_games["home_score"].sum()
)
games_played = wins + losses + ties
# Last 5 games form
all_team = pd.concat([
home_games.assign(
team_score=home_games["home_score"],
opp_score=home_games["away_score"]
),
away_games.assign(
team_score=away_games["away_score"],
opp_score=away_games["home_score"]
)
]).sort_values("week").tail(5)
recent_wins = (all_team["team_score"] > all_team["opp_score"]).sum()
recent_form = f"{recent_wins}-{len(all_team) - recent_wins}"
stats.append({
"team": team,
"sport": completed[
(completed["home_team"] == team) |
(completed["away_team"] == team)
]["sport"].iloc[0],
"week": week,
"wins": wins,
"losses": losses,
"ties": ties,
"games_played": games_played,
"points_for": points_for,
"points_against": points_against,
"ppg": round(points_for / max(games_played, 1), 1),
"papg": round(points_against / max(games_played, 1), 1),
"point_diff": points_for - points_against,
"recent_form": recent_form,
})
return pd.DataFrame(stats)
def package_season(self, output_dir: str = "simulation_data"):
"""
Package all season data into the simulation directory structure.
Creates weekly subdirectories with appropriate data files.
"""
out = Path(output_dir)
out.mkdir(parents=True, exist_ok=True)
# Write full schedule (game times revealed, results hidden)
# Write odds files for each book
# Write initial team stats
# Write weekly result files (sealed, revealed one at a time)
print(f"Season package written to {out}")
print(f"Total games: {len(self.season_data.get('schedule', []))}")
print("Ready for simulation.")
Phase 1: Pre-Season Analysis and Strategy Development
Duration: Week 1 (of simulation, which maps to 1 week of real time)
Relevant chapters: Chapter 4 (Bankroll Management), Chapter 13 (Value Betting), Chapter 14 (Advanced Bankroll), Chapter 15--22 (Sport-Specific Modeling), Chapter 36 (Psychology)
Before the simulation begins, you must submit a written Pre-Season Strategy Document (3--5 pages) that establishes your approach. This document will be compared to your actual behavior during the simulation.
1.1 Required Strategy Document Contents
Section A: Overall Philosophy (1 page) - What is your betting philosophy? (e.g., model-driven, market-driven, hybrid) - Which sports and markets will you prioritize and why? - What is your edge hypothesis? Where do you believe inefficiencies exist? - How will you balance expected value against variance? - Reference the value betting framework from Chapter 13.
Section B: Bankroll Management Plan (1 page) - What staking method will you use? (Flat bet, percentage, Kelly, fractional Kelly) - Justify your choice using the analysis from Chapter 4 and Chapter 14. - What is your standard unit size? - What are your maximum bet size rules? - Under what conditions will you adjust your unit size? - What are your stop-loss rules? (Daily, weekly, monthly) - How will you handle a 20% drawdown? A 40% drawdown?
Section C: Model Description (1--2 pages) - What predictive model(s) will you use? - What data inputs drive your model? - How do you estimate probabilities? - How do you identify value relative to the market? - What are your minimum edge thresholds for placing a bet? - What is your process for each sport you plan to bet? - Reference specific techniques from Chapters 9, 15--22, 26, and 27 as appropriate.
Section D: Process and Discipline (0.5 page) - Describe your weekly workflow: how will you analyze games, select bets, and record decisions? - What safeguards do you have against tilt and emotional betting (Chapter 36)? - How will you track your performance (Chapter 37)?
1.2 Pre-Season Model Setup
Before the simulation begins, you should build or calibrate your predictive models. You may use any combination of the following approaches (reference the appropriate chapters):
| Approach | Description | Chapter |
|---|---|---|
| Elo ratings | Rating system updated through prior seasons | Ch 26 |
| Power ratings | Regression-based team strength estimates | Ch 9, 26 |
| EPA/efficiency models | Sport-specific efficiency metrics | Ch 15--22 |
| Machine learning | XGBoost, neural nets, etc. | Ch 27, 29 |
| Market-based | Use sharp lines as truth, exploit soft books | Ch 11, 12 |
| Hybrid | Combine model probabilities with market information | Ch 13, 41 |
You may also enter the simulation with no formal model and rely on qualitative analysis, but your documentation must explain your reasoning framework. Note that the grading rubric heavily weights process quality, so a well-documented qualitative approach can score well.
Phase 2: Monthly Simulation Rounds
Duration: Weeks 2--7 (the season unfolds over approximately 6 simulation rounds)
Relevant chapters: Chapter 3 (Expected Value), Chapter 12 (Line Shopping), Chapter 13 (Value Betting), Chapter 14 (Staking), Chapter 37 (Record-Keeping)
2.1 Weekly Betting Process
Each simulation week, you will:
- Receive the weekly data package: Schedule, odds from three books, team stats, and injury reports for upcoming games.
- Analyze the games: Apply your models, identify value opportunities, and compare to market odds.
- Submit your bet slip: A structured CSV file listing every bet you wish to place.
- Receive results: After submission, the framework reveals that week's outcomes and grades your bets.
2.2 Bet Slip Format
Each weekly bet slip is a CSV file with the following columns:
game_id,bet_type,selection,sportsbook,odds,stake,model_probability,reasoning
| Column | Description | Example |
|---|---|---|
| game_id | Unique identifier for the game | NFL_2023_W05_KC_MIN |
| bet_type | Market type | "spread", "moneyline", "total" |
| selection | What you are betting on | "KC -3", "MIN +3", "Over 48.5", "MIN ML" |
| sportsbook | Which book you are placing at | "book_A", "book_B", "book_C" |
| odds | American odds at that book | -110, +155 |
| stake | Dollar amount wagered | 100.00 |
| model_probability | Your estimated probability of winning | 0.55 |
| reasoning | Brief explanation (required for every bet) | "EPA model gives KC 3.8 edge; market at 3.0 is 0.8 pts of value" |
2.3 Simulation Framework
The following code implements the simulation engine that manages the season simulation, accepts bet slips, grades bets, and tracks performance.
"""
phase2_simulation_framework.py
Season-long betting simulation engine.
This framework manages the simulation lifecycle:
1. Loads the sealed season data
2. Reveals data one week at a time
3. Accepts bet slips and validates them against rules
4. Grades bets against actual results
5. Tracks bankroll and performance metrics
6. Produces weekly and season-level reports
"""
import pandas as pd
import numpy as np
import json
import csv
from pathlib import Path
from typing import Dict, List, Tuple, Optional
from dataclasses import dataclass, field, asdict
from datetime import datetime
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Configuration and Constants
# ---------------------------------------------------------------------------
INITIAL_BANKROLL = 10000.0
MIN_BET = 25.0
MAX_BET = 500.0
MAX_DAILY_EXPOSURE = 2000.0
MAX_WEEKLY_EXPOSURE = 4000.0
MIN_TOTAL_BETS = 200
MIN_SPORTS = 3
MIN_SECONDARY_SPORT_PCT = 0.20
MIN_MARKET_TYPES = 3
MIN_SECONDARY_MARKET_PCT = 0.10
@dataclass
class BetSlip:
"""A single bet submission."""
game_id: str
bet_type: str
selection: str
sportsbook: str
odds: int
stake: float
model_probability: float
reasoning: str
@dataclass
class GradedBet:
"""A bet after grading against actual results."""
game_id: str
bet_type: str
selection: str
sportsbook: str
odds: int
stake: float
model_probability: float
reasoning: str
week: int
sport: str
result: str # "win", "loss", "push"
profit: float
home_score: int
away_score: int
closing_line: float
@dataclass
class WeeklyReport:
"""Summary of one week's betting activity."""
week: int
bets_placed: int
bets_won: int
bets_lost: int
bets_pushed: int
total_staked: float
total_profit: float
weekly_roi: float
bankroll_start: float
bankroll_end: float
best_bet: str
worst_bet: str
@dataclass
class SimulationState:
"""Tracks the complete state of the simulation."""
current_week: int = 0
bankroll: float = INITIAL_BANKROLL
total_bets: int = 0
total_profit: float = 0.0
all_bets: List[GradedBet] = field(default_factory=list)
weekly_reports: List[WeeklyReport] = field(default_factory=list)
peak_bankroll: float = INITIAL_BANKROLL
max_drawdown: float = 0.0
sports_bet_on: Dict[str, int] = field(default_factory=dict)
markets_bet_on: Dict[str, int] = field(default_factory=dict)
# ---------------------------------------------------------------------------
# Simulation Engine
# ---------------------------------------------------------------------------
class SeasonSimulator:
"""
Manages the season-long betting simulation.
Lifecycle:
1. Initialize with sealed season data
2. For each week:
a. Reveal that week's schedule, odds, stats, and injuries
b. Accept bet slip from the student
c. Validate bets against rules
d. Grade bets against actual results
e. Update bankroll and metrics
f. Generate weekly report
3. After final week, generate season summary
"""
def __init__(self, data_dir: str = "simulation_data"):
self.data_dir = Path(data_dir)
self.state = SimulationState()
# Load season data
self.schedule = pd.read_csv(self.data_dir / "schedule.csv")
self.results = pd.read_csv(self.data_dir / "results.csv")
self.books = {
"book_A": pd.read_csv(self.data_dir / "odds_book_a.csv"),
"book_B": pd.read_csv(self.data_dir / "odds_book_b.csv"),
"book_C": pd.read_csv(self.data_dir / "odds_book_c.csv"),
}
self.injuries = pd.read_csv(self.data_dir / "injuries.csv")
self.total_weeks = self.schedule["week_number"].max()
logger.info(f"Simulation loaded: {len(self.schedule)} games "
f"across {self.total_weeks} weeks")
def get_week_data(self, week: int) -> Dict:
"""
Reveal data for the specified week.
Only returns information that would be available BEFORE
the week's games are played.
"""
if week > self.total_weeks:
raise ValueError(f"Week {week} exceeds season length "
f"({self.total_weeks})")
week_schedule = self.schedule[
self.schedule["week_number"] == week
].copy()
# Odds for this week's games
week_game_ids = set(week_schedule["game_id"])
week_odds = {}
for book_name, book_df in self.books.items():
week_odds[book_name] = book_df[
book_df["game_id"].isin(week_game_ids)
].copy()
# Injuries for this week
week_injuries = self.injuries[
self.injuries["week"] == week
].copy()
# Team stats through PREVIOUS week (no current week info)
# This is loaded from pre-computed stats
stats_file = self.data_dir / f"team_stats_week_{week - 1}.csv"
if stats_file.exists():
team_stats = pd.read_csv(stats_file)
else:
team_stats = pd.DataFrame()
# Prior results (all completed weeks)
prior_results = self.results[
self.results["game_id"].isin(
self.schedule[self.schedule["week_number"] < week]["game_id"]
)
].copy()
return {
"week": week,
"schedule": week_schedule,
"odds": week_odds,
"injuries": week_injuries,
"team_stats": team_stats,
"prior_results": prior_results,
"bankroll": self.state.bankroll,
"season_bets_so_far": self.state.total_bets,
}
def validate_bet_slip(self, bets: List[BetSlip],
week: int) -> Tuple[List[BetSlip], List[str]]:
"""
Validate a bet slip against the simulation rules.
Returns (valid_bets, list_of_error_messages).
"""
errors = []
valid = []
weekly_total = 0.0
for i, bet in enumerate(bets):
bet_errors = []
# Check game exists this week
week_games = set(self.schedule[
self.schedule["week_number"] == week
]["game_id"])
if bet.game_id not in week_games:
bet_errors.append(
f"Bet {i+1}: game_id '{bet.game_id}' not in week {week}"
)
# Check bet size limits
if bet.stake < MIN_BET:
bet_errors.append(
f"Bet {i+1}: stake ${bet.stake:.2f} below minimum "
f"${MIN_BET:.2f}"
)
if bet.stake > MAX_BET:
bet_errors.append(
f"Bet {i+1}: stake ${bet.stake:.2f} exceeds maximum "
f"${MAX_BET:.2f}"
)
# Check bankroll sufficiency
if bet.stake > self.state.bankroll:
bet_errors.append(
f"Bet {i+1}: stake ${bet.stake:.2f} exceeds bankroll "
f"${self.state.bankroll:.2f}"
)
# Check weekly exposure
weekly_total += bet.stake
if weekly_total > MAX_WEEKLY_EXPOSURE:
bet_errors.append(
f"Bet {i+1}: cumulative weekly exposure "
f"${weekly_total:.2f} exceeds maximum "
f"${MAX_WEEKLY_EXPOSURE:.2f}"
)
# Check sportsbook exists
if bet.sportsbook not in self.books:
bet_errors.append(
f"Bet {i+1}: unknown sportsbook '{bet.sportsbook}'"
)
# Verify odds match the book
if bet.sportsbook in self.books:
book_df = self.books[bet.sportsbook]
game_odds = book_df[book_df["game_id"] == bet.game_id]
if not game_odds.empty:
# Verify the submitted odds match what the book offers
# (allow small rounding differences)
pass # Detailed verification logic here
# Check model probability is valid
if not (0.0 < bet.model_probability < 1.0):
bet_errors.append(
f"Bet {i+1}: model_probability must be between 0 and 1"
)
# Check reasoning is not empty
if not bet.reasoning or len(bet.reasoning.strip()) < 10:
bet_errors.append(
f"Bet {i+1}: reasoning must be at least 10 characters"
)
if bet_errors:
errors.extend(bet_errors)
else:
valid.append(bet)
return valid, errors
def grade_bets(self, bets: List[BetSlip], week: int) -> List[GradedBet]:
"""
Grade submitted bets against actual results.
Grading logic by bet type:
- Spread: home_margin + spread vs. 0
- Moneyline: did the selected team win?
- Total: home_score + away_score vs. total line
"""
graded = []
week_results = self.results[
self.results["game_id"].isin(
self.schedule[self.schedule["week_number"] == week]["game_id"]
)
]
for bet in bets:
game_result = week_results[
week_results["game_id"] == bet.game_id
]
if game_result.empty:
logger.warning(f"No result found for game {bet.game_id}")
continue
game = game_result.iloc[0]
home_score = int(game["home_score"])
away_score = int(game["away_score"])
home_margin = home_score - away_score
total_score = home_score + away_score
# Get game sport
game_info = self.schedule[
self.schedule["game_id"] == bet.game_id
].iloc[0]
sport = game_info["sport"]
# Grade the bet
result, profit = self._grade_single_bet(
bet, home_margin, total_score
)
# Get closing line for CLV calculation
# Use the average across all three books as the closing line
closing_line = self._get_closing_line(bet)
graded.append(GradedBet(
game_id=bet.game_id,
bet_type=bet.bet_type,
selection=bet.selection,
sportsbook=bet.sportsbook,
odds=bet.odds,
stake=bet.stake,
model_probability=bet.model_probability,
reasoning=bet.reasoning,
week=week,
sport=sport,
result=result,
profit=profit,
home_score=home_score,
away_score=away_score,
closing_line=closing_line,
))
return graded
def _grade_single_bet(self, bet: BetSlip, home_margin: int,
total_score: int) -> Tuple[str, float]:
"""Grade a single bet and return (result, profit)."""
odds = bet.odds
stake = bet.stake
if bet.bet_type == "spread":
# Parse the spread from selection (e.g., "KC -3" or "MIN +3")
parts = bet.selection.rsplit(" ", 1)
spread = float(parts[-1])
# Determine which side: if team name matches home, use spread as-is
# If it matches away, the effective margin check is different
# For simplicity: the selection states the team and their spread
# Cover check: team's margin relative to their spread
# "KC -3" covers if KC wins by more than 3
# "MIN +3" covers if MIN loses by less than 3 or wins
# Determine if this is a home or away bet from the game schedule
game_info = self.schedule[
self.schedule["game_id"] == bet.game_id
].iloc[0]
team_name = parts[0].strip()
if team_name == game_info["home_team"]:
cover_margin = home_margin + spread
else:
cover_margin = -home_margin + spread
if cover_margin > 0:
result = "win"
elif cover_margin == 0:
result = "push"
else:
result = "loss"
elif bet.bet_type == "moneyline":
parts = bet.selection.split(" ML")
team_name = parts[0].strip()
game_info = self.schedule[
self.schedule["game_id"] == bet.game_id
].iloc[0]
if team_name == game_info["home_team"]:
won = home_margin > 0
else:
won = home_margin < 0
if home_margin == 0:
result = "push"
elif won:
result = "win"
else:
result = "loss"
elif bet.bet_type == "total":
if "Over" in bet.selection:
line = float(bet.selection.split("Over ")[1])
if total_score > line:
result = "win"
elif total_score == line:
result = "push"
else:
result = "loss"
else: # Under
line = float(bet.selection.split("Under ")[1])
if total_score < line:
result = "win"
elif total_score == line:
result = "push"
else:
result = "loss"
else:
result = "loss" # Unknown bet type
# Calculate profit
if result == "win":
if odds > 0:
profit = stake * (odds / 100.0)
else:
profit = stake * (100.0 / abs(odds))
elif result == "push":
profit = 0.0
else:
profit = -stake
return result, round(profit, 2)
def _get_closing_line(self, bet: BetSlip) -> float:
"""Get the consensus closing line for CLV calculation."""
# Average the relevant line across all three books
# This serves as a proxy for the closing line
lines = []
for book_name, book_df in self.books.items():
game_row = book_df[book_df["game_id"] == bet.game_id]
if game_row.empty:
continue
if bet.bet_type == "spread":
lines.append(game_row.iloc[0]["spread"])
elif bet.bet_type == "total":
lines.append(game_row.iloc[0]["total"])
elif bet.bet_type == "moneyline":
if "home" in bet.selection.lower() or bet.selection.split(" ML")[0] == game_row.iloc[0].get("home_team", ""):
lines.append(game_row.iloc[0]["home_ml"])
else:
lines.append(game_row.iloc[0]["away_ml"])
return np.mean(lines) if lines else 0
def process_week(self, week: int,
bet_slip_path: str) -> WeeklyReport:
"""
Process a complete week: validate, grade, update state.
"""
# Load bet slip
bets = self._load_bet_slip(bet_slip_path)
logger.info(f"Week {week}: Loaded {len(bets)} bets")
# Validate
valid_bets, errors = self.validate_bet_slip(bets, week)
if errors:
logger.warning(f"Validation errors:\n" +
"\n".join(f" - {e}" for e in errors))
logger.info(f"Week {week}: {len(valid_bets)} valid bets "
f"({len(errors)} errors)")
# Grade
graded = self.grade_bets(valid_bets, week)
# Update state
bankroll_start = self.state.bankroll
week_staked = 0.0
week_profit = 0.0
wins = losses = pushes = 0
for g in graded:
self.state.bankroll += g.profit
self.state.total_bets += 1
self.state.total_profit += g.profit
self.state.all_bets.append(g)
week_staked += g.stake
week_profit += g.profit
if g.result == "win":
wins += 1
elif g.result == "loss":
losses += 1
else:
pushes += 1
# Track sports and markets
self.state.sports_bet_on[g.sport] = (
self.state.sports_bet_on.get(g.sport, 0) + 1
)
self.state.markets_bet_on[g.bet_type] = (
self.state.markets_bet_on.get(g.bet_type, 0) + 1
)
# Update peak and drawdown
if self.state.bankroll > self.state.peak_bankroll:
self.state.peak_bankroll = self.state.bankroll
current_drawdown = (
(self.state.peak_bankroll - self.state.bankroll) /
self.state.peak_bankroll * 100
)
if current_drawdown > self.state.max_drawdown:
self.state.max_drawdown = current_drawdown
# Build weekly report
best_bet = max(graded, key=lambda g: g.profit).selection if graded else "N/A"
worst_bet = min(graded, key=lambda g: g.profit).selection if graded else "N/A"
report = WeeklyReport(
week=week,
bets_placed=len(graded),
bets_won=wins,
bets_lost=losses,
bets_pushed=pushes,
total_staked=week_staked,
total_profit=week_profit,
weekly_roi=(week_profit / week_staked * 100) if week_staked > 0 else 0,
bankroll_start=bankroll_start,
bankroll_end=self.state.bankroll,
best_bet=best_bet,
worst_bet=worst_bet,
)
self.state.weekly_reports.append(report)
self._print_weekly_summary(report)
return report
def _load_bet_slip(self, path: str) -> List[BetSlip]:
"""Load a bet slip from CSV."""
bets = []
df = pd.read_csv(path)
for _, row in df.iterrows():
bets.append(BetSlip(
game_id=str(row["game_id"]),
bet_type=str(row["bet_type"]),
selection=str(row["selection"]),
sportsbook=str(row["sportsbook"]),
odds=int(row["odds"]),
stake=float(row["stake"]),
model_probability=float(row["model_probability"]),
reasoning=str(row["reasoning"]),
))
return bets
def _print_weekly_summary(self, report: WeeklyReport):
"""Print a formatted weekly summary."""
print(f"\n{'=' * 60}")
print(f" WEEK {report.week} SUMMARY")
print(f"{'=' * 60}")
print(f" Bets: {report.bets_placed} "
f"({report.bets_won}W-{report.bets_lost}L-"
f"{report.bets_pushed}P)")
print(f" Staked: ${report.total_staked:,.2f}")
print(f" Profit: ${report.total_profit:,.2f} "
f"(ROI: {report.weekly_roi:+.1f}%)")
print(f" Bankroll: ${report.bankroll_start:,.2f} -> "
f"${report.bankroll_end:,.2f}")
print(f" Best bet: {report.best_bet}")
print(f" Worst bet: {report.worst_bet}")
print(f"{'=' * 60}\n")
def generate_season_summary(self) -> Dict:
"""
Generate the final season summary with all performance metrics.
Metrics computed (from Chapters 3, 12, 14, 30, 37):
- ROI: total profit / total staked
- Sharpe ratio: risk-adjusted return measure (Chapter 14)
- Max drawdown: worst peak-to-trough decline (Chapter 14)
- CLV: average closing line value (Chapter 12)
- Brier score: calibration of model probabilities (Chapter 30)
- Win rate by sport, by market, by confidence level
"""
bets_df = pd.DataFrame([asdict(b) for b in self.state.all_bets])
if bets_df.empty:
return {"error": "No bets placed"}
total_staked = bets_df["stake"].sum()
total_profit = bets_df["profit"].sum()
roi = total_profit / total_staked * 100 if total_staked > 0 else 0
# Sharpe ratio (Chapter 14, Section 14.2)
bet_returns = bets_df["profit"] / bets_df["stake"]
sharpe = (bet_returns.mean() / bet_returns.std() *
np.sqrt(len(bets_df))) if bet_returns.std() > 0 else 0
# CLV (Chapter 12) -- requires closing line data
# This would be computed from the closing_line field
# Brier score (Chapter 30)
bets_df["actual_win"] = (bets_df["result"] == "win").astype(float)
non_push = bets_df[bets_df["result"] != "push"]
if len(non_push) > 0:
brier = np.mean(
(non_push["model_probability"] - non_push["actual_win"]) ** 2
)
else:
brier = None
# Diversity checks
sports_count = len(self.state.sports_bet_on)
market_count = len(self.state.markets_bet_on)
primary_sport = max(self.state.sports_bet_on,
key=self.state.sports_bet_on.get)
secondary_pct = 1.0 - (
self.state.sports_bet_on[primary_sport] /
self.state.total_bets
)
# Monthly breakdown
monthly_profits = []
for report in self.state.weekly_reports:
monthly_profits.append(report.total_profit)
return {
"total_bets": self.state.total_bets,
"total_staked": round(total_staked, 2),
"total_profit": round(total_profit, 2),
"roi_pct": round(roi, 2),
"final_bankroll": round(self.state.bankroll, 2),
"peak_bankroll": round(self.state.peak_bankroll, 2),
"max_drawdown_pct": round(self.state.max_drawdown, 2),
"sharpe_ratio": round(sharpe, 3),
"brier_score": round(brier, 4) if brier is not None else None,
"win_rate": round(
(bets_df["result"] == "win").sum() /
(bets_df["result"] != "push").sum() * 100, 1
),
"sports_bet_on": self.state.sports_bet_on,
"markets_bet_on": self.state.markets_bet_on,
"sports_count": sports_count,
"market_count": market_count,
"secondary_sport_pct": round(secondary_pct * 100, 1),
"meets_min_bets": self.state.total_bets >= MIN_TOTAL_BETS,
"meets_sport_diversity": sports_count >= MIN_SPORTS,
"meets_market_diversity": market_count >= MIN_MARKET_TYPES,
}
2.4 Weekly Bet Log Template
In addition to the CSV bet slip, you must maintain a weekly bet log journal (Chapter 37) that includes:
- Market observations: What did you notice about the lines this week? Any significant movements?
- Model output summary: What did your model say? Which games had the largest edges?
- Selection rationale: For each bet, why did you choose this bet and this stake size?
- Post-results reflection: After seeing results, what did you learn? Were any losses due to bad process vs. bad luck?
- Running metrics: Current bankroll, season-to-date ROI, win rate, and any concern flags.
Phase 3: Mid-Season Review and Adjustment
Duration: After Week 3--4 of the simulation (roughly mid-season)
Relevant chapters: Chapter 13 (Evaluating Your Edge), Chapter 30 (Model Evaluation), Chapter 37 (Performance Review), Chapter 41 (Performance Attribution)
At the midpoint of the simulation, you must submit a Mid-Season Review Document (3--5 pages).
3.1 Required Mid-Season Review Contents
Section A: Performance Summary (1 page) - Current bankroll and ROI - Win rate by sport and market type - Cumulative P&L chart - Comparison to pre-season expectations - Key performance metrics: CLV, Brier score, Sharpe ratio
Section B: Model Assessment (1 page) - Is your model performing as expected? - Calibration analysis: are your probability estimates accurate? (Chapter 30) - Which sports/markets have been profitable and which have not? - Feature importance or model diagnostics - Any evidence of model degradation or changing market conditions?
Section C: Strategy Adjustments (1 page) - Based on the data, what will you change for the second half? - Adjustments to staking (increase/decrease unit size, change Kelly fraction) - Adjustments to model (new features, recalibration, different thresholds) - Sports or markets you will add, drop, or reallocate capital toward - Justify every change with evidence, not feelings (Chapter 36)
Section D: Behavioral Self-Assessment (0.5--1 page) - Have you experienced tilt or emotional betting? When and how did you handle it? (Chapter 36) - Have you deviated from your pre-season strategy? If so, why? - What has been your psychological experience? Describe specific moments of doubt, overconfidence, or frustration. - Are your stop-loss and risk management rules being followed?
Phase 4: Final Results and Analysis
Duration: Week 7 (final simulation week)
Relevant chapters: Chapter 14 (Drawdown Analysis), Chapter 30 (Model Evaluation), Chapter 37 (Performance Analysis), Chapter 41 (Performance Attribution)
After the final simulation week, the season is complete. You now have the full record of every bet, every result, and your final bankroll.
4.1 Final Performance Metrics
Calculate and report the following metrics for your complete season:
| Metric | Definition | Chapter | What It Tells You |
|---|---|---|---|
| Total ROI | Total profit / total staked | Ch 3 | Overall efficiency of your betting |
| Win Rate | Wins / (wins + losses) | Ch 3 | Raw accuracy (context-dependent) |
| Yield per Bet | Average profit per bet | Ch 3 | Average edge per wager |
| Sharpe Ratio | Mean return / std dev of returns, annualized | Ch 14 | Risk-adjusted performance |
| Max Drawdown | Largest peak-to-trough decline | Ch 14 | Worst case experienced |
| Max Drawdown Duration | Longest period between equity highs | Ch 14 | Endurance of cold streaks |
| CLV (avg) | Mean closing line value | Ch 12 | Are you consistently beating the market? |
| Brier Score | Mean squared error of probability estimates | Ch 30 | Calibration of your model |
| Kelly Growth Rate | Actual geometric growth vs. Kelly optimal | Ch 14 | Efficiency of your staking |
| Sport-Level ROI | ROI broken down by sport | Ch 37 | Where your edge comes from |
| Market-Level ROI | ROI broken down by market type | Ch 37 | Which markets you exploit best |
| Monthly ROI | ROI broken down by simulation month | Ch 37 | Consistency over time |
Phase 5: Write-Up
Duration: Week 8 (after simulation ends)
Relevant chapters: Chapter 37 (Continuous Improvement), Chapter 41 (Putting It Together)
5.1 Final Report Requirements (10--15 pages)
The final report is the most heavily weighted deliverable. It must demonstrate deep analytical thinking, honest self-assessment, and the ability to separate skill from luck.
Section 1: Executive Summary (1 page) - Final bankroll, ROI, total bets, win rate - One-paragraph summary of what happened - One-paragraph summary of what you learned
Section 2: Strategy Description (2 pages) - Detailed description of your betting approach - Models used and their theoretical basis - Staking methodology with justification - How your strategy evolved from pre-season to final form - Reference specific textbook concepts and chapters
Section 3: Performance Analysis (3--4 pages) - All metrics from the table above, with visualizations - Cumulative P&L chart with drawdown overlay - Calibration plot (predicted probability vs. actual win rate, 10 bins minimum) - Performance breakdown by sport, market type, month, and confidence level - CLV distribution plot - Comparison of actual results to a Monte Carlo simulation of expected results given your edge estimates (Chapter 24)
Section 4: Skill vs. Variance Analysis (2 pages) - Was your result (good or bad) primarily skill or variance? - Use the binomial test or bootstrap confidence interval (Chapter 8, Chapter 24) to assess whether your win rate is significantly different from breakeven (52.4% at -110) - Compare your actual ROI to the distribution of ROIs a random bettor would achieve - If profitable: what evidence supports that your edge is real and not just luck? - If unprofitable: what evidence suggests you had an edge that variance obscured, or alternatively, what evidence suggests your model was not working?
Section 5: Behavioral and Process Review (2 pages) - Honest assessment of your decision-making discipline - Specific examples of good process (even with bad outcomes) - Specific examples of bad process (even with good outcomes) - How did drawdowns affect your behavior? Did you deviate from strategy? - What cognitive biases (Chapter 36) did you observe in yourself? - What would you do differently in a second simulation?
Section 6: Lessons Learned (1--2 pages) - What are the three most important things you learned? - How has this simulation changed your understanding of sports betting? - What surprised you most? - What specific textbook concepts proved most valuable in practice? - What concepts seemed important in theory but were hard to apply in practice?
Scoring Criteria
Grading Rubric
| Component | Weight | Description |
|---|---|---|
| Process Quality | 40% | Rigor, consistency, and discipline throughout the simulation |
| Model Quality | 20% | Statistical validity, calibration, and innovation of predictive approach |
| Results Analysis | 25% | Depth and honesty of self-evaluation, skill vs. variance analysis |
| Presentation | 15% | Clarity of writing, quality of visualizations, professionalism |
Detailed Scoring Breakdown
Process Quality (40%)
| Sub-Component | Points | Criteria |
|---|---|---|
| Pre-season strategy document | 5 | Complete, specific, and grounded in textbook methods |
| Bet reasoning quality | 10 | Every bet has documented reasoning; reasoning is analytical, not emotional |
| Staking discipline | 8 | Consistent staking methodology; rules followed even during drawdowns |
| Weekly bet log quality | 7 | Thorough, reflective, includes market observations |
| Mid-season review depth | 5 | Evidence-based adjustments with clear rationale |
| Rule compliance | 5 | All simulation rules followed; diversity requirements met |
Model Quality (20%)
| Sub-Component | Points | Criteria |
|---|---|---|
| Model methodology | 8 | Sound statistical approach with clear theoretical basis |
| Calibration | 5 | Probability estimates are well-calibrated (Brier score, calibration plot) |
| Feature engineering | 4 | Thoughtful feature selection grounded in domain knowledge |
| Innovation | 3 | Creative approaches, novel combinations, or unique insights |
Results Analysis (25%)
| Sub-Component | Points | Criteria |
|---|---|---|
| Performance metrics | 5 | All required metrics computed correctly |
| Visualizations | 5 | Clear, informative charts that support the narrative |
| Skill vs. variance analysis | 8 | Rigorous statistical assessment of whether results reflect edge |
| Behavioral self-assessment | 7 | Honest, specific, and reflective analysis of decision-making |
Presentation (15%)
| Sub-Component | Points | Criteria |
|---|---|---|
| Writing quality | 5 | Clear, professional, well-organized |
| Report completeness | 5 | All required sections included with sufficient depth |
| Visual presentation | 5 | Charts are labeled, well-formatted, and support the analysis |
Important Grading Notes
-
A profitable simulation does NOT guarantee a high grade. A student who profits through undocumented, undisciplined betting will score lower than a student who loses money through a rigorous, well-documented process.
-
An unprofitable simulation does NOT guarantee a low grade. Variance is real. A well-executed strategy can lose money over one season due to sampling variability. What matters is whether the process was sound and whether the analysis honestly evaluates the result.
-
Honesty is rewarded. Self-awareness about mistakes, biases, and limitations demonstrates mastery of Chapter 36 and Chapter 37 more effectively than claiming perfection.
-
The skill vs. variance analysis is critical. This section separates students who truly understand the statistical nature of betting from those who merely participate. A 2,000-word analysis using bootstrap confidence intervals, Monte Carlo simulations, and CLV analysis will score much higher than a 200-word assertion that "I was just unlucky" or "my model works."
Suggested Timeline
| Week | Phase | Key Activities | Deliverable |
|---|---|---|---|
| 1 | Phase 1: Pre-Season | Build/calibrate models; develop strategy; write strategy doc | Pre-Season Strategy Document |
| 2 | Phase 2: Month 1 | Weeks 1--4 of simulation; submit weekly bet slips and logs | 4 weekly bet slips + bet logs |
| 3 | Phase 2: Month 2 | Weeks 5--8 of simulation | 4 weekly bet slips + bet logs |
| 4 | Phase 3: Mid-Season | Mid-season review; strategy adjustments | Mid-Season Review Document |
| 5 | Phase 2: Month 3 | Weeks 9--12 of simulation | 4 weekly bet slips + bet logs |
| 6 | Phase 2: Month 4 | Weeks 13--16 of simulation | 4 weekly bet slips + bet logs |
| 7 | Phase 4: Final weeks | Weeks 17--18 of simulation; final grading | Final 2 weekly bet slips + logs |
| 8 | Phase 5: Write-Up | Compile analysis; produce final report | Final Report (10--15 pages) |
Chapter Reference Index
- Chapter 2 (Probability and Odds): Interpreting odds across the three simulated sportsbooks
- Chapter 3 (Expected Value): EV calculation as the basis for every bet decision
- Chapter 4 (Bankroll Management): Foundational staking strategy and survival math
- Chapter 5 (Data Literacy): Working with the provided data files, maintaining records
- Chapter 8 (Hypothesis Testing): Testing whether your win rate is statistically significant
- Chapter 9 (Regression Analysis): Building predictive models for game outcomes
- Chapter 11 (Betting Markets): Understanding what the odds are telling you
- Chapter 12 (Line Shopping): Exploiting differences between the three sportsbooks; CLV tracking
- Chapter 13 (Value Betting): Identifying +EV opportunities systematically
- Chapter 14 (Advanced Bankroll): Kelly sizing, drawdown management, Sharpe ratio
- Chapter 15--22 (Sport-Specific Modeling): Building models for each sport you bet on
- Chapter 24 (Monte Carlo Simulation): Simulating expected results given your estimated edge
- Chapter 26 (Ratings and Rankings): Elo or similar rating systems as model inputs
- Chapter 27 (Advanced Regression): Machine learning models, probability calibration
- Chapter 30 (Model Evaluation): Brier score, calibration plots, walk-forward validation of your model
- Chapter 36 (Psychology): Managing emotions, identifying biases, maintaining discipline
- Chapter 37 (Discipline and Systems): Bet logging, performance tracking, review processes
- Chapter 38 (Risk Management): Stop-loss rules, responsible bankroll management
- Chapter 41 (Putting It Together): Integrating all components into a coherent operation
Tips for Success
-
Start with a clear plan. The pre-season strategy document is not busywork. Students who write a thoughtful strategy document and follow it consistently outperform students who "wing it." The discipline of committing to a plan in writing creates accountability.
-
Your bet reasoning is the most important thing you write. A one-sentence reasoning like "model likes this" will score poorly. A reasoning like "Elo model gives KC a 0.58 win probability vs. market implied 0.53 (after vig removal). 5% edge exceeds my 3% threshold. Taking KC at book_B which has the best line at -145 vs. -155 at book_A" demonstrates mastery.
-
Embrace the losing streaks. You will have losing weeks. Probably multiple in a row. This is mathematically certain even for a skilled bettor. What matters is how you respond. Do you increase bet sizes to chase losses (bad)? Do you abandon your model after a 3-game losing streak (bad)? Or do you maintain discipline, log your bets carefully, and trust the process (good)?
-
Track CLV religiously. Closing Line Value (Chapter 12) is your best real-time indicator of whether your model has edge. If you are consistently getting better prices than the closing line -- even during a losing streak -- you should maintain confidence in your process.
-
The skill vs. variance section separates good students from great ones. Use the Monte Carlo simulation technique from Chapter 24 to generate 10,000 simulated seasons with your estimated edge, and see where your actual result falls in that distribution. Use the binomial test from Chapter 8 to test statistical significance. This is where the textbook knowledge converts into genuine understanding.
-
Be honest about what you do not know. The strongest final reports acknowledge uncertainty. If your model was wrong about something, say so. If you cannot determine whether a loss was bad luck or a bad model, say that too. Intellectual honesty is a hallmark of sophisticated analytical thinking.
This capstone project integrates material from Chapters 2--5, 8--9, 11--14, 15--22, 24, 26--27, 30, 36--38, and 41 of The Sports Betting Textbook.