Chapter 29: Trade Deadline & Contract Analytics

The trade deadline and contract decisions represent some of the most critical moments in a baseball front office's calendar. These decisions require sophisticated analytics that combine player valuation, financial modeling, competitive window analysis, and risk assessment. This chapter explores the frameworks and tools used by modern MLB front offices to navigate trades, contract negotiations, and roster construction.

Advanced ~5 min read 8 sections 11 code examples
Book Progress
56%
Chapter 30 of 54
What You'll Learn
  • Trade Deadline Decision Framework
  • Prospect Valuation & Trade Value
  • Surplus Value Calculations
  • Contract Optimization & Arbitration Projections
  • And 4 more topics...
Languages in This Chapter
R (5) Python (6)

All code examples can be copied and run in your environment.

29.1 Trade Deadline Decision Framework

The July 31st trade deadline (or the new July 30th deadline in recent years) forces teams to make critical decisions about their competitive window. The fundamental question every team faces is: should we buy, sell, or hold?

The Buy/Sell/Hold Decision Matrix

The decision to buy or sell at the deadline depends on several key factors:

  1. Current playoff probability: Teams with >70% playoff odds typically buy; teams with <20% typically sell
  2. Playoff expectation vs. preseason projection: Overperforming teams may hold rather than sell
  3. Competitive window timeline: Young, improving teams may buy even with modest playoff odds
  4. Farm system strength: Teams with deep systems can more easily afford to trade prospects
  5. Financial flexibility: Payroll constraints limit buying power

Expected Value Framework

The expected value of a trade deadline acquisition can be modeled as:

EV(Trade) = Δ(Pplayoff) × EV(Playoff) + Δ(Pdivision) × EV(Division) - ProspectCost - FinancialCost

Where:


  • Δ(P_playoff) = Change in playoff probability from the acquisition

  • EV(Playoff) = Expected value of making the playoffs (~$30-50M in revenue)

  • Δ(P_division) = Change in division win probability

  • EV(Division) = Additional value of division title (home field advantage, additional revenue)

  • Prospect_Cost = Expected future value of traded prospects

  • Financial_Cost = Salary obligations

R Implementation: Trade Decision Model

library(tidyverse)
library(mgcv)

# Function to calculate playoff probability impact
calculate_playoff_ev <- function(current_prob, new_prob,
                                  playoff_revenue = 40000000,
                                  division_bonus = 10000000) {
  # Calculate probability changes
  delta_playoff <- new_prob$playoff - current_prob$playoff
  delta_division <- new_prob$division - current_prob$division

  # Expected value calculation
  ev_playoff <- delta_playoff * playoff_revenue
  ev_division <- delta_division * division_bonus

  total_ev <- ev_playoff + ev_division

  return(list(
    delta_playoff = delta_playoff,
    delta_division = delta_division,
    ev_playoff = ev_playoff,
    ev_division = ev_division,
    total_ev = total_ev
  ))
}

# Function to simulate season outcomes with and without acquisition
simulate_trade_impact <- function(team_data, player_war, games_remaining) {
  # Current team strength (in wins above .500)
  current_strength <- team_data$current_wins - team_data$current_losses

  # Project rest of season
  expected_wins_current <- team_data$current_wins +
    (games_remaining * 0.5) + (current_strength / 162 * games_remaining)

  # With acquisition (simplified - assumes even distribution of WAR)
  war_impact <- player_war * (games_remaining / 162)
  expected_wins_with_trade <- expected_wins_current + war_impact

  # Convert to playoff probabilities using logistic model
  # Based on historical data: ~90 wins = 70% playoff probability
  prob_current <- plogis((expected_wins_current - 90) / 5)
  prob_with_trade <- plogis((expected_wins_with_trade - 90) / 5)

  # Division probability (simplified)
  div_prob_current <- plogis((expected_wins_current - 93) / 4)
  div_prob_with_trade <- plogis((expected_wins_with_trade - 93) / 4)

  return(list(
    current = list(playoff = prob_current, division = div_prob_current),
    with_trade = list(playoff = prob_with_trade, division = div_prob_with_trade),
    win_improvement = war_impact
  ))
}

# Example: Yankees considering a trade for a 2 WAR pitcher
team_situation <- list(
  current_wins = 55,
  current_losses = 48,
  games_remaining = 59
)

impact <- simulate_trade_impact(team_situation, player_war = 2.0, games_remaining = 59)
ev_result <- calculate_playoff_ev(impact$current, impact$with_trade)

cat("Trade Impact Analysis\n")
cat("=====================\n")
cat(sprintf("Current Playoff Probability: %.1f%%\n", impact$current$playoff * 100))
cat(sprintf("With Trade Playoff Probability: %.1f%%\n", impact$with_trade$playoff * 100))
cat(sprintf("Probability Increase: %.1f%%\n", ev_result$delta_playoff * 100))
cat(sprintf("\nExpected Value of Trade: $%.1fM\n", ev_result$total_ev / 1000000))

Python Implementation: Advanced Trade Decision Model

import numpy as np
import pandas as pd
from scipy.stats import norm
from scipy.optimize import minimize
import matplotlib.pyplot as plt

class TradeDeadlineAnalyzer:
    """
    Comprehensive trade deadline decision support system
    """

    def __init__(self, playoff_revenue=40e6, division_bonus=10e6):
        self.playoff_revenue = playoff_revenue
        self.division_bonus = division_bonus

    def estimate_playoff_probability(self, wins, losses, games_remaining,
                                     division_lead=0, strength_of_schedule=0):
        """
        Estimate playoff probability using multiple factors

        Parameters:
        -----------
        wins : int
            Current wins
        losses : int
            Current losses
        games_remaining : int
            Games left in season
        division_lead : float
            Games ahead in division (negative if behind)
        strength_of_schedule : float
            Remaining SOS adjustment (-1 to 1, negative = easier)
        """
        # Calculate current win percentage
        games_played = wins + losses
        win_pct = wins / games_played if games_played > 0 else 0.5

        # Project final wins
        expected_final_wins = wins + (games_remaining * win_pct)

        # Adjust for strength of schedule
        sos_adjustment = -strength_of_schedule * games_remaining * 0.05
        expected_final_wins += sos_adjustment

        # Historical playoff cutoff: ~87 wins for Wild Card, ~92 for Division
        wc_threshold = 87
        div_threshold = 92

        # Use normal distribution to estimate probabilities
        win_std = np.sqrt(games_remaining * 0.25)  # Binomial approximation

        playoff_prob = 1 - norm.cdf(wc_threshold, expected_final_wins, win_std)
        division_prob = 1 - norm.cdf(div_threshold, expected_final_wins + division_lead, win_std)

        return {
            'playoff_prob': playoff_prob,
            'division_prob': division_prob,
            'expected_wins': expected_final_wins
        }

    def calculate_war_impact(self, player_war, games_remaining):
        """
        Calculate the win impact of acquiring a player
        """
        # WAR is prorated for remaining season
        return player_war * (games_remaining / 162)

    def evaluate_trade(self, current_record, player_war, prospect_value,
                       salary_cost, games_remaining, division_lead=0):
        """
        Complete trade evaluation framework

        Returns expected value and recommendation
        """
        wins, losses = current_record

        # Current situation
        current_situation = self.estimate_playoff_probability(
            wins, losses, games_remaining, division_lead
        )

        # With acquisition
        war_impact = self.calculate_war_impact(player_war, games_remaining)
        new_wins = wins + war_impact

        new_situation = self.estimate_playoff_probability(
            new_wins, losses, games_remaining, division_lead + war_impact
        )

        # Calculate expected value
        delta_playoff = new_situation['playoff_prob'] - current_situation['playoff_prob']
        delta_division = new_situation['division_prob'] - current_situation['division_prob']

        ev_playoff = delta_playoff * self.playoff_revenue
        ev_division = delta_division * self.division_bonus

        total_revenue_ev = ev_playoff + ev_division
        total_cost = prospect_value + salary_cost

        net_ev = total_revenue_ev - total_cost

        # Recommendation
        if net_ev > 5e6:
            recommendation = "STRONG BUY"
        elif net_ev > 0:
            recommendation = "BUY"
        elif net_ev > -5e6:
            recommendation = "HOLD/EVALUATE"
        else:
            recommendation = "PASS"

        return {
            'current_playoff_prob': current_situation['playoff_prob'],
            'new_playoff_prob': new_situation['playoff_prob'],
            'delta_playoff_prob': delta_playoff,
            'expected_wins_added': war_impact,
            'revenue_ev': total_revenue_ev,
            'total_cost': total_cost,
            'net_ev': net_ev,
            'recommendation': recommendation
        }

    def sensitivity_analysis(self, base_scenario, param_ranges):
        """
        Perform sensitivity analysis on key parameters
        """
        results = []

        for param, values in param_ranges.items():
            for value in values:
                scenario = base_scenario.copy()
                scenario[param] = value

                result = self.evaluate_trade(**scenario)
                result['param'] = param
                result['value'] = value
                results.append(result)

        return pd.DataFrame(results)

# Example usage
analyzer = TradeDeadlineAnalyzer()

# Scenario: Contending team considering ace pitcher acquisition
base_scenario = {
    'current_record': (62, 45),
    'player_war': 2.5,
    'prospect_value': 25e6,  # Value of prospects given up
    'salary_cost': 8e6,      # Remaining salary obligation
    'games_remaining': 55,
    'division_lead': 2.5
}

result = analyzer.evaluate_trade(**base_scenario)

print("Trade Evaluation Report")
print("=" * 50)
print(f"Current Playoff Probability: {result['current_playoff_prob']:.1%}")
print(f"Playoff Prob. with Trade: {result['new_playoff_prob']:.1%}")
print(f"Probability Increase: {result['delta_playoff_prob']:.1%}")
print(f"\nExpected Wins Added: {result['expected_wins_added']:.2f}")
print(f"\nRevenue Expected Value: ${result['revenue_ev']/1e6:.2f}M")
print(f"Total Cost: ${result['total_cost']/1e6:.2f}M")
print(f"Net Expected Value: ${result['net_ev']/1e6:.2f}M")
print(f"\nRecommendation: {result['recommendation']}")

# Sensitivity analysis
param_ranges = {
    'player_war': [1.5, 2.0, 2.5, 3.0, 3.5],
    'prospect_value': [15e6, 20e6, 25e6, 30e6, 35e6]
}

# Run one parameter at a time
for param, values in param_ranges.items():
    scenarios = []
    for value in values:
        scenario = base_scenario.copy()
        scenario[param] = value
        result = analyzer.evaluate_trade(**scenario)
        scenarios.append({
            param: value,
            'net_ev': result['net_ev']
        })

    df = pd.DataFrame(scenarios)
    print(f"\nSensitivity to {param}:")
    print(df)
R
library(tidyverse)
library(mgcv)

# Function to calculate playoff probability impact
calculate_playoff_ev <- function(current_prob, new_prob,
                                  playoff_revenue = 40000000,
                                  division_bonus = 10000000) {
  # Calculate probability changes
  delta_playoff <- new_prob$playoff - current_prob$playoff
  delta_division <- new_prob$division - current_prob$division

  # Expected value calculation
  ev_playoff <- delta_playoff * playoff_revenue
  ev_division <- delta_division * division_bonus

  total_ev <- ev_playoff + ev_division

  return(list(
    delta_playoff = delta_playoff,
    delta_division = delta_division,
    ev_playoff = ev_playoff,
    ev_division = ev_division,
    total_ev = total_ev
  ))
}

# Function to simulate season outcomes with and without acquisition
simulate_trade_impact <- function(team_data, player_war, games_remaining) {
  # Current team strength (in wins above .500)
  current_strength <- team_data$current_wins - team_data$current_losses

  # Project rest of season
  expected_wins_current <- team_data$current_wins +
    (games_remaining * 0.5) + (current_strength / 162 * games_remaining)

  # With acquisition (simplified - assumes even distribution of WAR)
  war_impact <- player_war * (games_remaining / 162)
  expected_wins_with_trade <- expected_wins_current + war_impact

  # Convert to playoff probabilities using logistic model
  # Based on historical data: ~90 wins = 70% playoff probability
  prob_current <- plogis((expected_wins_current - 90) / 5)
  prob_with_trade <- plogis((expected_wins_with_trade - 90) / 5)

  # Division probability (simplified)
  div_prob_current <- plogis((expected_wins_current - 93) / 4)
  div_prob_with_trade <- plogis((expected_wins_with_trade - 93) / 4)

  return(list(
    current = list(playoff = prob_current, division = div_prob_current),
    with_trade = list(playoff = prob_with_trade, division = div_prob_with_trade),
    win_improvement = war_impact
  ))
}

# Example: Yankees considering a trade for a 2 WAR pitcher
team_situation <- list(
  current_wins = 55,
  current_losses = 48,
  games_remaining = 59
)

impact <- simulate_trade_impact(team_situation, player_war = 2.0, games_remaining = 59)
ev_result <- calculate_playoff_ev(impact$current, impact$with_trade)

cat("Trade Impact Analysis\n")
cat("=====================\n")
cat(sprintf("Current Playoff Probability: %.1f%%\n", impact$current$playoff * 100))
cat(sprintf("With Trade Playoff Probability: %.1f%%\n", impact$with_trade$playoff * 100))
cat(sprintf("Probability Increase: %.1f%%\n", ev_result$delta_playoff * 100))
cat(sprintf("\nExpected Value of Trade: $%.1fM\n", ev_result$total_ev / 1000000))
Python
import numpy as np
import pandas as pd
from scipy.stats import norm
from scipy.optimize import minimize
import matplotlib.pyplot as plt

class TradeDeadlineAnalyzer:
    """
    Comprehensive trade deadline decision support system
    """

    def __init__(self, playoff_revenue=40e6, division_bonus=10e6):
        self.playoff_revenue = playoff_revenue
        self.division_bonus = division_bonus

    def estimate_playoff_probability(self, wins, losses, games_remaining,
                                     division_lead=0, strength_of_schedule=0):
        """
        Estimate playoff probability using multiple factors

        Parameters:
        -----------
        wins : int
            Current wins
        losses : int
            Current losses
        games_remaining : int
            Games left in season
        division_lead : float
            Games ahead in division (negative if behind)
        strength_of_schedule : float
            Remaining SOS adjustment (-1 to 1, negative = easier)
        """
        # Calculate current win percentage
        games_played = wins + losses
        win_pct = wins / games_played if games_played > 0 else 0.5

        # Project final wins
        expected_final_wins = wins + (games_remaining * win_pct)

        # Adjust for strength of schedule
        sos_adjustment = -strength_of_schedule * games_remaining * 0.05
        expected_final_wins += sos_adjustment

        # Historical playoff cutoff: ~87 wins for Wild Card, ~92 for Division
        wc_threshold = 87
        div_threshold = 92

        # Use normal distribution to estimate probabilities
        win_std = np.sqrt(games_remaining * 0.25)  # Binomial approximation

        playoff_prob = 1 - norm.cdf(wc_threshold, expected_final_wins, win_std)
        division_prob = 1 - norm.cdf(div_threshold, expected_final_wins + division_lead, win_std)

        return {
            'playoff_prob': playoff_prob,
            'division_prob': division_prob,
            'expected_wins': expected_final_wins
        }

    def calculate_war_impact(self, player_war, games_remaining):
        """
        Calculate the win impact of acquiring a player
        """
        # WAR is prorated for remaining season
        return player_war * (games_remaining / 162)

    def evaluate_trade(self, current_record, player_war, prospect_value,
                       salary_cost, games_remaining, division_lead=0):
        """
        Complete trade evaluation framework

        Returns expected value and recommendation
        """
        wins, losses = current_record

        # Current situation
        current_situation = self.estimate_playoff_probability(
            wins, losses, games_remaining, division_lead
        )

        # With acquisition
        war_impact = self.calculate_war_impact(player_war, games_remaining)
        new_wins = wins + war_impact

        new_situation = self.estimate_playoff_probability(
            new_wins, losses, games_remaining, division_lead + war_impact
        )

        # Calculate expected value
        delta_playoff = new_situation['playoff_prob'] - current_situation['playoff_prob']
        delta_division = new_situation['division_prob'] - current_situation['division_prob']

        ev_playoff = delta_playoff * self.playoff_revenue
        ev_division = delta_division * self.division_bonus

        total_revenue_ev = ev_playoff + ev_division
        total_cost = prospect_value + salary_cost

        net_ev = total_revenue_ev - total_cost

        # Recommendation
        if net_ev > 5e6:
            recommendation = "STRONG BUY"
        elif net_ev > 0:
            recommendation = "BUY"
        elif net_ev > -5e6:
            recommendation = "HOLD/EVALUATE"
        else:
            recommendation = "PASS"

        return {
            'current_playoff_prob': current_situation['playoff_prob'],
            'new_playoff_prob': new_situation['playoff_prob'],
            'delta_playoff_prob': delta_playoff,
            'expected_wins_added': war_impact,
            'revenue_ev': total_revenue_ev,
            'total_cost': total_cost,
            'net_ev': net_ev,
            'recommendation': recommendation
        }

    def sensitivity_analysis(self, base_scenario, param_ranges):
        """
        Perform sensitivity analysis on key parameters
        """
        results = []

        for param, values in param_ranges.items():
            for value in values:
                scenario = base_scenario.copy()
                scenario[param] = value

                result = self.evaluate_trade(**scenario)
                result['param'] = param
                result['value'] = value
                results.append(result)

        return pd.DataFrame(results)

