NBA Salary Cap Explained

Beginner 10 min read 1 views Nov 27, 2025

NBA Salary Cap Basics

Introduction to the NBA Salary Cap

The NBA Salary Cap is a complex system designed to promote competitive balance and financial stability across the league. Unlike "hard cap" systems in other sports, the NBA employs a soft cap with numerous exceptions that allow teams to exceed the cap under specific circumstances.

Understanding the salary cap is crucial for analyzing team construction, player value, and front office decision-making. The cap affects everything from free agency to trades to the draft, making it one of the most important factors in modern NBA analytics.

2023-24 Salary Cap Numbers

  • Salary Cap: $136,021,000
  • Luxury Tax Threshold: $165,294,000
  • First Apron: $172,346,000
  • Second Apron: $182,794,000
  • Minimum Team Salary: $122,418,900 (90% of cap)

Soft Cap vs Hard Cap Systems

NBA Soft Cap System

The NBA uses a soft cap, meaning teams can exceed the salary cap limit using various exceptions. This creates flexibility for teams to retain their own players and improve their rosters even when over the cap.

Soft Cap Characteristics:

  • Teams can exceed the cap using exceptions (Bird Rights, MLE, etc.)
  • No absolute maximum team payroll (though luxury tax creates disincentive)
  • Favors player retention and allows competitive teams to reload
  • More complex rules requiring strategic cap management
  • Creates wide disparity in team payrolls ($150M+ difference possible)

Hard Cap Triggers (New CBA 2023)

While the NBA has a soft cap, certain transactions trigger a hard cap that teams cannot exceed for that season:

  • Using Non-Taxpayer Mid-Level Exception: Hard cap at first apron ($172.3M)
  • Using Bi-Annual Exception: Hard cap at first apron
  • Sign-and-Trade Reception: Hard cap at first apron
Important: The 2023 CBA introduced "aprons" above the luxury tax threshold that create increasingly severe restrictions on team-building flexibility.

Comparison: NFL vs MLB vs NBA

League Cap Type Key Features
NFL Hard Cap Strict limit, no exceptions, heavy roster turnover
MLB Luxury Tax (No Cap) No salary cap, only competitive balance tax, huge payroll disparities
NBA Soft Cap Cap with exceptions, luxury tax, apron restrictions
NHL Hard Cap Strict limit with limited exceptions

Luxury Tax System

Overview

The luxury tax (officially the "competitive balance tax") is a penalty charged to teams whose total payroll exceeds the luxury tax threshold. It's designed to discourage excessive spending and redistribute wealth to smaller-market teams.

Tax Calculation (Progressive Rates)

The luxury tax is calculated on a progressive scale based on how far over the threshold a team goes:

Amount Over Tax Line Tax Rate (Non-Repeater) Tax Rate (Repeater)
$0 - $5M $1.50 per $1 $2.50 per $1
$5M - $10M $1.75 per $1 $2.75 per $1
$10M - $15M $2.50 per $1 $3.50 per $1
$15M - $20M $3.25 per $1 $4.25 per $1
$20M+ $3.75 per $1 (incremental increases) $4.75 per $1 (incremental increases)

Repeater Tax Definition

A team is a repeater if they paid luxury tax in at least 3 of the 4 previous seasons. Repeater penalties are significantly higher.

Tax Example Calculation

Team A Payroll: $180M
Tax Threshold: $165.3M
Overage: $14.7M
Repeater Status: Non-Repeater

Tax Calculation:

  • First $5M over: $5M × $1.50 = $7.5M
  • Next $5M over: $5M × $1.75 = $8.75M
  • Next $4.7M over: $4.7M × $2.50 = $11.75M
  • Total Tax Bill: $28M
  • Total Cost: $208M (payroll + tax)

First and Second Aprons (2023 CBA)

The new CBA introduced two additional thresholds above the luxury tax line that trigger increasingly severe team-building restrictions:

First Apron Restrictions ($172.3M in 2023-24)

  • Cannot use the Taxpayer Mid-Level Exception (limited to smaller BAE)
  • Cannot acquire players via sign-and-trade
  • Cannot use cash in trades to acquire players
  • More restrictive salary matching rules in trades

Second Apron Restrictions ($182.8M in 2023-24)

  • Cannot trade multiple players for one player
  • Cannot aggregate salaries in trades
  • Future first-round pick frozen (cannot trade 7 years out)
  • Stricter trade matching (110% instead of 125%)
  • Draft pick penalty: If team is over second apron in 2+ of 5 seasons, their highest-earning pick in next draft moves to end of first round
