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:
- Current playoff probability: Teams with >70% playoff odds typically buy; teams with <20% typically sell
- Playoff expectation vs. preseason projection: Overperforming teams may hold rather than sell
- Competitive window timeline: Young, improving teams may buy even with modest playoff odds
- Farm system strength: Teams with deep systems can more easily afford to trade prospects
- 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)
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))
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)
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:
- Probability of reaching MLB (varies by prospect level)
- Expected performance if they reach MLB
- Years of team control
- 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")
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))
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")
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)")
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)
)
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)")
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")
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))
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")
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:
- Age curve: Performance typically peaks at 27-29, declines ~0.5 WAR per year after 30
- Position scarcity: Premium positions (C, SS, CF, SP) command higher prices
- Market timing: Weak free agent classes inflate prices
- Qualifying offer: $20.1M one-year offer that attaches draft pick compensation
- 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")
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")
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:
- High baseline talent (projected 85+ wins without additions)
- Young core under control (maximizing surplus value window)
- 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))
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))
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']}")
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']}")
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:
- Calculate your current playoff probability
- Estimate the playoff probability with the addition
- Calculate the expected value of the trade
- 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:
- Estimate his arbitration salary using the surplus value framework
- Compare to free agent market rate
- Calculate his surplus value over the next 2 arbitration years (project 3.8 WAR this year, 3.5 next)
- 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:
- Calculate the surplus value you're trading away
- Calculate the surplus value of the package you're receiving
- Determine if this is a fair trade
- What additional piece would make this balanced?
Exercise 4: Competitive Window Analysis (Hard)
Your team has the following core players:
| Player | Age | WAR | Service Time | Position |
|---|---|---|---|---|
| SS | 26 | 5.5 | 3.5 | SS |
| 1B | 29 | 4.0 | 6.5 | 1B |
| SP1 | 27 | 4.5 | 4.1 | SP |
| CF | 25 | 3.5 | 2.8 | CF |
| C | 30 | 2.5 | 7.2 | C |
Tasks:
- Project each player's WAR for the next 5 years using age curves
- Estimate when players will be lost to free agency
- Identify your competitive window (when team projects >87 wins)
- 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:
- Calculate his expected arbitration salaries (years 3-6 of service)
- Estimate his free agent value at age 27
- 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
- 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:
- Calculate surplus value for each team
- Determine if trade is balanced for all three teams
- Identify which team gets the best deal
- Suggest modifications to improve balance
- 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:
- Takes player inputs (name, WAR projection, service time, age)
- Calculates surplus value for each player
- Simulates playoff probability impact
- Outputs trade recommendation for both teams
- 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:
- Trade Deadline Strategy: Understanding the expected value framework helps teams make rational buy/sell decisions based on playoff odds and prospect value.
- Prospect Valuation: Probabilistic modeling of prospect outcomes accounts for development risk and allows for fair trade comparisons.
- Surplus Value: The fundamental currency of team-building, allowing comparison across free agents, arbitration players, and prospects.
- Contract Optimization: Knowing when to extend players requires balancing risk, team control, and market value.
- 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)