# Example usage
analyzer = TradeDeadlineAnalyzer()

# Scenario: Contending team considering ace pitcher acquisition
base_scenario = {
    'current_record': (62, 45),
    'player_war': 2.5,
    'prospect_value': 25e6,  # Value of prospects given up
    'salary_cost': 8e6,      # Remaining salary obligation
    'games_remaining': 55,
    'division_lead': 2.5
}

result = analyzer.evaluate_trade(**base_scenario)

print("Trade Evaluation Report")
print("=" * 50)
print(f"Current Playoff Probability: {result['current_playoff_prob']:.1%}")
print(f"Playoff Prob. with Trade: {result['new_playoff_prob']:.1%}")
print(f"Probability Increase: {result['delta_playoff_prob']:.1%}")
print(f"\nExpected Wins Added: {result['expected_wins_added']:.2f}")
print(f"\nRevenue Expected Value: ${result['revenue_ev']/1e6:.2f}M")
print(f"Total Cost: ${result['total_cost']/1e6:.2f}M")
print(f"Net Expected Value: ${result['net_ev']/1e6:.2f}M")
print(f"\nRecommendation: {result['recommendation']}")

# Sensitivity analysis
param_ranges = {
    'player_war': [1.5, 2.0, 2.5, 3.0, 3.5],
    'prospect_value': [15e6, 20e6, 25e6, 30e6, 35e6]
}

# Run one parameter at a time
for param, values in param_ranges.items():
    scenarios = []
    for value in values:
        scenario = base_scenario.copy()
        scenario[param] = value
        result = analyzer.evaluate_trade(**scenario)
        scenarios.append({
            param: value,
            'net_ev': result['net_ev']
        })

    df = pd.DataFrame(scenarios)
    print(f"\nSensitivity to {param}:")
    print(df)

29.2 Prospect Valuation & Trade Value

Accurately valuing prospects is one of the most challenging aspects of trade analysis. Teams must project future performance while accounting for development risk, injury risk, and the time value of wins.

The Surplus Value Framework

Surplus value represents the difference between a player's expected production and their salary cost. For prospects, this calculation includes:

  1. Probability of reaching MLB (varies by prospect level)
  2. Expected performance if they reach MLB
  3. Years of team control
  4. Discount rate (future value vs. present value)

Prospect Value Curves

Research by FanGraphs and other sources suggests the following approximate MLB arrival rates:

  • Top 10 prospects: 70-80% reach MLB, 40-50% become above-average regulars
  • Top 50 prospects: 60-70% reach MLB, 25-35% become above-average regulars
  • Top 100 prospects: 50-60% reach MLB, 15-25% become above-average regulars
  • Fringe prospects: 20-30% reach MLB, 5-10% become above-average regulars

R Implementation: Prospect Valuation Model

library(tidyverse)

# Prospect valuation function
value_prospect <- function(rank, position, eta,
                          discount_rate = 0.10,
                          mlb_arrival_prob = NULL) {

  # Default MLB arrival probabilities by prospect rank
  if (is.null(mlb_arrival_prob)) {
    mlb_arrival_prob <- case_when(
      rank <= 10 ~ 0.75,
      rank <= 30 ~ 0.65,
      rank <= 50 ~ 0.55,
      rank <= 100 ~ 0.45,
      TRUE ~ 0.25
    )
  }

  # Expected performance if reaches MLB (in WAR per year)
  expected_war <- case_when(
    rank <= 10 ~ 3.5,
    rank <= 30 ~ 2.5,
    rank <= 50 ~ 2.0,
    rank <= 100 ~ 1.5,
    TRUE ~ 0.8
  )

  # Years of team control (typically 6 years)
  control_years <- 6

  # Cost structure (simplified)
  # Pre-arb: 3 years at ~$1M each
  # Arb: 3 years at increasing rates
  prearb_salary <- c(700000, 750000, 800000)

  # Arbitration salary as % of market value
  # Roughly 40%, 60%, 80% of market value
  dollars_per_war <- 8000000  # Market rate for 1 WAR

  arb_salary <- c(
    expected_war * dollars_per_war * 0.40,
    expected_war * dollars_per_war * 0.60,
    expected_war * dollars_per_war * 0.80
  )

  # Calculate surplus value for each year
  total_value <- 0

  for (year in 1:control_years) {
    # Time discount
    discount_factor <- (1 + discount_rate) ^ (eta + year - 1)

    # Expected production value
    production_value <- expected_war * dollars_per_war

    # Cost
    if (year <= 3) {
      cost <- prearb_salary[year]
    } else {
      cost <- arb_salary[year - 3]
    }

    # Surplus for this year
    surplus <- (production_value - cost) / discount_factor

    total_value <- total_value + surplus
  }

  # Multiply by MLB arrival probability
  expected_surplus <- total_value * mlb_arrival_prob

  return(list(
    rank = rank,
    mlb_prob = mlb_arrival_prob,
    expected_war = expected_war,
    total_surplus = total_value,
    expected_surplus = expected_surplus
  ))
}

# Create a prospect value chart
prospect_ranks <- c(1, 5, 10, 20, 30, 50, 75, 100)
valuations <- map_df(prospect_ranks, ~{
  val <- value_prospect(.x, "SS", eta = 1)
  tibble(
    rank = val$rank,
    mlb_prob = val$mlb_prob,
    expected_war = val$expected_war,
    expected_surplus = val$expected_surplus
  )
})

print("Prospect Value Chart (in millions)")
print(valuations %>%
  mutate(expected_surplus = expected_surplus / 1e6) %>%
  mutate(across(where(is.numeric), ~round(., 2))))

# Visualize
ggplot(valuations, aes(x = rank, y = expected_surplus / 1e6)) +
  geom_line(color = "blue", size = 1.2) +
  geom_point(color = "red", size = 3) +
  scale_x_reverse() +
  labs(
    title = "Prospect Value Curve",
    subtitle = "Expected surplus value by prospect ranking",
    x = "Prospect Rank (lower is better)",
    y = "Expected Surplus Value ($M)",
    caption = "Assumes 1 year to MLB, 10% discount rate"
  ) +
  theme_minimal() +
  theme(plot.title = element_text(face = "bold", size = 14))

Python Implementation: Advanced Prospect Portfolio Valuation

import numpy as np
import pandas as pd
from scipy.stats import beta, norm
import matplotlib.pyplot as plt
import seaborn as sns

class ProspectValuationSystem:
    """
    Advanced prospect valuation using probabilistic modeling
    """

    def __init__(self, dollars_per_war=8e6, discount_rate=0.10):
        self.dollars_per_war = dollars_per_war
        self.discount_rate = discount_rate

        # Salary structure constants
        self.prearb_salaries = [700000, 750000, 800000]
        self.arb_percentages = [0.40, 0.60, 0.80]

    def estimate_mlb_probability(self, rank, org_rank=None, age=None, level=None):
        """
        Estimate probability of reaching MLB using multiple factors
        """
        # Base probability from ranking
        if rank <= 10:
            base_prob = 0.75
        elif rank <= 30:
            base_prob = 0.65
        elif rank <= 50:
            base_prob = 0.55
        elif rank <= 100:
            base_prob = 0.45
        else:
            base_prob = 0.25

        # Adjustments
        adjustments = 0

        # Organization rank adjustment
        if org_rank is not None:
            if org_rank == 1:
                adjustments += 0.05
            elif org_rank <= 3:
                adjustments += 0.03

        # Age adjustment (younger = more risk but more upside)
        if age is not None:
            if age < 20:
                adjustments -= 0.05  # More developmental risk
            elif age > 24:
                adjustments -= 0.10  # Less time to develop

        # Level adjustment
        if level is not None:
            level_adjustments = {
                'MLB': 0.95,  # Already there
                'AAA': 0.15,
                'AA': 0.05,
                'A+': 0.00,
                'A': -0.05,
                'Rookie': -0.10
            }
            adjustments += level_adjustments.get(level, 0)

        return np.clip(base_prob + adjustments, 0.05, 0.95)

    def simulate_war_distribution(self, rank, n_simulations=10000):
        """
        Simulate distribution of potential WAR outcomes
        Uses beta distribution to model performance uncertainty
        """
        # Expected WAR by rank
        if rank <= 10:
            mean_war = 3.5
            std_war = 1.5
        elif rank <= 30:
            mean_war = 2.5
            std_war = 1.2
        elif rank <= 50:
            mean_war = 2.0
            std_war = 1.0
        elif rank <= 100:
            mean_war = 1.5
            std_war = 0.8
        else:
            mean_war = 0.8
            std_war = 0.6

        # Simulate from normal distribution, bounded at 0
        war_simulations = np.random.normal(mean_war, std_war, n_simulations)
        war_simulations = np.maximum(war_simulations, 0)

        return war_simulations

    def calculate_surplus_value(self, war_per_year, years_to_mlb=1, control_years=6):
        """
        Calculate surplus value for a given performance level
        """
        total_surplus = 0

        for year in range(1, control_years + 1):
            # Discount factor
            discount_factor = (1 + self.discount_rate) ** (years_to_mlb + year - 1)

            # Production value
            production_value = war_per_year * self.dollars_per_war

            # Cost
            if year <= 3:
                cost = self.prearb_salaries[year - 1]
            else:
                cost = war_per_year * self.dollars_per_war * self.arb_percentages[year - 4]

            # Surplus
            surplus = (production_value - cost) / discount_factor
            total_surplus += surplus

        return total_surplus

    def value_prospect(self, rank, years_to_mlb=1, position='generic',
                       org_rank=None, age=None, level=None, n_simulations=10000):
        """
        Complete prospect valuation with uncertainty
        """
        # MLB arrival probability
        mlb_prob = self.estimate_mlb_probability(rank, org_rank, age, level)

        # Simulate WAR outcomes
        war_simulations = self.simulate_war_distribution(rank, n_simulations)

        # Calculate surplus for each simulation
        surplus_simulations = np.array([
            self.calculate_surplus_value(war, years_to_mlb)
            for war in war_simulations
        ])

        # Apply MLB probability
        expected_surplus_simulations = surplus_simulations * mlb_prob

        return {
            'rank': rank,
            'mlb_probability': mlb_prob,
            'mean_war': np.mean(war_simulations),
            'median_war': np.median(war_simulations),
            'p90_war': np.percentile(war_simulations, 90),
            'p10_war': np.percentile(war_simulations, 10),
            'mean_surplus': np.mean(surplus_simulations),
            'expected_surplus': np.mean(expected_surplus_simulations),
            'median_surplus': np.median(expected_surplus_simulations),
            'p90_surplus': np.percentile(expected_surplus_simulations, 90),
            'p10_surplus': np.percentile(expected_surplus_simulations, 10)
        }

    def value_prospect_portfolio(self, prospects):
        """
        Value a portfolio of prospects (for multi-player trades)
        """
        results = []

        for prospect in prospects:
            valuation = self.value_prospect(**prospect)
            valuation['name'] = prospect.get('name', f"Prospect {prospect['rank']}")
            results.append(valuation)

        df = pd.DataFrame(results)

        # Portfolio statistics
        portfolio_value = df['expected_surplus'].sum()

        return df, portfolio_value

# Example usage
valuator = ProspectValuationSystem()

# Value a single top prospect
prospect_value = valuator.value_prospect(
    rank=15,
    years_to_mlb=1,
    org_rank=2,
    age=21,
    level='AA'
)

print("Prospect Valuation Report")
print("=" * 50)
print(f"MLB Probability: {prospect_value['mlb_probability']:.1%}")
print(f"\nProjected Performance:")
print(f"  Mean WAR: {prospect_value['mean_war']:.2f}")
print(f"  90th Percentile: {prospect_value['p90_war']:.2f}")
print(f"  10th Percentile: {prospect_value['p10_war']:.2f}")
print(f"\nSurplus Value:")
print(f"  Expected: ${prospect_value['expected_surplus']/1e6:.2f}M")
print(f"  90th Percentile: ${prospect_value['p90_surplus']/1e6:.2f}M")
print(f"  10th Percentile: ${prospect_value['p10_surplus']/1e6:.2f}M")

# Value a prospect package (typical trade scenario)
prospect_package = [
    {'name': 'Top Pitching Prospect', 'rank': 12, 'years_to_mlb': 1, 'age': 20, 'level': 'AA'},
    {'name': 'Shortstop Prospect', 'rank': 45, 'years_to_mlb': 2, 'age': 19, 'level': 'A+'},
    {'name': 'Outfield Lottery Ticket', 'rank': 85, 'years_to_mlb': 3, 'age': 18, 'level': 'A'}
]

portfolio_df, total_value = valuator.value_prospect_portfolio(prospect_package)

print("\n\nProspect Package Valuation")
print("=" * 50)
print(portfolio_df[['name', 'rank', 'mlb_probability', 'mean_war', 'expected_surplus']].to_string(index=False))
print(f"\nTotal Package Value: ${total_value/1e6:.2f}M")
R
library(tidyverse)

# Prospect valuation function
value_prospect <- function(rank, position, eta,
                          discount_rate = 0.10,
                          mlb_arrival_prob = NULL) {

  # Default MLB arrival probabilities by prospect rank
  if (is.null(mlb_arrival_prob)) {
    mlb_arrival_prob <- case_when(
      rank <= 10 ~ 0.75,
      rank <= 30 ~ 0.65,
      rank <= 50 ~ 0.55,
      rank <= 100 ~ 0.45,
      TRUE ~ 0.25
    )
  }

  # Expected performance if reaches MLB (in WAR per year)
  expected_war <- case_when(
    rank <= 10 ~ 3.5,
    rank <= 30 ~ 2.5,
    rank <= 50 ~ 2.0,
    rank <= 100 ~ 1.5,
    TRUE ~ 0.8
  )

  # Years of team control (typically 6 years)
  control_years <- 6

  # Cost structure (simplified)
  # Pre-arb: 3 years at ~$1M each
  # Arb: 3 years at increasing rates
  prearb_salary <- c(700000, 750000, 800000)

  # Arbitration salary as % of market value
  # Roughly 40%, 60%, 80% of market value
  dollars_per_war <- 8000000  # Market rate for 1 WAR

  arb_salary <- c(
    expected_war * dollars_per_war * 0.40,
    expected_war * dollars_per_war * 0.60,
    expected_war * dollars_per_war * 0.80
  )

  # Calculate surplus value for each year
  total_value <- 0

  for (year in 1:control_years) {
    # Time discount
    discount_factor <- (1 + discount_rate) ^ (eta + year - 1)

    # Expected production value
    production_value <- expected_war * dollars_per_war

    # Cost
    if (year <= 3) {
      cost <- prearb_salary[year]
    } else {
      cost <- arb_salary[year - 3]
    }

    # Surplus for this year
    surplus <- (production_value - cost) / discount_factor

    total_value <- total_value + surplus
  }

  # Multiply by MLB arrival probability
  expected_surplus <- total_value * mlb_arrival_prob

  return(list(
    rank = rank,
    mlb_prob = mlb_arrival_prob,
    expected_war = expected_war,
    total_surplus = total_value,
    expected_surplus = expected_surplus
  ))
}

# Create a prospect value chart
prospect_ranks <- c(1, 5, 10, 20, 30, 50, 75, 100)
valuations <- map_df(prospect_ranks, ~{
  val <- value_prospect(.x, "SS", eta = 1)
  tibble(
    rank = val$rank,
    mlb_prob = val$mlb_prob,
    expected_war = val$expected_war,
    expected_surplus = val$expected_surplus
  )
})

print("Prospect Value Chart (in millions)")
print(valuations %>%
  mutate(expected_surplus = expected_surplus / 1e6) %>%
  mutate(across(where(is.numeric), ~round(., 2))))