Impact: These apron restrictions fundamentally change roster construction for high-spending teams, making it much harder to improve via trades and free agency.

Salary Cap Exceptions

Exceptions allow teams to sign players even when over the salary cap. Each exception has specific rules, limitations, and strategic implications.

1. Larry Bird Exception

The Bird Exception (named after Larry Bird) allows teams to re-sign their own free agents without cap restrictions, even if over the cap.

Requirements (Full Bird Rights):

  • Player must be with team for 3+ consecutive seasons (via any means)
  • Player not waived and re-signed in the same or previous season
  • Team retains Bird Rights even if player changes teams via trade

Contract Limitations:

  • Salary Start: Up to player's maximum salary (based on years of service)
  • Length: Maximum 5 years
  • Raises: Up to 8% annually

Variations:

  • Early Bird: 2 years with team, max 175% of previous salary or average salary (whichever higher), 4 years max, 5% raises
  • Non-Bird: 1+ year with team, max 120% of previous salary or minimum salary (whichever higher), 4 years max, 5% raises
Example: Golden State re-signing Stephen Curry to a 4-year, $215M extension in 2021 despite being $40M+ over the cap (using Full Bird Rights).

2. Mid-Level Exception (MLE)

The MLE allows teams to sign free agents at a set amount regardless of cap space. There are three types based on team cap situation:

MLE Type 2023-24 Amount Max Length Availability
Non-Taxpayer MLE $12,405,000 4 years Under luxury tax threshold; triggers hard cap at first apron
Taxpayer MLE $5,000,000 3 years Over luxury tax, under first apron
Room MLE $7,700,000 2 years Used cap space, now under the cap

Strategic Considerations:

  • Non-Taxpayer MLE triggers hard cap: must plan entire roster around it
  • Can split MLE among multiple players (total cannot exceed MLE amount)
  • Using MLE wisely can be difference between contender and pretender
  • Taxpayer MLE significantly less valuable ($5M vs $12.4M)

3. Bi-Annual Exception (BAE)

Details:

  • Amount (2023-24): $4,500,000
  • Length: Maximum 2 years
  • Availability: Can only be used every other year
  • Restriction: Not available if over first apron
  • Hard Cap: Triggers hard cap at first apron

4. Rookie Exception

Allows teams to sign their drafted rookies or first-round picks acquired via trade, even if over the cap.

  • Based on pick number (slotted scale)
  • First-round picks: Guaranteed scale contracts (120% to 80% of slot)
  • Can sign to max 4 years (2 guaranteed, 2 team options)

5. Minimum Salary Exception

Teams can sign unlimited players to minimum contracts regardless of cap situation.

  • Based on years of NBA service
  • Only counts against cap at minimum amount (even if player has higher minimum due to service time)
  • Crucial for filling out rosters for capped-out teams

6. Disabled Player Exception (DPE)

Criteria:

  • Player suffers season-ending or career-ending injury
  • Team physician and NBA-designated doctor must certify injury
  • Amount: 50% of injured player's salary OR non-taxpayer MLE (whichever less)
  • Usage: Sign a replacement player or acquire via trade
  • Length: One year only

Maximum Contracts

Max Salary Calculation

Maximum salaries are based on a player's years of NBA service and are calculated as a percentage of the salary cap:

Years of Service % of Salary Cap 2023-24 Max Salary
0-6 years 25% $34,005,250
7-9 years 30% $40,806,300
10+ years 35% $47,607,350

Designated Player Extensions

Teams can offer "supermax" contracts (Designated Veteran Player Extension) to select players:

Criteria for Supermax (35% max):

  • Player won MVP in previous 3 seasons, OR
  • Player made All-NBA team in previous season or 2 of last 3 seasons, OR
  • Player won Defensive Player of the Year in previous season or 2 of last 3 seasons
  • Player must be with team that drafted them (or traded for on rookie contract)

Contract Terms:

  • Starting Salary: 35% of cap (instead of 30%)
  • Length: Up to 5 years
  • Raises: 8% annually
  • Total Value: Can exceed $250M+ for elite players

Max Contract Examples (2023-24)