# Visualize
ggplot(valuations, aes(x = rank, y = expected_surplus / 1e6)) +
  geom_line(color = "blue", size = 1.2) +
  geom_point(color = "red", size = 3) +
  scale_x_reverse() +
  labs(
    title = "Prospect Value Curve",
    subtitle = "Expected surplus value by prospect ranking",
    x = "Prospect Rank (lower is better)",
    y = "Expected Surplus Value ($M)",
    caption = "Assumes 1 year to MLB, 10% discount rate"
  ) +
  theme_minimal() +
  theme(plot.title = element_text(face = "bold", size = 14))
Python
import numpy as np
import pandas as pd
from scipy.stats import beta, norm
import matplotlib.pyplot as plt
import seaborn as sns

class ProspectValuationSystem:
    """
    Advanced prospect valuation using probabilistic modeling
    """

    def __init__(self, dollars_per_war=8e6, discount_rate=0.10):
        self.dollars_per_war = dollars_per_war
        self.discount_rate = discount_rate

        # Salary structure constants
        self.prearb_salaries = [700000, 750000, 800000]
        self.arb_percentages = [0.40, 0.60, 0.80]

    def estimate_mlb_probability(self, rank, org_rank=None, age=None, level=None):
        """
        Estimate probability of reaching MLB using multiple factors
        """
        # Base probability from ranking
        if rank <= 10:
            base_prob = 0.75
        elif rank <= 30:
            base_prob = 0.65
        elif rank <= 50:
            base_prob = 0.55
        elif rank <= 100:
            base_prob = 0.45
        else:
            base_prob = 0.25

        # Adjustments
        adjustments = 0

        # Organization rank adjustment
        if org_rank is not None:
            if org_rank == 1:
                adjustments += 0.05
            elif org_rank <= 3:
                adjustments += 0.03

        # Age adjustment (younger = more risk but more upside)
        if age is not None:
            if age < 20:
                adjustments -= 0.05  # More developmental risk
            elif age > 24:
                adjustments -= 0.10  # Less time to develop

        # Level adjustment
        if level is not None:
            level_adjustments = {
                'MLB': 0.95,  # Already there
                'AAA': 0.15,
                'AA': 0.05,
                'A+': 0.00,
                'A': -0.05,
                'Rookie': -0.10
            }
            adjustments += level_adjustments.get(level, 0)

        return np.clip(base_prob + adjustments, 0.05, 0.95)

    def simulate_war_distribution(self, rank, n_simulations=10000):
        """
        Simulate distribution of potential WAR outcomes
        Uses beta distribution to model performance uncertainty
        """
        # Expected WAR by rank
        if rank <= 10:
            mean_war = 3.5
            std_war = 1.5
        elif rank <= 30:
            mean_war = 2.5
            std_war = 1.2
        elif rank <= 50:
            mean_war = 2.0
            std_war = 1.0
        elif rank <= 100:
            mean_war = 1.5
            std_war = 0.8
        else:
            mean_war = 0.8
            std_war = 0.6

        # Simulate from normal distribution, bounded at 0
        war_simulations = np.random.normal(mean_war, std_war, n_simulations)
        war_simulations = np.maximum(war_simulations, 0)

        return war_simulations

    def calculate_surplus_value(self, war_per_year, years_to_mlb=1, control_years=6):
        """
        Calculate surplus value for a given performance level
        """
        total_surplus = 0

        for year in range(1, control_years + 1):
            # Discount factor
            discount_factor = (1 + self.discount_rate) ** (years_to_mlb + year - 1)

            # Production value
            production_value = war_per_year * self.dollars_per_war

            # Cost
            if year <= 3:
                cost = self.prearb_salaries[year - 1]
            else:
                cost = war_per_year * self.dollars_per_war * self.arb_percentages[year - 4]

            # Surplus
            surplus = (production_value - cost) / discount_factor
            total_surplus += surplus

        return total_surplus

    def value_prospect(self, rank, years_to_mlb=1, position='generic',
                       org_rank=None, age=None, level=None, n_simulations=10000):
        """
        Complete prospect valuation with uncertainty
        """
        # MLB arrival probability
        mlb_prob = self.estimate_mlb_probability(rank, org_rank, age, level)

        # Simulate WAR outcomes
        war_simulations = self.simulate_war_distribution(rank, n_simulations)

        # Calculate surplus for each simulation
        surplus_simulations = np.array([
            self.calculate_surplus_value(war, years_to_mlb)
            for war in war_simulations
        ])

        # Apply MLB probability
        expected_surplus_simulations = surplus_simulations * mlb_prob

        return {
            'rank': rank,
            'mlb_probability': mlb_prob,
            'mean_war': np.mean(war_simulations),
            'median_war': np.median(war_simulations),
            'p90_war': np.percentile(war_simulations, 90),
            'p10_war': np.percentile(war_simulations, 10),
            'mean_surplus': np.mean(surplus_simulations),
            'expected_surplus': np.mean(expected_surplus_simulations),
            'median_surplus': np.median(expected_surplus_simulations),
            'p90_surplus': np.percentile(expected_surplus_simulations, 90),
            'p10_surplus': np.percentile(expected_surplus_simulations, 10)
        }

    def value_prospect_portfolio(self, prospects):
        """
        Value a portfolio of prospects (for multi-player trades)
        """
        results = []

        for prospect in prospects:
            valuation = self.value_prospect(**prospect)
            valuation['name'] = prospect.get('name', f"Prospect {prospect['rank']}")
            results.append(valuation)

        df = pd.DataFrame(results)

        # Portfolio statistics
        portfolio_value = df['expected_surplus'].sum()

        return df, portfolio_value

# Example usage
valuator = ProspectValuationSystem()

# Value a single top prospect
prospect_value = valuator.value_prospect(
    rank=15,
    years_to_mlb=1,
    org_rank=2,
    age=21,
    level='AA'
)

print("Prospect Valuation Report")
print("=" * 50)
print(f"MLB Probability: {prospect_value['mlb_probability']:.1%}")
print(f"\nProjected Performance:")
print(f"  Mean WAR: {prospect_value['mean_war']:.2f}")
print(f"  90th Percentile: {prospect_value['p90_war']:.2f}")
print(f"  10th Percentile: {prospect_value['p10_war']:.2f}")
print(f"\nSurplus Value:")
print(f"  Expected: ${prospect_value['expected_surplus']/1e6:.2f}M")
print(f"  90th Percentile: ${prospect_value['p90_surplus']/1e6:.2f}M")
print(f"  10th Percentile: ${prospect_value['p10_surplus']/1e6:.2f}M")

# Value a prospect package (typical trade scenario)
prospect_package = [
    {'name': 'Top Pitching Prospect', 'rank': 12, 'years_to_mlb': 1, 'age': 20, 'level': 'AA'},
    {'name': 'Shortstop Prospect', 'rank': 45, 'years_to_mlb': 2, 'age': 19, 'level': 'A+'},
    {'name': 'Outfield Lottery Ticket', 'rank': 85, 'years_to_mlb': 3, 'age': 18, 'level': 'A'}
]

portfolio_df, total_value = valuator.value_prospect_portfolio(prospect_package)

print("\n\nProspect Package Valuation")
print("=" * 50)
print(portfolio_df[['name', 'rank', 'mlb_probability', 'mean_war', 'expected_surplus']].to_string(index=False))
print(f"\nTotal Package Value: ${total_value/1e6:.2f}M")

29.3 Surplus Value Calculations

Surplus value is the cornerstone of modern baseball economics. It represents the difference between what a player produces and what they cost, allowing teams to compare the value of pre-arbitration prospects, arbitration-eligible players, and free agents.

The Surplus Value Formula

For any player, surplus value over their remaining team control is:

Surplus = Σ(Expected WAR × $/WAR - Salary) / (1 + r)^t

Where:


  • Expected WAR is projected wins above replacement

  • $/WAR is the market rate (approximately $8M per WAR in 2024)

  • Salary is the player's actual cost

  • r is the discount rate (typically 8-12%)

  • t is years in the future

Comparing Player Values

library(tidyverse)
library(knitr)

# Surplus value calculator
calculate_surplus <- function(player_name, war_projections, salaries,
                             dollars_per_war = 8000000,
                             discount_rate = 0.10) {

  years <- length(war_projections)
  surplus_by_year <- numeric(years)

  for (i in 1:years) {
    market_value <- war_projections[i] * dollars_per_war
    cost <- salaries[i]
    discount_factor <- (1 + discount_rate) ^ (i - 1)

    surplus_by_year[i] <- (market_value - cost) / discount_factor
  }

  total_surplus <- sum(surplus_by_year)

  return(list(
    player = player_name,
    war_projections = war_projections,
    salaries = salaries,
    surplus_by_year = surplus_by_year,
    total_surplus = total_surplus
  ))
}

# Example: Compare different player acquisition scenarios

# Scenario 1: Young controllable starter (3 years pre-arb, 3 years arb)
young_ace <- calculate_surplus(
  "Young Ace (Pre-Arb)",
  war_projections = c(4.0, 4.5, 4.5, 4.0, 3.5, 3.0),
  salaries = c(750000, 800000, 850000, 15000000, 20000000, 25000000)
)

# Scenario 2: Established starter (2 years arb, then free agent)
established_ace <- calculate_surplus(
  "Established Ace (Arb)",
  war_projections = c(4.5, 4.0),
  salaries = c(18000000, 22000000)
)

# Scenario 3: Free agent rental (half season)
rental_starter <- calculate_surplus(
  "Rental Starter (FA)",
  war_projections = c(1.5),  # Half season
  salaries = c(8000000)  # Prorated salary
)

# Scenario 4: Top prospect (expected value)
top_prospect <- calculate_surplus(
  "Top Prospect (Top-25)",
  war_projections = c(0, 2.0, 3.0, 3.5, 3.5, 3.0, 2.5),  # One year away
  salaries = c(0, 750000, 800000, 850000, 12000000, 16000000, 20000000)
)
# Adjust for 65% MLB arrival probability
top_prospect$total_surplus <- top_prospect$total_surplus * 0.65

# Create comparison table
comparison <- tibble(
  Player = c(young_ace$player, established_ace$player,
             rental_starter$player, top_prospect$player),
  Years = c(length(young_ace$war_projections),
            length(established_ace$war_projections),
            length(rental_starter$war_projections),
            length(top_prospect$war_projections)),
  Total_WAR = c(sum(young_ace$war_projections),
                sum(established_ace$war_projections),
                sum(rental_starter$war_projections),
                sum(top_prospect$war_projections) * 0.65),
  Total_Salary = c(sum(young_ace$salaries),
                   sum(established_ace$salaries),
                   sum(rental_starter$salaries),
                   sum(top_prospect$salaries) * 0.65),
  Surplus_Value = c(young_ace$total_surplus,
                    established_ace$total_surplus,
                    rental_starter$total_surplus,
                    top_prospect$total_surplus)
) %>%
  mutate(
    Total_Salary = Total_Salary / 1e6,
    Surplus_Value = Surplus_Value / 1e6,
    $/WAR = Total_Salary / Total_WAR
  )

print("Player Value Comparison")
print(comparison %>%
  mutate(across(where(is.numeric), ~round(., 2))) %>%
  knitr::kable(col.names = c("Player Type", "Years", "Total WAR",
                             "Salary ($M)", "Surplus ($M)", "Cost per WAR ($M)")))

# Visualize surplus value by player type
ggplot(comparison, aes(x = reorder(Player, -Surplus_Value), y = Surplus_Value)) +
  geom_col(aes(fill = Player), show.legend = FALSE) +
  geom_text(aes(label = sprintf("$%.1fM", Surplus_Value)),
            vjust = -0.5, fontface = "bold") +
  labs(
    title = "Surplus Value Comparison",
    subtitle = "Different acquisition strategies for starting pitching",
    x = NULL,
    y = "Surplus Value ($ Millions)"
  ) +
  scale_fill_brewer(palette = "Set2") +
  theme_minimal() +
  theme(
    plot.title = element_text(face = "bold", size = 14),
    axis.text.x = element_text(angle = 20, hjust = 1)
  )

Python Implementation: Trade Value Matcher

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from itertools import combinations

class TradeValueMatcher:
    """
    System for matching trade values between teams
    """

    def __init__(self, dollars_per_war=8e6, discount_rate=0.10):
        self.dollars_per_war = dollars_per_war
        self.discount_rate = discount_rate

    def calculate_player_surplus(self, war_projections, salaries, mlb_prob=1.0):
        """
        Calculate total surplus value for a player
        """
        surplus_by_year = []

        for i, (war, salary) in enumerate(zip(war_projections, salaries)):
            market_value = war * self.dollars_per_war
            discount_factor = (1 + self.discount_rate) ** i
            surplus = (market_value - salary) / discount_factor
            surplus_by_year.append(surplus)

        total_surplus = sum(surplus_by_year) * mlb_prob

        return {
            'total_surplus': total_surplus,
            'surplus_by_year': surplus_by_year,
            'npv_war': sum([w / (1 + self.discount_rate) ** i
                           for i, w in enumerate(war_projections)]) * mlb_prob,
            'npv_salary': sum([s / (1 + self.discount_rate) ** i
                              for i, s in enumerate(salaries)]) * mlb_prob
        }

    def find_balanced_trades(self, team_a_players, team_b_players,
                            tolerance=5e6, max_players=3):
        """
        Find balanced trade combinations between two teams

        Parameters:
        -----------
        team_a_players : list of dict
            Players available from team A
        team_b_players : list of dict
            Players available from team B
        tolerance : float
            Maximum value difference to consider balanced (default $5M)
        max_players : int
            Maximum players per side
        """
        balanced_trades = []

        # Generate combinations from each team
        for a_size in range(1, min(max_players + 1, len(team_a_players) + 1)):
            for b_size in range(1, min(max_players + 1, len(team_b_players) + 1)):

                a_combos = combinations(range(len(team_a_players)), a_size)
                b_combos = combinations(range(len(team_b_players)), b_size)

                for a_indices in a_combos:
                    for b_indices in b_combos:
                        # Calculate total value for each side
                        a_value = sum([team_a_players[i]['surplus'] for i in a_indices])
                        b_value = sum([team_b_players[i]['surplus'] for i in b_indices])

                        # Check if balanced
                        value_diff = abs(a_value - b_value)

                        if value_diff <= tolerance:
                            balanced_trades.append({
                                'team_a_gives': [team_a_players[i]['name'] for i in a_indices],
                                'team_a_value': a_value,
                                'team_b_gives': [team_b_players[i]['name'] for i in b_indices],
                                'team_b_value': b_value,
                                'value_difference': value_diff,
                                'balance_pct': min(a_value, b_value) / max(a_value, b_value)
                            })

        # Sort by most balanced
        balanced_trades.sort(key=lambda x: x['value_difference'])

        return balanced_trades

# Example: Real trade analysis (2023 Dylan Cease trade framework)
matcher = TradeValueMatcher()

# Chicago White Sox perspective: Trading Dylan Cease
# Cease: 2 years of control, projected 3.5-4.0 WAR per year
cease_surplus = matcher.calculate_player_surplus(
    war_projections=[3.5, 4.0],
    salaries=[8e6, 12e6]  # Arbitration estimates
)

print("Dylan Cease Surplus Value Analysis")
print("=" * 50)
print(f"Total Surplus Value: ${cease_surplus['total_surplus']/1e6:.2f}M")
print(f"NPV of WAR: {cease_surplus['npv_war']:.2f}")
print(f"NPV of Salary: ${cease_surplus['npv_salary']/1e6:.2f}M")

# What would White Sox need in return? (~$35-40M in surplus)
# Typical package: Top-50 prospect + Top-100 prospect + lottery ticket

white_sox_assets = [
    {'name': 'Dylan Cease', 'surplus': cease_surplus['total_surplus']}
]

# Potential return package
padres_prospects = [
    {
        'name': 'Top Pitching Prospect (Rank 25)',
        'surplus': matcher.calculate_player_surplus(
            war_projections=[0, 2.5, 3.0, 3.5, 3.5, 3.0, 2.5],
            salaries=[0, 750000, 800000, 850000, 11e6, 15e6, 19e6],
            mlb_prob=0.65
        )['total_surplus']
    },
    {
        'name': 'Shortstop Prospect (Rank 60)',
        'surplus': matcher.calculate_player_surplus(
            war_projections=[0, 0, 1.5, 2.0, 2.5, 2.5, 2.0],
            salaries=[0, 0, 750000, 800000, 850000, 9e6, 12e6],
            mlb_prob=0.50
        )['total_surplus']
    },
    {
        'name': 'Power Arm Lottery Ticket (Rank 120)',
        'surplus': matcher.calculate_player_surplus(
            war_projections=[0, 0, 0, 1.0, 1.5, 2.0, 2.0],
            salaries=[0, 0, 0, 750000, 800000, 850000, 7e6],
            mlb_prob=0.35
        )['total_surplus']
    },
    {
        'name': 'MLB Ready Utility Player',
        'surplus': matcher.calculate_player_surplus(
            war_projections=[1.5, 1.5, 1.5],
            salaries=[750000, 800000, 850000],
            mlb_prob=1.0
        )['total_surplus']
    }
]