Player Type Total Value Years Avg/Year
Jaylen Brown Supermax (35%) $304M 5 years $60.8M
Nikola Jokić Supermax (35%) $276M 5 years $55.2M
Damian Lillard Veteran Max (30%) $216M 4 years $54M
Anthony Edwards Rookie Extension (25%) $207M 5 years $41.4M
Rose Rule: Players who make All-NBA (at any team), win MVP, or win DPOY on their rookie scale contract can sign for 30% of cap instead of 25% on their extension (named after Derrick Rose).

Minimum Salary Scale

NBA Minimum Salaries by Years of Service

Minimum salaries increase based on years of NBA experience:

Years of Service 2023-24 Minimum 2024-25 Minimum
0$1,119,563$1,157,153
1$1,836,090$1,902,133
2$1,988,965$2,087,519
3$2,087,519$2,162,606
4$2,162,606$2,237,691
5$2,346,614$2,425,403
6$2,530,680$2,613,120
7$2,714,746$2,800,837
8$2,898,812$2,988,554
9$2,953,694$3,045,234
10+$3,196,448$3,303,771

Veteran Minimum Cap Hit

Important Rule: When a team signs a veteran to a minimum salary contract, only the 2-year minimum salary ($1,988,965 in 2023-24) counts against the cap, regardless of the player's actual minimum.

This allows teams to sign veteran players without being penalized for their years of service, encouraging teams to add experienced players. The league reimburses the difference.

Example: Team signs 10-year veteran to minimum ($3,196,448)
Cap Hit: $1,988,965 (2-year minimum)
Savings: $1,207,483

Cap Holds

What Are Cap Holds?

Cap holds are placeholder charges against a team's salary cap for players whose rights the team still holds but who are not currently under contract (free agents, unsigned draft picks, etc.).

Types of Cap Holds

1. Free Agent Cap Holds

When a player's contract expires, a cap hold remains on the team's books until the player is renounced or re-signed:

Bird Rights Status Cap Hold Amount
Full Bird Rights 190% of previous salary (or maximum salary if less)
Early Bird Rights 130% of previous salary (or maximum salary if less)
Non-Bird Rights 120% of previous salary (or minimum salary if greater)
Minimum Salary Player Minimum salary for player's years of service

2. Unsigned First-Round Pick Cap Holds

  • Equal to 120% of rookie scale amount for that pick
  • Remains until player is signed or rights are renounced
  • Counts even if pick is traded (until signed by new team)

3. Offer Sheet Cap Holds

  • When restricted free agent receives offer sheet
  • Hold equals average salary of offer sheet
  • Lasts until team matches or declines (3 days)

Strategic Use of Cap Holds

Retaining Cap Holds:

  • Preserve Bird Rights: Allows re-signing player over the cap later
  • Negotiating leverage: Keep options open during free agency
  • Timing flexibility: Sign other free agents first, then use Bird Rights

Renouncing Cap Holds:

  • Create cap space: Necessary to clear room for max free agents
  • Lose Bird Rights: Cannot re-sign player over cap (only with cap space or exception)
  • Irreversible: Once renounced, Bird Rights cannot be recovered

Cap Hold Examples

Scenario 1: Keeping Cap Hold

Player: Starting guard, previous salary $15M
Cap Hold: $28.5M (190% for Full Bird)
Strategy: Team uses cap space on other players, then re-signs guard for $20M using Bird Rights

Scenario 2: Renouncing Cap Hold

Player: Backup center, previous salary $8M
Cap Hold: $15.2M (190% for Full Bird)
Strategy: Team renounces to create max cap space, lets player walk
Result: Gain $15.2M in cap space but lose Bird Rights

Team Salary Calculation with Cap Holds

Total Team Salary for Cap Purposes:

Total Cap Number =
    Active Player Salaries
  + Free Agent Cap Holds
  + Unsigned Draft Pick Cap Holds
  + Trade Bonuses
  + Offer Sheet Cap Holds
  - Trade Exceptions (don't count against cap)
            

Salary Cap Analysis Code Examples

Python Example: Salary Cap Analysis


"""
NBA Salary Cap Analysis Tools
Analyze team cap situations, calculate tax bills, model cap scenarios

Install: pip install pandas numpy matplotlib requests beautifulsoup4
"""

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from typing import Dict, List, Tuple

# 2023-24 Salary Cap Constants
SALARY_CAP = 136_021_000
LUXURY_TAX = 165_294_000
FIRST_APRON = 172_346_000
SECOND_APRON = 182_794_000
MIN_TEAM_SALARY = 122_418_900

# Tax Rates (Non-Repeater)
TAX_RATES = [
    (5_000_000, 1.50),
    (10_000_000, 1.75),
    (15_000_000, 2.50),
    (20_000_000, 3.25),
    (float('inf'), 3.75)
]

# Repeater Tax Rates
REPEATER_TAX_RATES = [
    (5_000_000, 2.50),
    (10_000_000, 2.75),
    (15_000_000, 3.50),
    (20_000_000, 4.25),
    (float('inf'), 4.75)
]

# Maximum Salaries by Years of Service
MAX_SALARIES = {
    '0-6': 0.25 * SALARY_CAP,
    '7-9': 0.30 * SALARY_CAP,
    '10+': 0.35 * SALARY_CAP
}

# Minimum Salaries by Years of Service (2023-24)
MIN_SALARIES = {
    0: 1_119_563,
    1: 1_836_090,
    2: 1_988_965,
    3: 2_087_519,
    4: 2_162_606,
    5: 2_346_614,
    6: 2_530_680,
    7: 2_714_746,
    8: 2_898_812,
    9: 2_953_694,
    10: 3_196_448
}


def calculate_luxury_tax(team_salary: float, is_repeater: bool = False) -> Dict:
    """
    Calculate luxury tax bill for a team

    Args:
        team_salary: Total team salary
        is_repeater: Whether team is repeater tax offender

    Returns:
        Dictionary with tax details
    """
    if team_salary <= LUXURY_TAX:
        return {
            'overage': 0,
            'tax_bill': 0,
            'total_cost': team_salary,
            'effective_rate': 0
        }

    overage = team_salary - LUXURY_TAX
    tax_bill = 0

    rates = REPEATER_TAX_RATES if is_repeater else TAX_RATES

    previous_threshold = 0
    for threshold, rate in rates:
        if overage <= previous_threshold:
            break

        taxable_amount = min(overage, threshold) - previous_threshold
        tax_bill += taxable_amount * rate

        previous_threshold = threshold

    total_cost = team_salary + tax_bill
    effective_rate = tax_bill / overage if overage > 0 else 0

    return {
        'overage': overage,
        'tax_bill': tax_bill,
        'total_cost': total_cost,
        'effective_rate': effective_rate,
        'repeater': is_repeater
    }


def calculate_max_salary(years_of_service: int, supermax: bool = False) -> float:
    """
    Calculate maximum salary for a player

    Args:
        years_of_service: Player's years in NBA
        supermax: Whether player qualifies for supermax (35%)

    Returns:
        Maximum salary amount
    """
    if supermax:
        return 0.35 * SALARY_CAP

    if years_of_service <= 6:
        return MAX_SALARIES['0-6']
    elif years_of_service <= 9:
        return MAX_SALARIES['7-9']
    else:
        return MAX_SALARIES['10+']


def calculate_cap_hold(previous_salary: float, bird_rights: str = 'full') -> float:
    """
    Calculate cap hold for free agent

    Args:
        previous_salary: Player's previous season salary
        bird_rights: 'full', 'early', or 'non'

    Returns:
        Cap hold amount
    """
    if bird_rights == 'full':
        percentage = 1.90
    elif bird_rights == 'early':
        percentage = 1.30
    else:  # non-bird
        percentage = 1.20

    cap_hold = previous_salary * percentage

    # Cap hold cannot exceed maximum salary
    max_salary = calculate_max_salary(10)  # Use highest max as ceiling

    return min(cap_hold, max_salary)


def project_contract_value(
    starting_salary: float,
    years: int,
    annual_raise: float = 0.08
) -> pd.DataFrame:
    """
    Project multi-year contract values

    Args:
        starting_salary: First year salary
        years: Contract length
        annual_raise: Annual raise percentage (default 8% for Bird)

    Returns:
        DataFrame with year-by-year breakdown
    """
    contract = []

    for year in range(1, years + 1):
        salary = starting_salary * (1 + annual_raise) ** (year - 1)
        contract.append({
            'Year': year,
            'Salary': salary,
            'Cumulative': sum([starting_salary * (1 + annual_raise) ** i
                             for i in range(year)])
        })

    df = pd.DataFrame(contract)
    df['Total_Value'] = df['Cumulative'].iloc[-1]

    return df


def analyze_team_cap_situation(roster_salaries: List[float]) -> Dict:
    """
    Analyze a team's cap situation

    Args:
        roster_salaries: List of player salaries

    Returns:
        Dictionary with cap analysis
    """
    total_salary = sum(roster_salaries)

    cap_space = max(0, SALARY_CAP - total_salary)
    room_exception = 7_700_000 if cap_space > 0 else 0

    # Determine available exceptions
    if total_salary < LUXURY_TAX:
        mle = 12_405_000  # Non-taxpayer MLE
        bae = 4_500_000
    elif total_salary < FIRST_APRON:
        mle = 5_000_000  # Taxpayer MLE
        bae = 0
    else:
        mle = 5_000_000
        bae = 0

    tax_info = calculate_luxury_tax(total_salary)

    return {
        'total_salary': total_salary,
        'cap_space': cap_space,
        'under_cap': total_salary < SALARY_CAP,
        'over_tax': total_salary > LUXURY_TAX,
        'over_first_apron': total_salary > FIRST_APRON,
        'over_second_apron': total_salary > SECOND_APRON,
        'available_mle': mle,
        'available_bae': bae,
        'room_exception': room_exception,
        'tax_bill': tax_info['tax_bill'],
        'total_cost': tax_info['total_cost']
    }


def visualize_luxury_tax_brackets():
    """
    Create visualization of luxury tax brackets
    """
    overages = np.linspace(0, 50_000_000, 1000)
    tax_bills_regular = []
    tax_bills_repeater = []

    for overage in overages:
        salary = LUXURY_TAX + overage

        regular = calculate_luxury_tax(salary, is_repeater=False)
        repeater = calculate_luxury_tax(salary, is_repeater=True)

        tax_bills_regular.append(regular['tax_bill'])
        tax_bills_repeater.append(repeater['tax_bill'])

    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))

    # Tax bill visualization
    ax1.plot(overages / 1e6, np.array(tax_bills_regular) / 1e6,
             label='Non-Repeater', linewidth=2, color='#1f77b4')
    ax1.plot(overages / 1e6, np.array(tax_bills_repeater) / 1e6,
             label='Repeater', linewidth=2, color='#ff7f0e')
    ax1.set_xlabel('Overage (Millions)', fontsize=12)
    ax1.set_ylabel('Tax Bill (Millions)', fontsize=12)
    ax1.set_title('Luxury Tax Bill by Overage Amount', fontsize=14, fontweight='bold')
    ax1.legend()
    ax1.grid(True, alpha=0.3)

    # Total cost visualization
    total_cost_regular = (LUXURY_TAX + overages + np.array(tax_bills_regular)) / 1e6
    total_cost_repeater = (LUXURY_TAX + overages + np.array(tax_bills_repeater)) / 1e6

    ax2.plot(overages / 1e6, total_cost_regular,
             label='Non-Repeater', linewidth=2, color='#1f77b4')
    ax2.plot(overages / 1e6, total_cost_repeater,
             label='Repeater', linewidth=2, color='#ff7f0e')
    ax2.axhline(y=LUXURY_TAX / 1e6, color='red', linestyle='--',
                linewidth=1, label='Tax Threshold')
    ax2.set_xlabel('Overage (Millions)', fontsize=12)
    ax2.set_ylabel('Total Cost (Millions)', fontsize=12)
    ax2.set_title('Total Team Cost (Salary + Tax)', fontsize=14, fontweight='bold')
    ax2.legend()
    ax2.grid(True, alpha=0.3)

    plt.tight_layout()
    plt.savefig('luxury_tax_analysis.png', dpi=300, bbox_inches='tight')
    plt.show()