print("\n\nProspect Return Package Values:")
print("=" * 50)
for prospect in padres_prospects:
    print(f"{prospect['name']}: ${prospect['surplus']/1e6:.2f}M")

# Find balanced trade packages
balanced_trades = matcher.find_balanced_trades(
    white_sox_assets,
    padres_prospects,
    tolerance=8e6,
    max_players=4
)

print("\n\nBalanced Trade Scenarios:")
print("=" * 50)
for i, trade in enumerate(balanced_trades[:5], 1):  # Top 5 most balanced
    print(f"\nScenario {i}:")
    print(f"  White Sox give: {', '.join(trade['team_a_gives'])}")
    print(f"  White Sox value: ${trade['team_a_value']/1e6:.2f}M")
    print(f"  Padres give: {', '.join(trade['team_b_gives'])}")
    print(f"  Padres value: ${trade['team_b_value']/1e6:.2f}M")
    print(f"  Difference: ${trade['value_difference']/1e6:.2f}M ({trade['balance_pct']:.1%} balanced)")
R
library(tidyverse)
library(knitr)

# Surplus value calculator
calculate_surplus <- function(player_name, war_projections, salaries,
                             dollars_per_war = 8000000,
                             discount_rate = 0.10) {

  years <- length(war_projections)
  surplus_by_year <- numeric(years)

  for (i in 1:years) {
    market_value <- war_projections[i] * dollars_per_war
    cost <- salaries[i]
    discount_factor <- (1 + discount_rate) ^ (i - 1)

    surplus_by_year[i] <- (market_value - cost) / discount_factor
  }

  total_surplus <- sum(surplus_by_year)

  return(list(
    player = player_name,
    war_projections = war_projections,
    salaries = salaries,
    surplus_by_year = surplus_by_year,
    total_surplus = total_surplus
  ))
}

# Example: Compare different player acquisition scenarios

# Scenario 1: Young controllable starter (3 years pre-arb, 3 years arb)
young_ace <- calculate_surplus(
  "Young Ace (Pre-Arb)",
  war_projections = c(4.0, 4.5, 4.5, 4.0, 3.5, 3.0),
  salaries = c(750000, 800000, 850000, 15000000, 20000000, 25000000)
)

# Scenario 2: Established starter (2 years arb, then free agent)
established_ace <- calculate_surplus(
  "Established Ace (Arb)",
  war_projections = c(4.5, 4.0),
  salaries = c(18000000, 22000000)
)

# Scenario 3: Free agent rental (half season)
rental_starter <- calculate_surplus(
  "Rental Starter (FA)",
  war_projections = c(1.5),  # Half season
  salaries = c(8000000)  # Prorated salary
)

# Scenario 4: Top prospect (expected value)
top_prospect <- calculate_surplus(
  "Top Prospect (Top-25)",
  war_projections = c(0, 2.0, 3.0, 3.5, 3.5, 3.0, 2.5),  # One year away
  salaries = c(0, 750000, 800000, 850000, 12000000, 16000000, 20000000)
)
# Adjust for 65% MLB arrival probability
top_prospect$total_surplus <- top_prospect$total_surplus * 0.65

# Create comparison table
comparison <- tibble(
  Player = c(young_ace$player, established_ace$player,
             rental_starter$player, top_prospect$player),
  Years = c(length(young_ace$war_projections),
            length(established_ace$war_projections),
            length(rental_starter$war_projections),
            length(top_prospect$war_projections)),
  Total_WAR = c(sum(young_ace$war_projections),
                sum(established_ace$war_projections),
                sum(rental_starter$war_projections),
                sum(top_prospect$war_projections) * 0.65),
  Total_Salary = c(sum(young_ace$salaries),
                   sum(established_ace$salaries),
                   sum(rental_starter$salaries),
                   sum(top_prospect$salaries) * 0.65),
  Surplus_Value = c(young_ace$total_surplus,
                    established_ace$total_surplus,
                    rental_starter$total_surplus,
                    top_prospect$total_surplus)
) %>%
  mutate(
    Total_Salary = Total_Salary / 1e6,
    Surplus_Value = Surplus_Value / 1e6,
    $/WAR = Total_Salary / Total_WAR
  )

print("Player Value Comparison")
print(comparison %>%
  mutate(across(where(is.numeric), ~round(., 2))) %>%
  knitr::kable(col.names = c("Player Type", "Years", "Total WAR",
                             "Salary ($M)", "Surplus ($M)", "Cost per WAR ($M)")))

# Visualize surplus value by player type
ggplot(comparison, aes(x = reorder(Player, -Surplus_Value), y = Surplus_Value)) +
  geom_col(aes(fill = Player), show.legend = FALSE) +
  geom_text(aes(label = sprintf("$%.1fM", Surplus_Value)),
            vjust = -0.5, fontface = "bold") +
  labs(
    title = "Surplus Value Comparison",
    subtitle = "Different acquisition strategies for starting pitching",
    x = NULL,
    y = "Surplus Value ($ Millions)"
  ) +
  scale_fill_brewer(palette = "Set2") +
  theme_minimal() +
  theme(
    plot.title = element_text(face = "bold", size = 14),
    axis.text.x = element_text(angle = 20, hjust = 1)
  )
Python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from itertools import combinations

class TradeValueMatcher:
    """
    System for matching trade values between teams
    """

    def __init__(self, dollars_per_war=8e6, discount_rate=0.10):
        self.dollars_per_war = dollars_per_war
        self.discount_rate = discount_rate

    def calculate_player_surplus(self, war_projections, salaries, mlb_prob=1.0):
        """
        Calculate total surplus value for a player
        """
        surplus_by_year = []

        for i, (war, salary) in enumerate(zip(war_projections, salaries)):
            market_value = war * self.dollars_per_war
            discount_factor = (1 + self.discount_rate) ** i
            surplus = (market_value - salary) / discount_factor
            surplus_by_year.append(surplus)

        total_surplus = sum(surplus_by_year) * mlb_prob

        return {
            'total_surplus': total_surplus,
            'surplus_by_year': surplus_by_year,
            'npv_war': sum([w / (1 + self.discount_rate) ** i
                           for i, w in enumerate(war_projections)]) * mlb_prob,
            'npv_salary': sum([s / (1 + self.discount_rate) ** i
                              for i, s in enumerate(salaries)]) * mlb_prob
        }

    def find_balanced_trades(self, team_a_players, team_b_players,
                            tolerance=5e6, max_players=3):
        """
        Find balanced trade combinations between two teams

        Parameters:
        -----------
        team_a_players : list of dict
            Players available from team A
        team_b_players : list of dict
            Players available from team B
        tolerance : float
            Maximum value difference to consider balanced (default $5M)
        max_players : int
            Maximum players per side
        """
        balanced_trades = []

        # Generate combinations from each team
        for a_size in range(1, min(max_players + 1, len(team_a_players) + 1)):
            for b_size in range(1, min(max_players + 1, len(team_b_players) + 1)):

                a_combos = combinations(range(len(team_a_players)), a_size)
                b_combos = combinations(range(len(team_b_players)), b_size)

                for a_indices in a_combos:
                    for b_indices in b_combos:
                        # Calculate total value for each side
                        a_value = sum([team_a_players[i]['surplus'] for i in a_indices])
                        b_value = sum([team_b_players[i]['surplus'] for i in b_indices])

                        # Check if balanced
                        value_diff = abs(a_value - b_value)

                        if value_diff <= tolerance:
                            balanced_trades.append({
                                'team_a_gives': [team_a_players[i]['name'] for i in a_indices],
                                'team_a_value': a_value,
                                'team_b_gives': [team_b_players[i]['name'] for i in b_indices],
                                'team_b_value': b_value,
                                'value_difference': value_diff,
                                'balance_pct': min(a_value, b_value) / max(a_value, b_value)
                            })

        # Sort by most balanced
        balanced_trades.sort(key=lambda x: x['value_difference'])

        return balanced_trades

# Example: Real trade analysis (2023 Dylan Cease trade framework)
matcher = TradeValueMatcher()

# Chicago White Sox perspective: Trading Dylan Cease
# Cease: 2 years of control, projected 3.5-4.0 WAR per year
cease_surplus = matcher.calculate_player_surplus(
    war_projections=[3.5, 4.0],
    salaries=[8e6, 12e6]  # Arbitration estimates
)

print("Dylan Cease Surplus Value Analysis")
print("=" * 50)
print(f"Total Surplus Value: ${cease_surplus['total_surplus']/1e6:.2f}M")
print(f"NPV of WAR: {cease_surplus['npv_war']:.2f}")
print(f"NPV of Salary: ${cease_surplus['npv_salary']/1e6:.2f}M")

# What would White Sox need in return? (~$35-40M in surplus)
# Typical package: Top-50 prospect + Top-100 prospect + lottery ticket

white_sox_assets = [
    {'name': 'Dylan Cease', 'surplus': cease_surplus['total_surplus']}
]

# Potential return package
padres_prospects = [
    {
        'name': 'Top Pitching Prospect (Rank 25)',
        'surplus': matcher.calculate_player_surplus(
            war_projections=[0, 2.5, 3.0, 3.5, 3.5, 3.0, 2.5],
            salaries=[0, 750000, 800000, 850000, 11e6, 15e6, 19e6],
            mlb_prob=0.65
        )['total_surplus']
    },
    {
        'name': 'Shortstop Prospect (Rank 60)',
        'surplus': matcher.calculate_player_surplus(
            war_projections=[0, 0, 1.5, 2.0, 2.5, 2.5, 2.0],
            salaries=[0, 0, 750000, 800000, 850000, 9e6, 12e6],
            mlb_prob=0.50
        )['total_surplus']
    },
    {
        'name': 'Power Arm Lottery Ticket (Rank 120)',
        'surplus': matcher.calculate_player_surplus(
            war_projections=[0, 0, 0, 1.0, 1.5, 2.0, 2.0],
            salaries=[0, 0, 0, 750000, 800000, 850000, 7e6],
            mlb_prob=0.35
        )['total_surplus']
    },
    {
        'name': 'MLB Ready Utility Player',
        'surplus': matcher.calculate_player_surplus(
            war_projections=[1.5, 1.5, 1.5],
            salaries=[750000, 800000, 850000],
            mlb_prob=1.0
        )['total_surplus']
    }
]

print("\n\nProspect Return Package Values:")
print("=" * 50)
for prospect in padres_prospects:
    print(f"{prospect['name']}: ${prospect['surplus']/1e6:.2f}M")

# Find balanced trade packages
balanced_trades = matcher.find_balanced_trades(
    white_sox_assets,
    padres_prospects,
    tolerance=8e6,
    max_players=4
)

print("\n\nBalanced Trade Scenarios:")
print("=" * 50)
for i, trade in enumerate(balanced_trades[:5], 1):  # Top 5 most balanced
    print(f"\nScenario {i}:")
    print(f"  White Sox give: {', '.join(trade['team_a_gives'])}")
    print(f"  White Sox value: ${trade['team_a_value']/1e6:.2f}M")
    print(f"  Padres give: {', '.join(trade['team_b_gives'])}")
    print(f"  Padres value: ${trade['team_b_value']/1e6:.2f}M")
    print(f"  Difference: ${trade['value_difference']/1e6:.2f}M ({trade['balance_pct']:.1%} balanced)")

29.4 Contract Optimization & Arbitration Projections

Teams must navigate three distinct salary structures: pre-arbitration (league minimum), arbitration (years 3-6 of service), and free agency. Understanding arbitration mechanics and optimizing extension timing is crucial for competitive success.

Arbitration Salary Estimation

MLB's arbitration system is based on comparables: players with similar service time and performance statistics. The key statistics vary by position:

  • Pitchers: Wins, ERA, innings pitched, saves (for relievers)
  • Hitters: Batting average, home runs, RBIs, games played

R Implementation: Arbitration Projection Model

library(tidyverse)
library(randomForest)

# Simulate arbitration case data
set.seed(123)

generate_arb_data <- function(n = 500) {
  tibble(
    player_id = 1:n,
    position = sample(c("SP", "RP", "C", "1B", "2B", "3B", "SS", "OF"), n, replace = TRUE),
    service_time = runif(n, 2.0, 5.9),
    arb_year = ceiling(service_time - 2),
    war_last_year = pmax(0, rnorm(n, 2.5, 1.5)),
    war_3yr_avg = pmax(0, rnorm(n, 2.3, 1.2)),

    # Hitter stats
    avg = ifelse(position %in% c("SP", "RP"), NA,
                 pmax(0.180, pmin(0.360, rnorm(n, 0.265, 0.035)))),
    hr = ifelse(position %in% c("SP", "RP"), NA,
                pmax(0, rnorm(n, 20, 12))),
    rbi = ifelse(position %in% c("SP", "RP"), NA,
                 pmax(0, rnorm(n, 65, 30))),

    # Pitcher stats
    wins = ifelse(position == "SP", pmax(0, rnorm(n, 10, 5)),
                  ifelse(position == "RP", pmax(0, rnorm(n, 4, 3)), NA)),
    era = ifelse(position %in% c("SP", "RP"),
                 pmax(2.0, pmin(6.0, rnorm(n, 4.0, 0.9))), NA),
    innings = ifelse(position == "SP", pmax(0, rnorm(n, 150, 50)),
                    ifelse(position == "RP", pmax(0, rnorm(n, 65, 20)), NA)),
    saves = ifelse(position == "RP", pmax(0, rnorm(n, 15, 15)), NA)
  ) %>%
    mutate(
      # Generate realistic salary based on performance
      base_salary = case_when(
        arb_year == 1 ~ 1000000 + war_last_year * 1500000,
        arb_year == 2 ~ 2500000 + war_last_year * 2500000,
        arb_year == 3 ~ 5000000 + war_last_year * 3500000,
        TRUE ~ 8000000 + war_last_year * 4000000
      ),
      salary = base_salary + rnorm(n, 0, base_salary * 0.15)
    ) %>%
    filter(salary > 0)
}

arb_data <- generate_arb_data()

# Build arbitration model
build_arb_model <- function(data) {
  # Separate models for pitchers and hitters
  pitcher_data <- data %>%
    filter(position %in% c("SP", "RP")) %>%
    select(salary, service_time, arb_year, war_last_year, war_3yr_avg,
           wins, era, innings, saves) %>%
    na.omit()

  hitter_data <- data %>%
    filter(!position %in% c("SP", "RP")) %>%
    select(salary, service_time, arb_year, war_last_year, war_3yr_avg,
           avg, hr, rbi) %>%
    na.omit()

  # Train random forest models
  pitcher_model <- randomForest(salary ~ ., data = pitcher_data, ntree = 100)
  hitter_model <- randomForest(salary ~ ., data = hitter_data, ntree = 100)

  list(pitcher = pitcher_model, hitter = hitter_model)
}

models <- build_arb_model(arb_data)

# Projection function
project_arb_salary <- function(player_type, service_time, war_last_year,
                               war_3yr_avg, ...) {
  arb_year <- ceiling(service_time - 2)

  if (player_type %in% c("SP", "RP")) {
    # Pitcher
    new_data <- tibble(
      service_time = service_time,
      arb_year = arb_year,
      war_last_year = war_last_year,
      war_3yr_avg = war_3yr_avg,
      ...
    )
    prediction <- predict(models$pitcher, new_data)
  } else {
    # Hitter
    new_data <- tibble(
      service_time = service_time,
      arb_year = arb_year,
      war_last_year = war_last_year,
      war_3yr_avg = war_3yr_avg,
      ...
    )
    prediction <- predict(models$hitter, new_data)
  }

  return(as.numeric(prediction))
}

# Example projections
cat("Arbitration Salary Projections\n")
cat("=" * 50, "\n")

# Star shortstop entering first arb year
ss_salary <- project_arb_salary(
  "SS",
  service_time = 3.1,
  war_last_year = 5.2,
  war_3yr_avg = 4.1,
  avg = 0.285,
  hr = 28,
  rbi = 85
)
cat(sprintf("Star SS (3.1 years, 5.2 WAR): $%.2fM\n", ss_salary / 1e6))

# Solid starter entering second arb year
sp_salary <- project_arb_salary(
  "SP",
  service_time = 4.2,
  war_last_year = 3.5,
  war_3yr_avg = 3.2,
  wins = 13,
  era = 3.45,
  innings = 185,
  saves = NA
)
cat(sprintf("Solid SP (4.2 years, 3.5 WAR): $%.2fM\n", sp_salary / 1e6))