def compare_signing_scenarios(
    player_salary: float,
    current_team_salary: float,
    is_repeater: bool = False
) -> pd.DataFrame:
    """
    Compare cost of signing player under different tax scenarios

    Args:
        player_salary: Proposed player salary
        current_team_salary: Current team salary before signing
        is_repeater: Repeater tax status

    Returns:
        DataFrame comparing scenarios
    """
    scenarios = []

    # Scenario 1: Don't sign player
    baseline = calculate_luxury_tax(current_team_salary, is_repeater)
    scenarios.append({
        'Scenario': 'No Signing',
        'Team_Salary': current_team_salary,
        'Tax_Bill': baseline['tax_bill'],
        'Total_Cost': baseline['total_cost'],
        'Marginal_Cost': 0
    })

    # Scenario 2: Sign player
    new_salary = current_team_salary + player_salary
    with_player = calculate_luxury_tax(new_salary, is_repeater)
    marginal_cost = with_player['total_cost'] - baseline['total_cost']

    scenarios.append({
        'Scenario': 'Sign Player',
        'Team_Salary': new_salary,
        'Tax_Bill': with_player['tax_bill'],
        'Total_Cost': with_player['total_cost'],
        'Marginal_Cost': marginal_cost
    })

    df = pd.DataFrame(scenarios)
    df['Tax_Multiplier'] = df['Marginal_Cost'] / player_salary

    return df


# Example usage
if __name__ == "__main__":
    print("=== NBA Salary Cap Analysis ===\n")

    # Example 1: Calculate luxury tax
    print("Example 1: Luxury Tax Calculation")
    team_salary = 180_000_000
    tax_info = calculate_luxury_tax(team_salary, is_repeater=False)
    print(f"Team Salary: ${team_salary:,.0f}")
    print(f"Overage: ${tax_info['overage']:,.0f}")
    print(f"Tax Bill: ${tax_info['tax_bill']:,.0f}")
    print(f"Total Cost: ${tax_info['total_cost']:,.0f}")
    print(f"Effective Tax Rate: {tax_info['effective_rate']:.2f}x\n")

    # Example 2: Max contract projection
    print("Example 2: Supermax Contract Projection")
    supermax = project_contract_value(
        starting_salary=0.35 * SALARY_CAP,
        years=5,
        annual_raise=0.08
    )
    print(supermax)
    print(f"\nTotal Contract Value: ${supermax['Total_Value'].iloc[0]:,.0f}\n")

    # Example 3: Team cap situation
    print("Example 3: Team Cap Situation Analysis")
    roster = [45_000_000, 35_000_000, 25_000_000, 18_000_000, 12_000_000,
              8_000_000, 5_000_000, 3_000_000, 2_500_000, 2_000_000,
              1_800_000, 1_500_000]

    cap_situation = analyze_team_cap_situation(roster)
    print(f"Total Salary: ${cap_situation['total_salary']:,.0f}")
    print(f"Cap Space: ${cap_situation['cap_space']:,.0f}")
    print(f"Over Tax Line: {cap_situation['over_tax']}")
    print(f"Available MLE: ${cap_situation['available_mle']:,.0f}")
    print(f"Tax Bill: ${cap_situation['tax_bill']:,.0f}\n")

    # Example 4: Signing scenario comparison
    print("Example 4: Marginal Cost of Signing Player")
    comparison = compare_signing_scenarios(
        player_salary=15_000_000,
        current_team_salary=170_000_000,
        is_repeater=False
    )
    print(comparison)

    # Create visualizations
    print("\nGenerating luxury tax visualization...")
    visualize_luxury_tax_brackets()

    print("\nAnalysis complete!")

R Example: Salary Cap Analysis


# NBA Salary Cap Analysis in R
# Comprehensive tools for cap calculations and team planning
# Install: install.packages(c("tidyverse", "scales", "ggplot2"))

library(tidyverse)
library(scales)
library(ggplot2)

# 2023-24 Salary Cap Constants
SALARY_CAP <- 136021000
LUXURY_TAX <- 165294000
FIRST_APRON <- 172346000
SECOND_APRON <- 182794000
MIN_TEAM_SALARY <- 122418900

# Maximum Salaries
MAX_SALARIES <- list(
  "0-6" = 0.25 * SALARY_CAP,
  "7-9" = 0.30 * SALARY_CAP,
  "10+" = 0.35 * SALARY_CAP
)

#' Calculate luxury tax bill
#'
#' @param team_salary Total team salary
#' @param is_repeater Whether team is repeater offender
#' @return List with tax details
calculate_luxury_tax <- function(team_salary, is_repeater = FALSE) {
  if (team_salary <= LUXURY_TAX) {
    return(list(
      overage = 0,
      tax_bill = 0,
      total_cost = team_salary,
      effective_rate = 0
    ))
  }

  overage <- team_salary - LUXURY_TAX

  # Define tax brackets
  if (is_repeater) {
    brackets <- tibble(
      threshold = c(5e6, 10e6, 15e6, 20e6, Inf),
      rate = c(2.50, 2.75, 3.50, 4.25, 4.75)
    )
  } else {
    brackets <- tibble(
      threshold = c(5e6, 10e6, 15e6, 20e6, Inf),
      rate = c(1.50, 1.75, 2.50, 3.25, 3.75)
    )
  }

  # Calculate progressive tax
  tax_bill <- 0
  previous_threshold <- 0

  for (i in 1:nrow(brackets)) {
    if (overage <= previous_threshold) break

    taxable_amount <- min(overage, brackets$threshold[i]) - previous_threshold
    tax_bill <- tax_bill + (taxable_amount * brackets$rate[i])

    previous_threshold <- brackets$threshold[i]
  }

  total_cost <- team_salary + tax_bill
  effective_rate <- if (overage > 0) tax_bill / overage else 0

  return(list(
    overage = overage,
    tax_bill = tax_bill,
    total_cost = total_cost,
    effective_rate = effective_rate,
    repeater = is_repeater
  ))
}