# Closer entering third arb year
rp_salary <- project_arb_salary(
  "RP",
  service_time = 5.1,
  war_last_year = 2.1,
  war_3yr_avg = 2.0,
  wins = 3,
  era = 2.85,
  innings = 62,
  saves = 35
)
cat(sprintf("Elite Closer (5.1 years, 2.1 WAR): $%.2fM\n", rp_salary / 1e6))

Python Implementation: Extension Optimization

import numpy as np
import pandas as pd
from scipy.optimize import minimize
import matplotlib.pyplot as plt

class ExtensionOptimizer:
    """
    Optimize contract extension timing and structure
    """

    def __init__(self, dollars_per_war=8e6, discount_rate=0.10):
        self.dollars_per_war = dollars_per_war
        self.discount_rate = discount_rate

    def project_arb_salaries(self, war_projections, service_time):
        """
        Project arbitration salaries for remaining arb years

        Uses simplified arbitration formula based on WAR
        """
        arb_salaries = []
        current_service = service_time

        for war in war_projections:
            years_since_arb = current_service - 3.0

            if current_service < 3.0:
                # Pre-arbitration
                salary = 750000
            elif current_service < 6.0:
                # Arbitration years
                # Rough formula: increases with service time and performance
                arb_year = int(years_since_arb) + 1

                if arb_year == 1:
                    pct_market = 0.40
                elif arb_year == 2:
                    pct_market = 0.60
                else:
                    pct_market = 0.80

                salary = max(750000, war * self.dollars_per_war * pct_market)
            else:
                # Free agent - full market value
                salary = war * self.dollars_per_war

            arb_salaries.append(salary)
            current_service += 1.0

        return arb_salaries

    def calculate_extension_value(self, war_projections, service_time,
                                  extension_years, extension_aav,
                                  buyout_arb_years=0):
        """
        Calculate the value of an extension vs. year-to-year arbitration

        Parameters:
        -----------
        war_projections : list
            WAR projection for each year
        service_time : float
            Current service time
        extension_years : int
            Length of extension
        extension_aav : float
            Average annual value of extension
        buyout_arb_years : int
            Number of arb years bought out by extension
        """
        # Arb salaries if no extension
        arb_salaries = self.project_arb_salaries(war_projections, service_time)

        # Calculate costs and values under each scenario

        # Scenario 1: No extension (year-to-year)
        no_extension_cost = 0
        no_extension_value = 0

        for i, (war, salary) in enumerate(zip(war_projections, arb_salaries)):
            discount = (1 + self.discount_rate) ** i
            no_extension_cost += salary / discount
            no_extension_value += (war * self.dollars_per_war) / discount

        # Scenario 2: Extension
        extension_cost = 0
        extension_value = 0

        for i in range(extension_years):
            if i < len(war_projections):
                war = war_projections[i]
            else:
                # Assume decline if projection doesn't cover full extension
                war = war_projections[-1] * 0.90 ** (i - len(war_projections) + 1)

            discount = (1 + self.discount_rate) ** i
            extension_cost += extension_aav / discount
            extension_value += (war * self.dollars_per_war) / discount

        # Team perspective: savings vs. year-to-year
        team_savings = no_extension_cost - extension_cost

        # Player perspective: guaranteed money vs. risk
        player_certainty_value = extension_cost - no_extension_cost

        # Surplus value under each scenario
        no_extension_surplus = no_extension_value - no_extension_cost
        extension_surplus = extension_value - extension_cost

        return {
            'no_extension_cost': no_extension_cost,
            'no_extension_value': no_extension_value,
            'no_extension_surplus': no_extension_surplus,
            'extension_cost': extension_cost,
            'extension_value': extension_value,
            'extension_surplus': extension_surplus,
            'team_savings': team_savings,
            'player_certainty_value': player_certainty_value
        }

    def find_optimal_extension(self, war_projections, service_time,
                               max_years=8, max_aav=30e6):
        """
        Find the extension that maximizes team surplus while being acceptable to player
        """
        best_extension = None
        best_surplus = -np.inf

        # Grid search over extension parameters
        for years in range(3, max_years + 1):
            for aav in np.linspace(5e6, max_aav, 20):
                result = self.calculate_extension_value(
                    war_projections, service_time, years, aav
                )

                # Extension is acceptable if player gets premium over projected arb
                if result['player_certainty_value'] > 5e6:  # $5M+ premium required
                    if result['extension_surplus'] > best_surplus:
                        best_surplus = result['extension_surplus']
                        best_extension = {
                            'years': years,
                            'aav': aav,
                            'total_value': years * aav,
                            **result
                        }

        return best_extension

# Example: Evaluating an extension for a young star
optimizer = ExtensionOptimizer()

# Young star entering arbitration (e.g., Ronald Acuna type player)
# Currently 2.5 years service, projected 5+ WAR player
war_projections = [5.5, 6.0, 5.5, 5.0, 4.5, 4.0, 3.5, 3.0]
service_time = 2.5

print("Extension Analysis: Young Star Player")
print("=" * 60)
print(f"Current Service Time: {service_time} years")
print(f"WAR Projections: {war_projections[:5]}...")

# Compare specific extension scenarios
scenarios = [
    {'years': 5, 'aav': 15e6, 'name': 'Conservative (5yr/$75M)'},
    {'years': 7, 'aav': 20e6, 'name': 'Moderate (7yr/$140M)'},
    {'years': 8, 'aav': 25e6, 'name': 'Aggressive (8yr/$200M)'},
]

print("\nExtension Scenario Comparison:")
print("-" * 60)

for scenario in scenarios:
    result = optimizer.calculate_extension_value(
        war_projections, service_time,
        scenario['years'], scenario['aav']
    )

    print(f"\n{scenario['name']}")
    print(f"  Extension Surplus: ${result['extension_surplus']/1e6:.1f}M")
    print(f"  Team Savings vs. Arb: ${result['team_savings']/1e6:.1f}M")
    print(f"  Player Premium: ${result['player_certainty_value']/1e6:.1f}M")

# Find optimal extension
optimal = optimizer.find_optimal_extension(war_projections, service_time)

if optimal:
    print("\n\nOptimal Extension Found:")
    print("=" * 60)
    print(f"Structure: {optimal['years']} years, ${optimal['aav']/1e6:.1f}M AAV")
    print(f"Total Value: ${optimal['total_value']/1e6:.1f}M")
    print(f"Team Surplus: ${optimal['extension_surplus']/1e6:.1f}M")
    print(f"Team Savings: ${optimal['team_savings']/1e6:.1f}M")
    print(f"Player Premium: ${optimal['player_certainty_value']/1e6:.1f}M")
R
library(tidyverse)
library(randomForest)

# Simulate arbitration case data
set.seed(123)

generate_arb_data <- function(n = 500) {
  tibble(
    player_id = 1:n,
    position = sample(c("SP", "RP", "C", "1B", "2B", "3B", "SS", "OF"), n, replace = TRUE),
    service_time = runif(n, 2.0, 5.9),
    arb_year = ceiling(service_time - 2),
    war_last_year = pmax(0, rnorm(n, 2.5, 1.5)),
    war_3yr_avg = pmax(0, rnorm(n, 2.3, 1.2)),

    # Hitter stats
    avg = ifelse(position %in% c("SP", "RP"), NA,
                 pmax(0.180, pmin(0.360, rnorm(n, 0.265, 0.035)))),
    hr = ifelse(position %in% c("SP", "RP"), NA,
                pmax(0, rnorm(n, 20, 12))),
    rbi = ifelse(position %in% c("SP", "RP"), NA,
                 pmax(0, rnorm(n, 65, 30))),

    # Pitcher stats
    wins = ifelse(position == "SP", pmax(0, rnorm(n, 10, 5)),
                  ifelse(position == "RP", pmax(0, rnorm(n, 4, 3)), NA)),
    era = ifelse(position %in% c("SP", "RP"),
                 pmax(2.0, pmin(6.0, rnorm(n, 4.0, 0.9))), NA),
    innings = ifelse(position == "SP", pmax(0, rnorm(n, 150, 50)),
                    ifelse(position == "RP", pmax(0, rnorm(n, 65, 20)), NA)),
    saves = ifelse(position == "RP", pmax(0, rnorm(n, 15, 15)), NA)
  ) %>%
    mutate(
      # Generate realistic salary based on performance
      base_salary = case_when(
        arb_year == 1 ~ 1000000 + war_last_year * 1500000,
        arb_year == 2 ~ 2500000 + war_last_year * 2500000,
        arb_year == 3 ~ 5000000 + war_last_year * 3500000,
        TRUE ~ 8000000 + war_last_year * 4000000
      ),
      salary = base_salary + rnorm(n, 0, base_salary * 0.15)
    ) %>%
    filter(salary > 0)
}

arb_data <- generate_arb_data()

# Build arbitration model
build_arb_model <- function(data) {
  # Separate models for pitchers and hitters
  pitcher_data <- data %>%
    filter(position %in% c("SP", "RP")) %>%
    select(salary, service_time, arb_year, war_last_year, war_3yr_avg,
           wins, era, innings, saves) %>%
    na.omit()

  hitter_data <- data %>%
    filter(!position %in% c("SP", "RP")) %>%
    select(salary, service_time, arb_year, war_last_year, war_3yr_avg,
           avg, hr, rbi) %>%
    na.omit()

  # Train random forest models
  pitcher_model <- randomForest(salary ~ ., data = pitcher_data, ntree = 100)
  hitter_model <- randomForest(salary ~ ., data = hitter_data, ntree = 100)

  list(pitcher = pitcher_model, hitter = hitter_model)
}

models <- build_arb_model(arb_data)

# Projection function
project_arb_salary <- function(player_type, service_time, war_last_year,
                               war_3yr_avg, ...) {
  arb_year <- ceiling(service_time - 2)

  if (player_type %in% c("SP", "RP")) {
    # Pitcher
    new_data <- tibble(
      service_time = service_time,
      arb_year = arb_year,
      war_last_year = war_last_year,
      war_3yr_avg = war_3yr_avg,
      ...
    )
    prediction <- predict(models$pitcher, new_data)
  } else {
    # Hitter
    new_data <- tibble(
      service_time = service_time,
      arb_year = arb_year,
      war_last_year = war_last_year,
      war_3yr_avg = war_3yr_avg,
      ...
    )
    prediction <- predict(models$hitter, new_data)
  }

  return(as.numeric(prediction))
}

# Example projections
cat("Arbitration Salary Projections\n")
cat("=" * 50, "\n")

# Star shortstop entering first arb year
ss_salary <- project_arb_salary(
  "SS",
  service_time = 3.1,
  war_last_year = 5.2,
  war_3yr_avg = 4.1,
  avg = 0.285,
  hr = 28,
  rbi = 85
)
cat(sprintf("Star SS (3.1 years, 5.2 WAR): $%.2fM\n", ss_salary / 1e6))

# Solid starter entering second arb year
sp_salary <- project_arb_salary(
  "SP",
  service_time = 4.2,
  war_last_year = 3.5,
  war_3yr_avg = 3.2,
  wins = 13,
  era = 3.45,
  innings = 185,
  saves = NA
)
cat(sprintf("Solid SP (4.2 years, 3.5 WAR): $%.2fM\n", sp_salary / 1e6))

# Closer entering third arb year
rp_salary <- project_arb_salary(
  "RP",
  service_time = 5.1,
  war_last_year = 2.1,
  war_3yr_avg = 2.0,
  wins = 3,
  era = 2.85,
  innings = 62,
  saves = 35
)
cat(sprintf("Elite Closer (5.1 years, 2.1 WAR): $%.2fM\n", rp_salary / 1e6))
Python
import numpy as np
import pandas as pd
from scipy.optimize import minimize
import matplotlib.pyplot as plt

class ExtensionOptimizer:
    """
    Optimize contract extension timing and structure
    """

    def __init__(self, dollars_per_war=8e6, discount_rate=0.10):
        self.dollars_per_war = dollars_per_war
        self.discount_rate = discount_rate

    def project_arb_salaries(self, war_projections, service_time):
        """
        Project arbitration salaries for remaining arb years

        Uses simplified arbitration formula based on WAR
        """
        arb_salaries = []
        current_service = service_time

        for war in war_projections:
            years_since_arb = current_service - 3.0

            if current_service < 3.0:
                # Pre-arbitration
                salary = 750000
            elif current_service < 6.0:
                # Arbitration years
                # Rough formula: increases with service time and performance
                arb_year = int(years_since_arb) + 1

                if arb_year == 1:
                    pct_market = 0.40
                elif arb_year == 2:
                    pct_market = 0.60
                else:
                    pct_market = 0.80

                salary = max(750000, war * self.dollars_per_war * pct_market)
            else:
                # Free agent - full market value
                salary = war * self.dollars_per_war

            arb_salaries.append(salary)
            current_service += 1.0

        return arb_salaries

    def calculate_extension_value(self, war_projections, service_time,
                                  extension_years, extension_aav,
                                  buyout_arb_years=0):
        """
        Calculate the value of an extension vs. year-to-year arbitration

        Parameters:
        -----------
        war_projections : list
            WAR projection for each year
        service_time : float
            Current service time
        extension_years : int
            Length of extension
        extension_aav : float
            Average annual value of extension
        buyout_arb_years : int
            Number of arb years bought out by extension
        """
        # Arb salaries if no extension
        arb_salaries = self.project_arb_salaries(war_projections, service_time)

        # Calculate costs and values under each scenario

        # Scenario 1: No extension (year-to-year)
        no_extension_cost = 0
        no_extension_value = 0

        for i, (war, salary) in enumerate(zip(war_projections, arb_salaries)):
            discount = (1 + self.discount_rate) ** i
            no_extension_cost += salary / discount
            no_extension_value += (war * self.dollars_per_war) / discount

        # Scenario 2: Extension
        extension_cost = 0
        extension_value = 0

        for i in range(extension_years):
            if i < len(war_projections):
                war = war_projections[i]
            else:
                # Assume decline if projection doesn't cover full extension
                war = war_projections[-1] * 0.90 ** (i - len(war_projections) + 1)

            discount = (1 + self.discount_rate) ** i
            extension_cost += extension_aav / discount
            extension_value += (war * self.dollars_per_war) / discount

        # Team perspective: savings vs. year-to-year
        team_savings = no_extension_cost - extension_cost

        # Player perspective: guaranteed money vs. risk
        player_certainty_value = extension_cost - no_extension_cost

        # Surplus value under each scenario
        no_extension_surplus = no_extension_value - no_extension_cost
        extension_surplus = extension_value - extension_cost

        return {
            'no_extension_cost': no_extension_cost,
            'no_extension_value': no_extension_value,
            'no_extension_surplus': no_extension_surplus,
            'extension_cost': extension_cost,
            'extension_value': extension_value,
            'extension_surplus': extension_surplus,
            'team_savings': team_savings,
            'player_certainty_value': player_certainty_value
        }

    def find_optimal_extension(self, war_projections, service_time,
                               max_years=8, max_aav=30e6):
        """
        Find the extension that maximizes team surplus while being acceptable to player
        """
        best_extension = None
        best_surplus = -np.inf

        # Grid search over extension parameters
        for years in range(3, max_years + 1):
            for aav in np.linspace(5e6, max_aav, 20):
                result = self.calculate_extension_value(
                    war_projections, service_time, years, aav
                )

                # Extension is acceptable if player gets premium over projected arb
                if result['player_certainty_value'] > 5e6:  # $5M+ premium required
                    if result['extension_surplus'] > best_surplus:
                        best_surplus = result['extension_surplus']
                        best_extension = {
                            'years': years,
                            'aav': aav,
                            'total_value': years * aav,
                            **result
                        }

        return best_extension

# Example: Evaluating an extension for a young star
optimizer = ExtensionOptimizer()

# Young star entering arbitration (e.g., Ronald Acuna type player)
# Currently 2.5 years service, projected 5+ WAR player
war_projections = [5.5, 6.0, 5.5, 5.0, 4.5, 4.0, 3.5, 3.0]
service_time = 2.5

print("Extension Analysis: Young Star Player")
print("=" * 60)
print(f"Current Service Time: {service_time} years")
print(f"WAR Projections: {war_projections[:5]}...")

# Compare specific extension scenarios
scenarios = [
    {'years': 5, 'aav': 15e6, 'name': 'Conservative (5yr/$75M)'},
    {'years': 7, 'aav': 20e6, 'name': 'Moderate (7yr/$140M)'},
    {'years': 8, 'aav': 25e6, 'name': 'Aggressive (8yr/$200M)'},
]

print("\nExtension Scenario Comparison:")
print("-" * 60)

for scenario in scenarios:
    result = optimizer.calculate_extension_value(
        war_projections, service_time,
        scenario['years'], scenario['aav']
    )

    print(f"\n{scenario['name']}")
    print(f"  Extension Surplus: ${result['extension_surplus']/1e6:.1f}M")
    print(f"  Team Savings vs. Arb: ${result['team_savings']/1e6:.1f}M")
    print(f"  Player Premium: ${result['player_certainty_value']/1e6:.1f}M")

# Find optimal extension
optimal = optimizer.find_optimal_extension(war_projections, service_time)

if optimal:
    print("\n\nOptimal Extension Found:")
    print("=" * 60)
    print(f"Structure: {optimal['years']} years, ${optimal['aav']/1e6:.1f}M AAV")
    print(f"Total Value: ${optimal['total_value']/1e6:.1f}M")
    print(f"Team Surplus: ${optimal['extension_surplus']/1e6:.1f}M")
    print(f"Team Savings: ${optimal['team_savings']/1e6:.1f}M")
    print(f"Player Premium: ${optimal['player_certainty_value']/1e6:.1f}M")

29.5 Free Agent Market Analysis

The free agent market operates on different principles than the trade market or arbitration system. Players receive multi-year guarantees at market rates, creating both opportunity and risk for teams.

Free Agent Valuation Principles

Key factors in free agent valuation:

  1. Age curve: Performance typically peaks at 27-29, declines ~0.5 WAR per year after 30
  2. Position scarcity: Premium positions (C, SS, CF, SP) command higher prices
  3. Market timing: Weak free agent classes inflate prices
  4. Qualifying offer: $20.1M one-year offer that attaches draft pick compensation
  5. Deferred money: Present value calculations for contract structure

Python Implementation: Free Agent Market Model

import numpy as np
import pandas as pd
from scipy.stats import linregress
import matplotlib.pyplot as plt
import seaborn as sns

class FreeAgentMarket:
    """
    Model free agent market dynamics and valuation
    """

    def __init__(self, dollars_per_war=8e6, discount_rate=0.08):
        self.dollars_per_war = dollars_per_war
        self.discount_rate = discount_rate

        # Age curve coefficients (based on research)
        self.peak_age = 28
        self.decline_rate = 0.50  # WAR per year after peak

    def apply_age_curve(self, base_war, current_age, projection_years):
        """
        Apply aging curve to WAR projections
        """
        projections = []

        for year in range(projection_years):
            age = current_age + year

            if age <= self.peak_age:
                # Still improving or at peak
                age_factor = min(1.0, 0.85 + (age - 22) * 0.025)
            else:
                # Declining
                years_past_peak = age - self.peak_age
                age_factor = max(0.1, 1.0 - (years_past_peak * self.decline_rate / base_war))

            projected_war = base_war * age_factor
            projections.append(max(0, projected_war))

        return projections

    def calculate_contract_value(self, war_projections, salaries,
                                 include_surplus=True):
        """
        Calculate the total value and surplus of a contract
        """
        total_salary = 0
        total_value = 0

        for i, (war, salary) in enumerate(zip(war_projections, salaries)):
            discount = (1 + self.discount_rate) ** i

            total_salary += salary / discount
            total_value += (war * self.dollars_per_war) / discount

        surplus = total_value - total_salary if include_surplus else None

        return {
            'npv_salary': total_salary,
            'npv_value': total_value,
            'surplus': surplus,
            'total_salary': sum(salaries),
            'total_war': sum(war_projections),
            'aav': sum(salaries) / len(salaries),
            'dollars_per_war': sum(salaries) / sum(war_projections) if sum(war_projections) > 0 else np.inf
        }

    def estimate_market_value(self, base_war, age, position='generic',
                             market_strength=1.0):
        """
        Estimate fair market contract for a free agent

        Parameters:
        -----------
        base_war : float
            Current/recent WAR performance
        age : int
            Player age
        position : str
            Position (affects scarcity premium)
        market_strength : float
            Market adjustment (>1 = seller's market, <1 = buyer's market)
        """
        # Position scarcity premiums
        position_premiums = {
            'C': 1.10,
            'SS': 1.10,
            'CF': 1.05,
            'SP': 1.05,
            '2B': 1.00,
            '3B': 1.00,
            'OF': 0.98,
            '1B': 0.95,
            'DH': 0.90,
            'RP': 0.85
        }

        position_factor = position_premiums.get(position, 1.0)

        # Contract length based on age and performance
        if age <= 28 and base_war >= 4.0:
            years = 7  # Long-term deal for prime talent
        elif age <= 30 and base_war >= 3.0:
            years = 5  # Medium-term for solid performers
        elif age <= 32 and base_war >= 2.0:
            years = 3  # Short-term for aging players
        else:
            years = 2  # Prove-it deals

        # Project performance
        war_projections = self.apply_age_curve(base_war, age, years)

        # Calculate fair value
        total_war = sum(war_projections)
        base_value = total_war * self.dollars_per_war * position_factor * market_strength

        # AAV calculation - front-loaded premium for free agents
        aav = base_value / years
        salaries = [aav] * years

        return {
            'recommended_years': years,
            'recommended_aav': aav,
            'total_value': base_value,
            'war_projections': war_projections,
            'salaries': salaries
        }

    def compare_free_agents(self, players):
        """
        Compare multiple free agent options
        """
        results = []

        for player in players:
            market_contract = self.estimate_market_value(
                player['base_war'],
                player['age'],
                player.get('position', 'generic'),
                player.get('market_strength', 1.0)
            )

            contract_value = self.calculate_contract_value(
                market_contract['war_projections'],
                market_contract['salaries']
            )

            results.append({
                'name': player['name'],
                'age': player['age'],
                'position': player.get('position', 'generic'),
                'base_war': player['base_war'],
                'years': market_contract['recommended_years'],
                'aav': market_contract['recommended_aav'],
                'total_value': market_contract['total_value'],
                'total_war': contract_value['total_war'],
                'surplus': contract_value['surplus'],
                'dollars_per_war': contract_value['dollars_per_war']
            })

        return pd.DataFrame(results)

# Example: 2024 Free Agent Class Analysis
market = FreeAgentMarket()

free_agents = [
    {'name': 'Ace Starter (Age 28)', 'base_war': 5.0, 'age': 28, 'position': 'SP'},
    {'name': 'Power Hitter (Age 29)', 'base_war': 4.5, 'age': 29, 'position': 'OF'},
    {'name': 'Elite SS (Age 26)', 'base_war': 5.5, 'age': 26, 'position': 'SS'},
    {'name': 'Veteran 3B (Age 33)', 'base_war': 3.0, 'age': 33, 'position': '3B'},
    {'name': 'Quality Closer (Age 30)', 'base_war': 2.5, 'age': 30, 'position': 'RP'},
]

comparison = market.compare_free_agents(free_agents)

print("Free Agent Market Valuation")
print("=" * 80)
print(comparison.to_string(index=False,
      formatters={
          'aav': lambda x: f'${x/1e6:.1f}M',
          'total_value': lambda x: f'${x/1e6:.1f}M',
          'surplus': lambda x: f'${x/1e6:.1f}M',
          'dollars_per_war': lambda x: f'${x/1e6:.1f}M'
      }))

# Detailed analysis for top target
print("\n\nDetailed Analysis: Elite SS (Age 26)")
print("=" * 80)

elite_ss = market.estimate_market_value(5.5, 26, 'SS', market_strength=1.1)
print(f"Recommended Contract: {elite_ss['recommended_years']} years, "
      f"${elite_ss['recommended_aav']/1e6:.1f}M AAV")
print(f"Total Value: ${elite_ss['total_value']/1e6:.1f}M")
print(f"\nWAR Projections by Year:")
for i, war in enumerate(elite_ss['war_projections'], 1):
    print(f"  Year {i}: {war:.2f} WAR (Age {26 + i - 1})")

contract_analysis = market.calculate_contract_value(
    elite_ss['war_projections'],
    elite_ss['salaries']
)

print(f"\nContract Value Analysis:")
print(f"  NPV of Salary: ${contract_analysis['npv_salary']/1e6:.1f}M")
print(f"  NPV of Production: ${contract_analysis['npv_value']/1e6:.1f}M")
print(f"  Surplus Value: ${contract_analysis['surplus']/1e6:.1f}M")
print(f"  Effective $/WAR: ${contract_analysis['dollars_per_war']/1e6:.1f}M")
Python
import numpy as np
import pandas as pd
from scipy.stats import linregress
import matplotlib.pyplot as plt
import seaborn as sns

class FreeAgentMarket:
    """
    Model free agent market dynamics and valuation
    """

    def __init__(self, dollars_per_war=8e6, discount_rate=0.08):
        self.dollars_per_war = dollars_per_war
        self.discount_rate = discount_rate

        # Age curve coefficients (based on research)
        self.peak_age = 28
        self.decline_rate = 0.50  # WAR per year after peak

    def apply_age_curve(self, base_war, current_age, projection_years):
        """
        Apply aging curve to WAR projections
        """
        projections = []

        for year in range(projection_years):
            age = current_age + year

            if age <= self.peak_age:
                # Still improving or at peak
                age_factor = min(1.0, 0.85 + (age - 22) * 0.025)
            else:
                # Declining
                years_past_peak = age - self.peak_age
                age_factor = max(0.1, 1.0 - (years_past_peak * self.decline_rate / base_war))

            projected_war = base_war * age_factor
            projections.append(max(0, projected_war))

        return projections

    def calculate_contract_value(self, war_projections, salaries,
                                 include_surplus=True):
        """
        Calculate the total value and surplus of a contract
        """
        total_salary = 0
        total_value = 0

        for i, (war, salary) in enumerate(zip(war_projections, salaries)):
            discount = (1 + self.discount_rate) ** i

            total_salary += salary / discount
            total_value += (war * self.dollars_per_war) / discount

        surplus = total_value - total_salary if include_surplus else None

        return {
            'npv_salary': total_salary,
            'npv_value': total_value,
            'surplus': surplus,
            'total_salary': sum(salaries),
            'total_war': sum(war_projections),
            'aav': sum(salaries) / len(salaries),
            'dollars_per_war': sum(salaries) / sum(war_projections) if sum(war_projections) > 0 else np.inf
        }

    def estimate_market_value(self, base_war, age, position='generic',
                             market_strength=1.0):
        """
        Estimate fair market contract for a free agent

        Parameters:
        -----------
        base_war : float
            Current/recent WAR performance
        age : int
            Player age
        position : str
            Position (affects scarcity premium)
        market_strength : float
            Market adjustment (>1 = seller's market, <1 = buyer's market)
        """
        # Position scarcity premiums
        position_premiums = {
            'C': 1.10,
            'SS': 1.10,
            'CF': 1.05,
            'SP': 1.05,
            '2B': 1.00,
            '3B': 1.00,
            'OF': 0.98,
            '1B': 0.95,
            'DH': 0.90,
            'RP': 0.85
        }

        position_factor = position_premiums.get(position, 1.0)

        # Contract length based on age and performance
        if age <= 28 and base_war >= 4.0:
            years = 7  # Long-term deal for prime talent
        elif age <= 30 and base_war >= 3.0:
            years = 5  # Medium-term for solid performers
        elif age <= 32 and base_war >= 2.0:
            years = 3  # Short-term for aging players
        else:
            years = 2  # Prove-it deals

        # Project performance
        war_projections = self.apply_age_curve(base_war, age, years)

        # Calculate fair value
        total_war = sum(war_projections)
        base_value = total_war * self.dollars_per_war * position_factor * market_strength

        # AAV calculation - front-loaded premium for free agents
        aav = base_value / years
        salaries = [aav] * years

        return {
            'recommended_years': years,
            'recommended_aav': aav,
            'total_value': base_value,
            'war_projections': war_projections,
            'salaries': salaries
        }

    def compare_free_agents(self, players):
        """
        Compare multiple free agent options
        """
        results = []

        for player in players:
            market_contract = self.estimate_market_value(
                player['base_war'],
                player['age'],
                player.get('position', 'generic'),
                player.get('market_strength', 1.0)
            )

            contract_value = self.calculate_contract_value(
                market_contract['war_projections'],
                market_contract['salaries']
            )

            results.append({
                'name': player['name'],
                'age': player['age'],
                'position': player.get('position', 'generic'),
                'base_war': player['base_war'],
                'years': market_contract['recommended_years'],
                'aav': market_contract['recommended_aav'],
                'total_value': market_contract['total_value'],
                'total_war': contract_value['total_war'],
                'surplus': contract_value['surplus'],
                'dollars_per_war': contract_value['dollars_per_war']
            })

        return pd.DataFrame(results)

# Example: 2024 Free Agent Class Analysis
market = FreeAgentMarket()

free_agents = [
    {'name': 'Ace Starter (Age 28)', 'base_war': 5.0, 'age': 28, 'position': 'SP'},
    {'name': 'Power Hitter (Age 29)', 'base_war': 4.5, 'age': 29, 'position': 'OF'},
    {'name': 'Elite SS (Age 26)', 'base_war': 5.5, 'age': 26, 'position': 'SS'},
    {'name': 'Veteran 3B (Age 33)', 'base_war': 3.0, 'age': 33, 'position': '3B'},
    {'name': 'Quality Closer (Age 30)', 'base_war': 2.5, 'age': 30, 'position': 'RP'},
]

comparison = market.compare_free_agents(free_agents)

print("Free Agent Market Valuation")
print("=" * 80)
print(comparison.to_string(index=False,
      formatters={
          'aav': lambda x: f'${x/1e6:.1f}M',
          'total_value': lambda x: f'${x/1e6:.1f}M',
          'surplus': lambda x: f'${x/1e6:.1f}M',
          'dollars_per_war': lambda x: f'${x/1e6:.1f}M'
      }))

# Detailed analysis for top target
print("\n\nDetailed Analysis: Elite SS (Age 26)")
print("=" * 80)

elite_ss = market.estimate_market_value(5.5, 26, 'SS', market_strength=1.1)
print(f"Recommended Contract: {elite_ss['recommended_years']} years, "
      f"${elite_ss['recommended_aav']/1e6:.1f}M AAV")
print(f"Total Value: ${elite_ss['total_value']/1e6:.1f}M")
print(f"\nWAR Projections by Year:")
for i, war in enumerate(elite_ss['war_projections'], 1):
    print(f"  Year {i}: {war:.2f} WAR (Age {26 + i - 1})")

contract_analysis = market.calculate_contract_value(
    elite_ss['war_projections'],
    elite_ss['salaries']
)

print(f"\nContract Value Analysis:")
print(f"  NPV of Salary: ${contract_analysis['npv_salary']/1e6:.1f}M")
print(f"  NPV of Production: ${contract_analysis['npv_value']/1e6:.1f}M")
print(f"  Surplus Value: ${contract_analysis['surplus']/1e6:.1f}M")
print(f"  Effective $/WAR: ${contract_analysis['dollars_per_war']/1e6:.1f}M")

29.6 Win Curve & Competitive Window Analysis

Understanding your team's competitive window is essential for making smart trade deadline and free agent decisions. The "win curve" concept helps teams optimize when to invest resources.

The Win Curve Concept

Teams should invest most heavily when they have:


  1. High baseline talent (projected 85+ wins without additions)

  2. Young core under control (maximizing surplus value window)

  3. Favorable division competition (realistic path to playoffs)

The marginal value of a win increases exponentially near playoff thresholds:


  • 80 to 81 wins: Low value (far from playoffs)

  • 87 to 88 wins: Medium value (wild card threshold)

  • 91 to 92 wins: High value (division title threshold)

R Implementation: Competitive Window Optimizer

library(tidyverse)
library(plotly)

# Function to calculate marginal win value
marginal_win_value <- function(current_wins, total_games = 162) {
  """
  Calculate the value of one additional win based on playoff thresholds
  """

  # Playoff probability as function of wins (logistic curve)
  playoff_prob <- plogis((current_wins - 87) / 4)
  playoff_prob_plus_one <- plogis((current_wins + 1 - 87) / 4)

  # Marginal playoff probability
  delta_playoff <- playoff_prob_plus_one - playoff_prob

  # Value of making playoffs (~$40M in revenue)
  playoff_value <- 40000000

  # Marginal value
  marginal_value <- delta_playoff * playoff_value

  return(marginal_value)
}

# Create win curve visualization
win_range <- 65:100
marginal_values <- sapply(win_range, marginal_win_value)

win_curve_data <- tibble(
  wins = win_range,
  marginal_value = marginal_values,
  cumulative_value = cumsum(marginal_values)
)