#' Calculate maximum salary
#'
#' @param years_of_service Player's NBA years
#' @param supermax Whether player qualifies for supermax
#' @return Maximum salary amount
calculate_max_salary <- function(years_of_service, supermax = FALSE) {
  if (supermax) {
    return(0.35 * SALARY_CAP)
  }

  if (years_of_service <= 6) {
    return(MAX_SALARIES[["0-6"]])
  } else if (years_of_service <= 9) {
    return(MAX_SALARIES[["7-9"]])
  } else {
    return(MAX_SALARIES[["10+"]])
  }
}


#' Project contract value over multiple years
#'
#' @param starting_salary First year salary
#' @param years Contract length
#' @param annual_raise Annual raise percentage
#' @return Tibble with year-by-year breakdown
project_contract <- function(starting_salary, years, annual_raise = 0.08) {
  tibble(
    Year = 1:years,
    Salary = starting_salary * (1 + annual_raise)^(Year - 1)
  ) %>%
    mutate(
      Cumulative = cumsum(Salary),
      Total_Value = last(Cumulative)
    )
}


#' Analyze team cap situation
#'
#' @param roster_salaries Vector of player salaries
#' @return List with cap analysis
analyze_team_cap <- function(roster_salaries) {
  total_salary <- sum(roster_salaries)

  cap_space <- max(0, SALARY_CAP - total_salary)

  # Determine available exceptions
  if (total_salary < LUXURY_TAX) {
    mle <- 12405000  # Non-taxpayer MLE
    bae <- 4500000
  } else if (total_salary < FIRST_APRON) {
    mle <- 5000000   # Taxpayer MLE
    bae <- 0
  } else {
    mle <- 5000000
    bae <- 0
  }

  tax_info <- calculate_luxury_tax(total_salary)

  list(
    total_salary = total_salary,
    cap_space = cap_space,
    under_cap = total_salary < SALARY_CAP,
    over_tax = total_salary > LUXURY_TAX,
    over_first_apron = total_salary > FIRST_APRON,
    over_second_apron = total_salary > SECOND_APRON,
    available_mle = mle,
    available_bae = bae,
    tax_bill = tax_info$tax_bill,
    total_cost = tax_info$total_cost
  )
}


#' Visualize luxury tax structure
visualize_tax_brackets <- function() {
  # Generate data
  overages <- seq(0, 50e6, length.out = 1000)

  tax_data <- map_dfr(overages, ~{
    regular <- calculate_luxury_tax(LUXURY_TAX + .x, is_repeater = FALSE)
    repeater <- calculate_luxury_tax(LUXURY_TAX + .x, is_repeater = TRUE)

    tibble(
      Overage = .x,
      Regular_Tax = regular$tax_bill,
      Repeater_Tax = repeater$tax_bill,
      Regular_Total = regular$total_cost,
      Repeater_Total = repeater$total_cost
    )
  })

  # Tax bill plot
  p1 <- tax_data %>%
    pivot_longer(
      cols = c(Regular_Tax, Repeater_Tax),
      names_to = "Type",
      values_to = "Tax_Bill"
    ) %>%
    mutate(Type = str_remove(Type, "_Tax")) %>%
    ggplot(aes(x = Overage / 1e6, y = Tax_Bill / 1e6, color = Type)) +
    geom_line(size = 1.5) +
    scale_color_manual(values = c("Regular" = "#1f77b4", "Repeater" = "#ff7f0e")) +
    labs(
      title = "Luxury Tax Bill by Overage Amount",
      x = "Overage (Millions)",
      y = "Tax Bill (Millions)",
      color = "Tax Status"
    ) +
    theme_minimal() +
    theme(
      plot.title = element_text(size = 14, face = "bold"),
      legend.position = "top"
    )

  # Total cost plot
  p2 <- tax_data %>%
    pivot_longer(
      cols = c(Regular_Total, Repeater_Total),
      names_to = "Type",
      values_to = "Total_Cost"
    ) %>%
    mutate(Type = str_remove(Type, "_Total")) %>%
    ggplot(aes(x = Overage / 1e6, y = Total_Cost / 1e6, color = Type)) +
    geom_line(size = 1.5) +
    geom_hline(yintercept = LUXURY_TAX / 1e6, linetype = "dashed",
               color = "red", size = 0.8) +
    scale_color_manual(values = c("Regular" = "#1f77b4", "Repeater" = "#ff7f0e")) +
    labs(
      title = "Total Team Cost (Salary + Tax)",
      x = "Overage (Millions)",
      y = "Total Cost (Millions)",
      color = "Tax Status"
    ) +
    theme_minimal() +
    theme(
      plot.title = element_text(size = 14, face = "bold"),
      legend.position = "top"
    )

  # Combine plots
  gridExtra::grid.arrange(p1, p2, ncol = 2)

  ggsave("luxury_tax_visualization.png",
         gridExtra::arrangeGrob(p1, p2, ncol = 2),
         width = 14, height = 6, dpi = 300)
}