# Plot marginal value of wins
ggplot(win_curve_data, aes(x = wins, y = marginal_value / 1e6)) +
  geom_line(color = "blue", size = 1.2) +
  geom_point(color = "red", size = 2) +
  geom_vline(xintercept = 87, linetype = "dashed", color = "green", size = 1) +
  annotate("text", x = 87, y = max(marginal_values) / 1e6 * 0.9,
           label = "Wild Card\nThreshold", hjust = -0.1) +
  labs(
    title = "Marginal Value of Wins",
    subtitle = "Value increases dramatically near playoff thresholds",
    x = "Team Wins",
    y = "Marginal Value of Next Win ($M)",
    caption = "Based on ~$40M playoff revenue and logistic playoff probability"
  ) +
  theme_minimal() +
  theme(plot.title = element_text(face = "bold", size = 14))

# Competitive window analysis
analyze_competitive_window <- function(team_name, current_year = 2024) {
  """
  Analyze a team's competitive window based on roster composition
  """

  # Example: Team with young core
  # Would normally pull from roster database
  roster <- tibble(
    player = c("Young Star SS", "Ace Pitcher", "Power Hitter", "Catcher",
               "Veteran 3B", "Setup Man", "Bench Piece"),
    age = c(25, 27, 26, 28, 34, 31, 29),
    war_2024 = c(5.5, 4.0, 4.5, 2.5, 2.0, 1.5, 0.5),
    control_years = c(5, 3, 4, 2, 1, 2, 1),
    salary_2024 = c(3e6, 15e6, 8e6, 12e6, 18e6, 7e6, 2e6)
  )

  # Project roster value over next 5 years
  projection_years <- 5
  projections <- tibble(year = current_year + 0:(projection_years - 1))

  for (i in 1:nrow(roster)) {
    player_wars <- numeric(projection_years)

    for (year_offset in 0:(projection_years - 1)) {
      if (year_offset < roster$control_years[i]) {
        # Apply aging curve
        age <- roster$age[i] + year_offset
        age_factor <- if(age <= 28) 1.0 else max(0.3, 1.0 - (age - 28) * 0.08)
        player_wars[year_offset + 1] <- roster$war_2024[i] * age_factor
      } else {
        # Lost to free agency
        player_wars[year_offset + 1] <- 0
      }
    }

    projections[[roster$player[i]]] <- player_wars
  }

  # Calculate total team WAR
  projections <- projections %>%
    mutate(
      total_war = rowSums(select(., -year)),
      baseline_wins = 48 + total_war,  # Replacement level ~48 wins
      window_strength = case_when(
        baseline_wins >= 90 ~ "Championship Window",
        baseline_wins >= 85 ~ "Competitive",
        baseline_wins >= 78 ~ "Fringe",
        TRUE ~ "Rebuilding"
      )
    )

  return(list(roster = roster, projections = projections))
}

# Example analysis
window_analysis <- analyze_competitive_window("Example Team", 2024)

cat("\nCompetitive Window Analysis\n")
cat("===========================\n\n")
print(window_analysis$projections %>%
  select(year, total_war, baseline_wins, window_strength) %>%
  mutate(
    total_war = round(total_war, 1),
    baseline_wins = round(baseline_wins, 0)
  ))

# Visualize window
ggplot(window_analysis$projections, aes(x = year, y = baseline_wins)) +
  geom_line(size = 1.2, color = "blue") +
  geom_point(size = 3, color = "red") +
  geom_hline(yintercept = 87, linetype = "dashed", color = "green") +
  geom_hline(yintercept = 92, linetype = "dashed", color = "darkgreen") +
  annotate("text", x = min(window_analysis$projections$year), y = 87,
           label = "Wild Card", hjust = 0, vjust = -0.5, color = "green") +
  annotate("text", x = min(window_analysis$projections$year), y = 92,
           label = "Division", hjust = 0, vjust = -0.5, color = "darkgreen") +
  labs(
    title = "Projected Competitive Window",
    subtitle = "Based on current roster and age curve projections",
    x = "Year",
    y = "Projected Wins"
  ) +
  theme_minimal() +
  theme(plot.title = element_text(face = "bold", size = 14))
R
library(tidyverse)
library(plotly)

# Function to calculate marginal win value
marginal_win_value <- function(current_wins, total_games = 162) {
  """
  Calculate the value of one additional win based on playoff thresholds
  """

  # Playoff probability as function of wins (logistic curve)
  playoff_prob <- plogis((current_wins - 87) / 4)
  playoff_prob_plus_one <- plogis((current_wins + 1 - 87) / 4)

  # Marginal playoff probability
  delta_playoff <- playoff_prob_plus_one - playoff_prob

  # Value of making playoffs (~$40M in revenue)
  playoff_value <- 40000000

  # Marginal value
  marginal_value <- delta_playoff * playoff_value

  return(marginal_value)
}

# Create win curve visualization
win_range <- 65:100
marginal_values <- sapply(win_range, marginal_win_value)

win_curve_data <- tibble(
  wins = win_range,
  marginal_value = marginal_values,
  cumulative_value = cumsum(marginal_values)
)

# Plot marginal value of wins
ggplot(win_curve_data, aes(x = wins, y = marginal_value / 1e6)) +
  geom_line(color = "blue", size = 1.2) +
  geom_point(color = "red", size = 2) +
  geom_vline(xintercept = 87, linetype = "dashed", color = "green", size = 1) +
  annotate("text", x = 87, y = max(marginal_values) / 1e6 * 0.9,
           label = "Wild Card\nThreshold", hjust = -0.1) +
  labs(
    title = "Marginal Value of Wins",
    subtitle = "Value increases dramatically near playoff thresholds",
    x = "Team Wins",
    y = "Marginal Value of Next Win ($M)",
    caption = "Based on ~$40M playoff revenue and logistic playoff probability"
  ) +
  theme_minimal() +
  theme(plot.title = element_text(face = "bold", size = 14))

# Competitive window analysis
analyze_competitive_window <- function(team_name, current_year = 2024) {
  """
  Analyze a team's competitive window based on roster composition
  """

  # Example: Team with young core
  # Would normally pull from roster database
  roster <- tibble(
    player = c("Young Star SS", "Ace Pitcher", "Power Hitter", "Catcher",
               "Veteran 3B", "Setup Man", "Bench Piece"),
    age = c(25, 27, 26, 28, 34, 31, 29),
    war_2024 = c(5.5, 4.0, 4.5, 2.5, 2.0, 1.5, 0.5),
    control_years = c(5, 3, 4, 2, 1, 2, 1),
    salary_2024 = c(3e6, 15e6, 8e6, 12e6, 18e6, 7e6, 2e6)
  )

  # Project roster value over next 5 years
  projection_years <- 5
  projections <- tibble(year = current_year + 0:(projection_years - 1))

  for (i in 1:nrow(roster)) {
    player_wars <- numeric(projection_years)

    for (year_offset in 0:(projection_years - 1)) {
      if (year_offset < roster$control_years[i]) {
        # Apply aging curve
        age <- roster$age[i] + year_offset
        age_factor <- if(age <= 28) 1.0 else max(0.3, 1.0 - (age - 28) * 0.08)
        player_wars[year_offset + 1] <- roster$war_2024[i] * age_factor
      } else {
        # Lost to free agency
        player_wars[year_offset + 1] <- 0
      }
    }

    projections[[roster$player[i]]] <- player_wars
  }

  # Calculate total team WAR
  projections <- projections %>%
    mutate(
      total_war = rowSums(select(., -year)),
      baseline_wins = 48 + total_war,  # Replacement level ~48 wins
      window_strength = case_when(
        baseline_wins >= 90 ~ "Championship Window",
        baseline_wins >= 85 ~ "Competitive",
        baseline_wins >= 78 ~ "Fringe",
        TRUE ~ "Rebuilding"
      )
    )

  return(list(roster = roster, projections = projections))
}

# Example analysis
window_analysis <- analyze_competitive_window("Example Team", 2024)

cat("\nCompetitive Window Analysis\n")
cat("===========================\n\n")
print(window_analysis$projections %>%
  select(year, total_war, baseline_wins, window_strength) %>%
  mutate(
    total_war = round(total_war, 1),
    baseline_wins = round(baseline_wins, 0)
  ))

# Visualize window
ggplot(window_analysis$projections, aes(x = year, y = baseline_wins)) +
  geom_line(size = 1.2, color = "blue") +
  geom_point(size = 3, color = "red") +
  geom_hline(yintercept = 87, linetype = "dashed", color = "green") +
  geom_hline(yintercept = 92, linetype = "dashed", color = "darkgreen") +
  annotate("text", x = min(window_analysis$projections$year), y = 87,
           label = "Wild Card", hjust = 0, vjust = -0.5, color = "green") +
  annotate("text", x = min(window_analysis$projections$year), y = 92,
           label = "Division", hjust = 0, vjust = -0.5, color = "darkgreen") +
  labs(
    title = "Projected Competitive Window",
    subtitle = "Based on current roster and age curve projections",
    x = "Year",
    y = "Projected Wins"
  ) +
  theme_minimal() +
  theme(plot.title = element_text(face = "bold", size = 14))

29.7 Building a Trade Analyzer Tool

Let's build a comprehensive tool that integrates everything we've learned to analyze potential trades from both teams' perspectives.

Python Implementation: Complete Trade Analyzer

import numpy as np
import pandas as pd
from dataclasses import dataclass
from typing import List, Dict
import matplotlib.pyplot as plt

@dataclass
class Player:
    """Player data structure"""
    name: str
    war_projection: List[float]  # WAR for each year of control
    service_time: float
    position: str
    age: int
    mlb_prob: float = 1.0  # For prospects

@dataclass
class Team:
    """Team data structure"""
    name: str
    current_wins: int
    current_losses: int
    games_remaining: int
    division_lead: float
    playoff_revenue: float = 40e6
    payroll_budget: float = 200e6

class ComprehensiveTradeAnalyzer:
    """
    Complete trade analysis system integrating all concepts
    """

    def __init__(self, dollars_per_war=8e6, discount_rate=0.10):
        self.dollars_per_war = dollars_per_war
        self.discount_rate = discount_rate

    def calculate_player_value(self, player: Player) -> Dict:
        """
        Calculate complete value metrics for a player
        """
        # Calculate surplus value
        surplus_by_year = []
        total_surplus = 0

        for i, war in enumerate(player.war_projection):
            # Discount factor
            discount = (1 + self.discount_rate) ** i

            # Salary estimation based on service time
            current_service = player.service_time + i

            if current_service < 3.0:
                salary = 750000
            elif current_service < 4.0:
                salary = max(750000, war * self.dollars_per_war * 0.40)
            elif current_service < 5.0:
                salary = max(750000, war * self.dollars_per_war * 0.60)
            elif current_service < 6.0:
                salary = max(750000, war * self.dollars_per_war * 0.80)
            else:
                salary = war * self.dollars_per_war

            # Surplus
            market_value = war * self.dollars_per_war
            surplus = (market_value - salary) / discount
            surplus_by_year.append(surplus)
            total_surplus += surplus

        # Apply MLB probability for prospects
        total_surplus *= player.mlb_prob

        return {
            'player': player.name,
            'total_war': sum(player.war_projection) * player.mlb_prob,
            'npv_war': sum([w / (1 + self.discount_rate) ** i
                           for i, w in enumerate(player.war_projection)]) * player.mlb_prob,
            'surplus_value': total_surplus,
            'years_control': len(player.war_projection),
            'mlb_prob': player.mlb_prob
        }

    def calculate_trade_impact(self, team: Team, players_in: List[Player],
                              players_out: List[Player]) -> Dict:
        """
        Calculate comprehensive trade impact for a team
        """
        # Value calculations
        value_in = sum([self.calculate_player_value(p)['surplus_value']
                       for p in players_in])
        value_out = sum([self.calculate_player_value(p)['surplus_value']
                        for p in players_out])

        net_value = value_in - value_out

        # WAR impact (current season only - for playoff odds)
        war_in_current = sum([p.war_projection[0] * p.mlb_prob
                             for p in players_in])
        war_out_current = sum([p.war_projection[0] * p.mlb_prob
                              for p in players_out])

        net_war_current = war_in_current - war_out_current

        # Playoff impact
        current_wins = team.current_wins
        projected_wins_base = current_wins + (team.games_remaining * 0.5)
        projected_wins_with_trade = projected_wins_base + net_war_current

        # Playoff probability (logistic model)
        prob_base = 1 / (1 + np.exp(-(projected_wins_base - 87) / 4))
        prob_with_trade = 1 / (1 + np.exp(-(projected_wins_with_trade - 87) / 4))

        delta_playoff_prob = prob_with_trade - prob_base
        playoff_ev = delta_playoff_prob * team.playoff_revenue

        return {
            'team': team.name,
            'players_acquired': [p.name for p in players_in],
            'players_traded': [p.name for p in players_out],
            'value_in': value_in,
            'value_out': value_out,
            'net_surplus_value': net_value,
            'current_year_war_impact': net_war_current,
            'projected_wins_base': projected_wins_base,
            'projected_wins_with_trade': projected_wins_with_trade,
            'playoff_prob_base': prob_base,
            'playoff_prob_with_trade': prob_with_trade,
            'delta_playoff_prob': delta_playoff_prob,
            'playoff_expected_value': playoff_ev,
            'recommendation': self._make_recommendation(net_value, playoff_ev, team)
        }

    def _make_recommendation(self, net_value, playoff_ev, team):
        """
        Make trade recommendation based on team context
        """
        # Contender logic
        if team.current_wins > team.current_losses * 1.15:  # >53% win rate
            if playoff_ev > abs(net_value):
                return "STRONG RECOMMEND - Playoff boost worth the cost"
            elif playoff_ev > 0 and net_value > -10e6:
                return "RECOMMEND - Modest cost for playoff push"
            else:
                return "CAUTION - High cost for playoff boost"

        # Seller logic
        elif team.current_losses > team.current_wins * 1.15:  # <47% win rate
            if net_value > 20e6:
                return "STRONG RECOMMEND - Excellent prospect return"
            elif net_value > 5e6:
                return "RECOMMEND - Good value for rebuilding"
            else:
                return "PASS - Insufficient return"

        # Middling team
        else:
            if net_value > 15e6:
                return "RECOMMEND - Strong value, build for future"
            else:
                return "EVALUATE - Consider competitive timeline"

    def analyze_trade(self, team_a: Team, team_a_gives: List[Player],
                     team_b: Team, team_b_gives: List[Player]) -> Dict:
        """
        Analyze trade from both perspectives
        """
        team_a_impact = self.calculate_trade_impact(team_a, team_b_gives, team_a_gives)
        team_b_impact = self.calculate_trade_impact(team_b, team_a_gives, team_b_gives)

        # Trade balance
        surplus_diff = abs(team_a_impact['net_surplus_value'] -
                          team_b_impact['net_surplus_value'])

        balance_rating = "BALANCED" if surplus_diff < 10e6 else \
                        "UNBALANCED" if surplus_diff < 25e6 else \
                        "VERY UNBALANCED"

        return {
            'team_a_perspective': team_a_impact,
            'team_b_perspective': team_b_impact,
            'surplus_difference': surplus_diff,
            'balance_rating': balance_rating
        }

# Example: Real-world trade analysis
# Scenario: Contender acquires closer from rebuilding team

analyzer = ComprehensiveTradeAnalyzer()

# Team A: Contender (e.g., Dodgers)
contender = Team(
    name="Contending Team",
    current_wins=68,
    current_losses=42,
    games_remaining=52,
    division_lead=4.5,
    playoff_revenue=45e6,
    payroll_budget=280e6
)

# Team B: Seller (e.g., rebuilding team)
seller = Team(
    name="Rebuilding Team",
    current_wins=42,
    current_losses=68,
    games_remaining=52,
    division_lead=-15.0,
    playoff_revenue=40e6,
    payroll_budget=150e6
)

# Contender gives up prospects
contender_gives = [
    Player(
        name="Top Pitching Prospect (#35 overall)",
        war_projection=[0, 2.0, 2.5, 3.0, 3.0, 2.5, 2.0],
        service_time=0,
        position="SP",
        age=21,
        mlb_prob=0.60
    ),
    Player(
        name="Outfield Prospect (#80 overall)",
        war_projection=[0, 0, 1.5, 2.0, 2.0, 1.5, 1.0],
        service_time=0,
        position="OF",
        age=20,
        mlb_prob=0.45
    )
]

# Seller gives up closer
seller_gives = [
    Player(
        name="Elite Closer",
        war_projection=[1.2, 1.5],  # Rest of season + next year (arb)
        service_time=4.1,
        position="RP",
        age=29,
        mlb_prob=1.0
    )
]

# Analyze trade
result = analyzer.analyze_trade(contender, contender_gives, seller, seller_gives)

print("COMPREHENSIVE TRADE ANALYSIS")
print("=" * 80)
print(f"\nTrade: {contender.name} acquires {seller_gives[0].name}")
print(f"       {seller.name} acquires {', '.join([p.name for p in contender_gives])}")

print(f"\n{contender.name} Perspective:")
print("-" * 80)
team_a = result['team_a_perspective']
print(f"Players Acquired: {', '.join(team_a['players_acquired'])}")
print(f"Players Traded: {', '.join(team_a['players_traded'])}")
print(f"Value Received: ${team_a['value_in']/1e6:.2f}M")
print(f"Value Given Up: ${team_a['value_out']/1e6:.2f}M")
print(f"Net Surplus Value: ${team_a['net_surplus_value']/1e6:.2f}M")
print(f"\nCurrent Season Impact:")
print(f"  Projected Wins (before): {team_a['projected_wins_base']:.1f}")
print(f"  Projected Wins (after): {team_a['projected_wins_with_trade']:.1f}")
print(f"  Playoff Probability Change: {team_a['delta_playoff_prob']:.1%}")
print(f"  Playoff Expected Value: ${team_a['playoff_expected_value']/1e6:.2f}M")
print(f"\nRecommendation: {team_a['recommendation']}")

print(f"\n{seller.name} Perspective:")
print("-" * 80)
team_b = result['team_b_perspective']
print(f"Players Acquired: {', '.join(team_b['players_acquired'])}")
print(f"Players Traded: {', '.join(team_b['players_traded'])}")
print(f"Value Received: ${team_b['value_in']/1e6:.2f}M")
print(f"Value Given Up: ${team_b['value_out']/1e6:.2f}M")
print(f"Net Surplus Value: ${team_b['net_surplus_value']/1e6:.2f}M")
print(f"\nRecommendation: {team_b['recommendation']}")

print(f"\nTrade Balance:")
print("-" * 80)
print(f"Surplus Value Difference: ${result['surplus_difference']/1e6:.2f}M")
print(f"Balance Rating: {result['balance_rating']}")
Python
import numpy as np
import pandas as pd
from dataclasses import dataclass
from typing import List, Dict
import matplotlib.pyplot as plt

@dataclass
class Player:
    """Player data structure"""
    name: str
    war_projection: List[float]  # WAR for each year of control
    service_time: float
    position: str
    age: int
    mlb_prob: float = 1.0  # For prospects

@dataclass
class Team:
    """Team data structure"""
    name: str
    current_wins: int
    current_losses: int
    games_remaining: int
    division_lead: float
    playoff_revenue: float = 40e6
    payroll_budget: float = 200e6

class ComprehensiveTradeAnalyzer:
    """
    Complete trade analysis system integrating all concepts
    """

    def __init__(self, dollars_per_war=8e6, discount_rate=0.10):
        self.dollars_per_war = dollars_per_war
        self.discount_rate = discount_rate

    def calculate_player_value(self, player: Player) -> Dict:
        """
        Calculate complete value metrics for a player
        """
        # Calculate surplus value
        surplus_by_year = []
        total_surplus = 0

        for i, war in enumerate(player.war_projection):
            # Discount factor
            discount = (1 + self.discount_rate) ** i

            # Salary estimation based on service time
            current_service = player.service_time + i

            if current_service < 3.0:
                salary = 750000
            elif current_service < 4.0:
                salary = max(750000, war * self.dollars_per_war * 0.40)
            elif current_service < 5.0:
                salary = max(750000, war * self.dollars_per_war * 0.60)
            elif current_service < 6.0:
                salary = max(750000, war * self.dollars_per_war * 0.80)
            else:
                salary = war * self.dollars_per_war

            # Surplus
            market_value = war * self.dollars_per_war
            surplus = (market_value - salary) / discount
            surplus_by_year.append(surplus)
            total_surplus += surplus

        # Apply MLB probability for prospects
        total_surplus *= player.mlb_prob

        return {
            'player': player.name,
            'total_war': sum(player.war_projection) * player.mlb_prob,
            'npv_war': sum([w / (1 + self.discount_rate) ** i
                           for i, w in enumerate(player.war_projection)]) * player.mlb_prob,
            'surplus_value': total_surplus,
            'years_control': len(player.war_projection),
            'mlb_prob': player.mlb_prob
        }

    def calculate_trade_impact(self, team: Team, players_in: List[Player],
                              players_out: List[Player]) -> Dict:
        """
        Calculate comprehensive trade impact for a team
        """
        # Value calculations
        value_in = sum([self.calculate_player_value(p)['surplus_value']
                       for p in players_in])
        value_out = sum([self.calculate_player_value(p)['surplus_value']
                        for p in players_out])

        net_value = value_in - value_out

        # WAR impact (current season only - for playoff odds)
        war_in_current = sum([p.war_projection[0] * p.mlb_prob
                             for p in players_in])
        war_out_current = sum([p.war_projection[0] * p.mlb_prob
                              for p in players_out])

        net_war_current = war_in_current - war_out_current

        # Playoff impact
        current_wins = team.current_wins
        projected_wins_base = current_wins + (team.games_remaining * 0.5)
        projected_wins_with_trade = projected_wins_base + net_war_current

        # Playoff probability (logistic model)
        prob_base = 1 / (1 + np.exp(-(projected_wins_base - 87) / 4))
        prob_with_trade = 1 / (1 + np.exp(-(projected_wins_with_trade - 87) / 4))

        delta_playoff_prob = prob_with_trade - prob_base
        playoff_ev = delta_playoff_prob * team.playoff_revenue

        return {
            'team': team.name,
            'players_acquired': [p.name for p in players_in],
            'players_traded': [p.name for p in players_out],
            'value_in': value_in,
            'value_out': value_out,
            'net_surplus_value': net_value,
            'current_year_war_impact': net_war_current,
            'projected_wins_base': projected_wins_base,
            'projected_wins_with_trade': projected_wins_with_trade,
            'playoff_prob_base': prob_base,
            'playoff_prob_with_trade': prob_with_trade,
            'delta_playoff_prob': delta_playoff_prob,
            'playoff_expected_value': playoff_ev,
            'recommendation': self._make_recommendation(net_value, playoff_ev, team)
        }

    def _make_recommendation(self, net_value, playoff_ev, team):
        """
        Make trade recommendation based on team context
        """
        # Contender logic
        if team.current_wins > team.current_losses * 1.15:  # >53% win rate
            if playoff_ev > abs(net_value):
                return "STRONG RECOMMEND - Playoff boost worth the cost"
            elif playoff_ev > 0 and net_value > -10e6:
                return "RECOMMEND - Modest cost for playoff push"
            else:
                return "CAUTION - High cost for playoff boost"

        # Seller logic
        elif team.current_losses > team.current_wins * 1.15:  # <47% win rate
            if net_value > 20e6:
                return "STRONG RECOMMEND - Excellent prospect return"
            elif net_value > 5e6:
                return "RECOMMEND - Good value for rebuilding"
            else:
                return "PASS - Insufficient return"

        # Middling team
        else:
            if net_value > 15e6:
                return "RECOMMEND - Strong value, build for future"
            else:
                return "EVALUATE - Consider competitive timeline"

    def analyze_trade(self, team_a: Team, team_a_gives: List[Player],
                     team_b: Team, team_b_gives: List[Player]) -> Dict:
        """
        Analyze trade from both perspectives
        """
        team_a_impact = self.calculate_trade_impact(team_a, team_b_gives, team_a_gives)
        team_b_impact = self.calculate_trade_impact(team_b, team_a_gives, team_b_gives)

        # Trade balance
        surplus_diff = abs(team_a_impact['net_surplus_value'] -
                          team_b_impact['net_surplus_value'])

        balance_rating = "BALANCED" if surplus_diff < 10e6 else \
                        "UNBALANCED" if surplus_diff < 25e6 else \
                        "VERY UNBALANCED"

        return {
            'team_a_perspective': team_a_impact,
            'team_b_perspective': team_b_impact,
            'surplus_difference': surplus_diff,
            'balance_rating': balance_rating
        }

# Example: Real-world trade analysis
# Scenario: Contender acquires closer from rebuilding team

analyzer = ComprehensiveTradeAnalyzer()

# Team A: Contender (e.g., Dodgers)
contender = Team(
    name="Contending Team",
    current_wins=68,
    current_losses=42,
    games_remaining=52,
    division_lead=4.5,
    playoff_revenue=45e6,
    payroll_budget=280e6
)

# Team B: Seller (e.g., rebuilding team)
seller = Team(
    name="Rebuilding Team",
    current_wins=42,
    current_losses=68,
    games_remaining=52,
    division_lead=-15.0,
    playoff_revenue=40e6,
    payroll_budget=150e6
)

# Contender gives up prospects
contender_gives = [
    Player(
        name="Top Pitching Prospect (#35 overall)",
        war_projection=[0, 2.0, 2.5, 3.0, 3.0, 2.5, 2.0],
        service_time=0,
        position="SP",
        age=21,
        mlb_prob=0.60
    ),
    Player(
        name="Outfield Prospect (#80 overall)",
        war_projection=[0, 0, 1.5, 2.0, 2.0, 1.5, 1.0],
        service_time=0,
        position="OF",
        age=20,
        mlb_prob=0.45
    )
]

# Seller gives up closer
seller_gives = [
    Player(
        name="Elite Closer",
        war_projection=[1.2, 1.5],  # Rest of season + next year (arb)
        service_time=4.1,
        position="RP",
        age=29,
        mlb_prob=1.0
    )
]

# Analyze trade
result = analyzer.analyze_trade(contender, contender_gives, seller, seller_gives)

print("COMPREHENSIVE TRADE ANALYSIS")
print("=" * 80)
print(f"\nTrade: {contender.name} acquires {seller_gives[0].name}")
print(f"       {seller.name} acquires {', '.join([p.name for p in contender_gives])}")

print(f"\n{contender.name} Perspective:")
print("-" * 80)
team_a = result['team_a_perspective']
print(f"Players Acquired: {', '.join(team_a['players_acquired'])}")
print(f"Players Traded: {', '.join(team_a['players_traded'])}")
print(f"Value Received: ${team_a['value_in']/1e6:.2f}M")
print(f"Value Given Up: ${team_a['value_out']/1e6:.2f}M")
print(f"Net Surplus Value: ${team_a['net_surplus_value']/1e6:.2f}M")
print(f"\nCurrent Season Impact:")
print(f"  Projected Wins (before): {team_a['projected_wins_base']:.1f}")
print(f"  Projected Wins (after): {team_a['projected_wins_with_trade']:.1f}")
print(f"  Playoff Probability Change: {team_a['delta_playoff_prob']:.1%}")
print(f"  Playoff Expected Value: ${team_a['playoff_expected_value']/1e6:.2f}M")
print(f"\nRecommendation: {team_a['recommendation']}")

print(f"\n{seller.name} Perspective:")
print("-" * 80)
team_b = result['team_b_perspective']
print(f"Players Acquired: {', '.join(team_b['players_acquired'])}")
print(f"Players Traded: {', '.join(team_b['players_traded'])}")
print(f"Value Received: ${team_b['value_in']/1e6:.2f}M")
print(f"Value Given Up: ${team_b['value_out']/1e6:.2f}M")
print(f"Net Surplus Value: ${team_b['net_surplus_value']/1e6:.2f}M")
print(f"\nRecommendation: {team_b['recommendation']}")

print(f"\nTrade Balance:")
print("-" * 80)
print(f"Surplus Value Difference: ${result['surplus_difference']/1e6:.2f}M")
print(f"Balance Rating: {result['balance_rating']}")

29.8 Exercises

Exercise 1: Trade Deadline Decision (Easy)

Your team is currently 72-55 with 35 games remaining. You're 2 games up in the division. A rival team offers you a rental starting pitcher (1.5 WAR over remaining games) for your #75 ranked prospect.

Tasks:


  1. Calculate your current playoff probability

  2. Estimate the playoff probability with the addition

  3. Calculate the expected value of the trade

  4. Should you make the trade?

Hint: Use the logistic playoff probability model: P(playoff) = 1 / (1 + exp(-(wins - 87) / 4))

Exercise 2: Arbitration Projection (Medium)

A 28-year-old starting pitcher is entering his second arbitration year (4.1 years service time). Last year he was 13-8 with a 3.25 ERA and 195 innings pitched, generating 4.2 WAR.

Tasks:


  1. Estimate his arbitration salary using the surplus value framework

  2. Compare to free agent market rate

  3. Calculate his surplus value over the next 2 arbitration years (project 3.8 WAR this year, 3.5 next)

  4. At what extension AAV would you sign him for 5 years?

Exercise 3: Prospect Package Valuation (Medium)

You're trading your ace (3.5 years of control, 4.0 WAR per year projected). You receive:


  • Team's #2 prospect (rank #40 overall), 60% MLB prob, 2 years away, projects as 2.8 WAR player

  • Team's #5 prospect (rank #95 overall), 45% MLB prob, 3 years away, projects as 1.8 WAR player

  • MLB-ready utility player (1.5 WAR per year, 4 years control)

Tasks:


  1. Calculate the surplus value you're trading away

  2. Calculate the surplus value of the package you're receiving

  3. Determine if this is a fair trade

  4. What additional piece would make this balanced?

Exercise 4: Competitive Window Analysis (Hard)

Your team has the following core players:

PlayerAgeWARService TimePosition
SS265.53.5SS
1B294.06.51B
SP1274.54.1SP
CF253.52.8CF
C302.57.2C

Tasks:


  1. Project each player's WAR for the next 5 years using age curves

  2. Estimate when players will be lost to free agency

  3. Identify your competitive window (when team projects >87 wins)

  4. Recommend whether to buy or sell at this year's deadline

Exercise 5: Extension Optimization (Hard)

Your 24-year-old star outfielder (1.5 years service) just had a 6.0 WAR season. You project:


  • Age 25: 6.5 WAR

  • Age 26: 6.5 WAR

  • Age 27-29: 6.0 WAR each

  • Age 30-32: 5.0 WAR each

Tasks:


  1. Calculate his expected arbitration salaries (years 3-6 of service)

  2. Estimate his free agent value at age 27

  3. Find an extension (6-8 years) that:



  • Gives him at least $15M more than arb path

  • Gives team at least $30M in surplus vs. year-to-year



  1. Structure the contract by year (consider deferrals, opt-outs)

Exercise 6: Multi-Team Trade Analysis (Hard)

Three-team trade:


  • Team A (Contender): Gets star rental SP (2.0 WAR, half season)

  • Team B (Mid-tier): Gets MLB CF (3.0 WAR, 4 years control) + Low-A lottery ticket

  • Team C (Seller): Gets #25 prospect, #60 prospect, #100 prospect

Tasks:


  1. Calculate surplus value for each team

  2. Determine if trade is balanced for all three teams

  3. Identify which team gets the best deal

  4. Suggest modifications to improve balance

  5. From each team's perspective, should they accept?

Exercise 7: Build Your Own Trade Analyzer (Hard)

Tasks:
Create a command-line tool in Python or R that:


  1. Takes player inputs (name, WAR projection, service time, age)

  2. Calculates surplus value for each player

  3. Simulates playoff probability impact

  4. Outputs trade recommendation for both teams

  5. Includes sensitivity analysis on key parameters

Bonus: Add a feature to scrape real prospect rankings and WAR projections from FanGraphs or Baseball Prospectus.


Summary

Trade deadline and contract analytics represent the cutting edge of baseball operations. Teams that excel in these areas gain significant competitive advantages:

  1. Trade Deadline Strategy: Understanding the expected value framework helps teams make rational buy/sell decisions based on playoff odds and prospect value.
  1. Prospect Valuation: Probabilistic modeling of prospect outcomes accounts for development risk and allows for fair trade comparisons.
  1. Surplus Value: The fundamental currency of team-building, allowing comparison across free agents, arbitration players, and prospects.
  1. Contract Optimization: Knowing when to extend players requires balancing risk, team control, and market value.
  1. Competitive Windows: Teams must understand their timeline to maximize value from aging stars and developing prospects.

The tools and frameworks in this chapter provide a foundation for analyzing baseball's most consequential decisions. As front offices become increasingly sophisticated, mastery of these concepts becomes essential for competitive success.


Further Reading:


  • "The Extra 2%: How Wall Street Strategies Took a Major League Baseball Team from Worst to First" by Jonah Keri

  • "Smart Baseball" by Keith Law

  • FanGraphs Trade Value series (annual)

  • Baseball Prospectus: "Finding the Next Cole Hamels: Prospect Projection Systems"

  • "Valuing Players: A Look at MLB Free Agent Contracts" (The Hardball Times)

Chapter Summary

In this chapter, you learned about trade deadline & contract analytics. Key topics covered:

  • Trade Deadline Decision Framework
  • Prospect Valuation & Trade Value
  • Surplus Value Calculations
  • Contract Optimization & Arbitration Projections
  • Free Agent Market Analysis
  • Win Curve & Competitive Window Analysis