#' Compare marginal cost of signing player
#'
#' @param player_salary Proposed player salary
#' @param current_salary Current team salary
#' @param is_repeater Repeater status
compare_signing_scenarios <- function(player_salary, current_salary, is_repeater = FALSE) {
  baseline <- calculate_luxury_tax(current_salary, is_repeater)
  with_player <- calculate_luxury_tax(current_salary + player_salary, is_repeater)

  marginal_cost <- with_player$total_cost - baseline$total_cost
  tax_multiplier <- marginal_cost / player_salary

  tibble(
    Scenario = c("No Signing", "Sign Player"),
    Team_Salary = c(current_salary, current_salary + player_salary),
    Tax_Bill = c(baseline$tax_bill, with_player$tax_bill),
    Total_Cost = c(baseline$total_cost, with_player$total_cost),
    Marginal_Cost = c(0, marginal_cost),
    Tax_Multiplier = c(0, tax_multiplier)
  )
}


# Example usage
main <- function() {
  cat("=== NBA Salary Cap Analysis ===\n\n")

  # Example 1: Luxury tax calculation
  cat("Example 1: Luxury Tax Calculation\n")
  team_salary <- 180e6
  tax_info <- calculate_luxury_tax(team_salary, is_repeater = FALSE)

  cat(sprintf("Team Salary: $%s\n", dollar(team_salary)))
  cat(sprintf("Overage: $%s\n", dollar(tax_info$overage)))
  cat(sprintf("Tax Bill: $%s\n", dollar(tax_info$tax_bill)))
  cat(sprintf("Total Cost: $%s\n", dollar(tax_info$total_cost)))
  cat(sprintf("Effective Rate: %.2fx\n\n", tax_info$effective_rate))

  # Example 2: Supermax projection
  cat("Example 2: Supermax Contract Projection\n")
  supermax <- project_contract(
    starting_salary = 0.35 * SALARY_CAP,
    years = 5,
    annual_raise = 0.08
  )
  print(supermax)
  cat(sprintf("\nTotal Value: $%s\n\n", dollar(supermax$Total_Value[1])))

  # Example 3: Team cap analysis
  cat("Example 3: Team Cap Situation\n")
  roster <- c(45e6, 35e6, 25e6, 18e6, 12e6, 8e6, 5e6, 3e6, 2.5e6, 2e6, 1.8e6, 1.5e6)

  cap_situation <- analyze_team_cap(roster)
  cat(sprintf("Total Salary: $%s\n", dollar(cap_situation$total_salary)))
  cat(sprintf("Cap Space: $%s\n", dollar(cap_situation$cap_space)))
  cat(sprintf("Over Tax: %s\n", cap_situation$over_tax))
  cat(sprintf("Available MLE: $%s\n", dollar(cap_situation$available_mle)))
  cat(sprintf("Tax Bill: $%s\n\n", dollar(cap_situation$tax_bill)))

  # Example 4: Signing comparison
  cat("Example 4: Marginal Cost Analysis\n")
  comparison <- compare_signing_scenarios(
    player_salary = 15e6,
    current_salary = 170e6,
    is_repeater = FALSE
  )
  print(comparison)

  # Visualizations
  cat("\nGenerating visualizations...\n")
  visualize_tax_brackets()

  cat("\nAnalysis complete!\n")
}

# Run analysis
main()

Discussion

Have questions or feedback? Join our community discussion on Discord or GitHub Discussions.