Contract health
Contract value, long-term risk, mobility, and optionality compare contractual obligations with frozen expected-salary evidence.
Aggregation
The table below is extracted from the pillar's declared metric weights at build time. Open the metric implementation notes and source to inspect its raw inputs, normalization, branches, and fallback handling.
pillar_score = Σ(metric_score × metric_weight)
Metric weights
| Metric | Declared weight |
|---|---|
| Contract value | 35.0% |
| Long term risk | 30.0% |
| Contract mobility | 20.0% |
| Contract optionality | 15.0% |
Implementation
These notes and source files are captured from this deployment's code. Expand a module for its algorithm and branches, or download the Elixir file for local inspection.
contract_health.exElixir
lib/trade_machine/gm_grade_v2/pillars/contract_health.ex
Aggregates AVC's four Contract Health metrics over the final roster.
Contract Value, Long-Term Risk, Contract Mobility, and Contract Optionality retain full precision through the fixed 35/30/20/15 weighted sum.
SHA-256 34e37af5e2dd63e1862f616ad1b86753d9026d98cacd57da82fbe0f09f2f990f
defmodule TradeMachine.GMGradeV2.Pillars.ContractHealth do
@moduledoc """
Aggregates AVC's four Contract Health metrics over the final roster.
Contract Value, Long-Term Risk, Contract Mobility, and Contract Optionality
retain full precision through the fixed 35/30/20/15 weighted sum.
"""
alias TradeMachine.GMGradeV2.Benchmark
alias TradeMachine.GMGradeV2.ChallengeSnapshot
alias TradeMachine.GMGradeV2.Pillars.ContractHealth.ContractMobility
alias TradeMachine.GMGradeV2.Pillars.ContractHealth.ContractOptionality
alias TradeMachine.GMGradeV2.Pillars.ContractHealth.ContractValue
alias TradeMachine.GMGradeV2.Pillars.ContractHealth.LongTermRisk
alias TradeMachine.GMGradeV2.Result.MetricResult
alias TradeMachine.GMGradeV2.Result.PillarResult
@metric_weights %{
contract_value: 0.35,
long_term_risk: 0.30,
contract_mobility: 0.20,
contract_optionality: 0.15
}
@metric_modules [
ContractValue,
LongTermRisk,
ContractMobility,
ContractOptionality
]
if abs(Enum.sum(Map.values(@metric_weights)) - 1.0) > 1.0e-9 do
raise "Contract Health metric weights must sum to 1.0"
end
@doc "Returns the full-precision Contract Health pillar result."
@spec score(map(), ChallengeSnapshot.t()) :: PillarResult.t()
def score(
scenario_state,
%ChallengeSnapshot{benchmark: %Benchmark{} = benchmark} = snapshot
)
when is_map(scenario_state) do
metric_results = Enum.map(@metric_modules, & &1.score(scenario_state, snapshot))
{strongest, weakest} = extrema(metric_results)
%PillarResult{
key: :contract_health,
version: benchmark.version,
score: weighted_score(metric_results),
metric_results: metric_results,
metric_weights: @metric_weights,
explanation: explanation(strongest, weakest),
strongest: strongest.key,
weakest: weakest.key
}
end
defp weighted_score(metric_results) do
Enum.sum(
Enum.map(metric_results, fn result ->
result.score * Map.fetch!(@metric_weights, result.key)
end)
)
end
defp extrema(metric_results) do
{
Enum.max_by(metric_results, & &1.score),
Enum.min_by(metric_results, & &1.score)
}
end
defp explanation(
%MetricResult{key: strongest_key, score: strongest_score},
%MetricResult{key: weakest_key, score: weakest_score}
) do
"Contract Health combines four final-roster contract metrics; " <>
"#{label(strongest_key)} is strongest at #{strongest_score}, while " <>
"#{label(weakest_key)} is weakest at #{weakest_score}."
end
defp label(:contract_value), do: "Contract Value"
defp label(:long_term_risk), do: "Long-Term Risk"
defp label(:contract_mobility), do: "Contract Mobility"
defp label(:contract_optionality), do: "Contract Optionality"
end
contract_mobility.exElixir
lib/trade_machine/gm_grade_v2/pillars/contract_health/contract_mobility.ex
Scores how easily the final roster's active contracts can be traded.
Player mobility blends Contract Value marketability (60%), salary utility
(25%), and guaranteed-term utility (15%), then applies every stored trade
restriction multiplicatively. Player options and trade bonuses of at least
5% each apply
0.9
; a no-trade clause applies
0.5
. Missing stored
restriction data never invents a penalty and is reported as "not stored".
Salary Utility implements D-25 as a step function. A boundary belongs to the higher salary-share band: exactly 2%, 8%, 18%, 28%, and 35% score 90, 100, 80, 60, and 40 respectively.
Team mobility is 70% discounted-remaining-salary-weighted average and 30%
Low-Mobility Exposure. The latter sorts by
{mobility, player_id}
, walks the
least-mobile contracts until reaching 25% of guaranteed remaining salary,
and splits the crossing contract pro-rata. Guaranteed dollars are the stored
ContractSeason.guaranteed
amounts; a player option uses at least its full
salary because the player controls whether that commitment remains. Team
options and explicitly non-guaranteed seasons contribute zero. When the team
has no guaranteed remaining salary, Low-Mobility Exposure falls back to the
weighted average.
SHA-256 c9ab97e8eb32b1d3dbd761d5dac54ffc113c86682e97c1c1e02ed82632daa04b
defmodule TradeMachine.GMGradeV2.Pillars.ContractHealth.ContractMobility do
@moduledoc """
Scores how easily the final roster's active contracts can be traded.
Player mobility blends Contract Value marketability (60%), salary utility
(25%), and guaranteed-term utility (15%), then applies every stored trade
restriction multiplicatively. Player options and trade bonuses of at least
5% each apply `0.9`; a no-trade clause applies `0.5`. Missing stored
restriction data never invents a penalty and is reported as "not stored".
Salary Utility implements D-25 as a step function. A boundary belongs to the
higher salary-share band: exactly 2%, 8%, 18%, 28%, and 35% score 90, 100,
80, 60, and 40 respectively.
Team mobility is 70% discounted-remaining-salary-weighted average and 30%
Low-Mobility Exposure. The latter sorts by `{mobility, player_id}`, walks the
least-mobile contracts until reaching 25% of guaranteed remaining salary,
and splits the crossing contract pro-rata. Guaranteed dollars are the stored
`ContractSeason.guaranteed` amounts; a player option uses at least its full
salary because the player controls whether that commitment remains. Team
options and explicitly non-guaranteed seasons contribute zero. When the team
has no guaranteed remaining salary, Low-Mobility Exposure falls back to the
weighted average.
"""
alias TradeMachine.GMGradeV2.ChallengeSnapshot
alias TradeMachine.GMGradeV2.ModelVersion
alias TradeMachine.GMGradeV2.Pillars.ContractHealth.ContractValue
alias TradeMachine.GMGradeV2.Pillars.ContractHealth.Shared
alias TradeMachine.GMGradeV2.Result.MetricResult
@neutral_score 50.0
@supporting_limit 3
@doc "Returns a full-precision Contract Mobility metric result."
@spec score(map(), ChallengeSnapshot.t()) :: MetricResult.t()
def score(scenario_state, %ChallengeSnapshot{} = snapshot) when is_map(scenario_state) do
override = Map.get(scenario_state, :constants_override, %{})
expected_salaries = ChallengeSnapshot.section(snapshot, :expected_salaries)
thresholds = ChallengeSnapshot.section(snapshot, :thresholds)
contracts = Shared.active_contracts(scenario_state)
current_season = current_season(scenario_state, contracts)
salary_cap = salary_cap(thresholds, current_season)
{players, player_fallbacks} =
Enum.map_reduce(contracts, [], fn contract, accumulated ->
stored_contract = stored_contract(scenario_state, contract)
player_id = fetch_any(contract, :player_id)
baseline_score =
ModelVersion.rookie_contract_baseline_for_snapshot(snapshot.data, player_id)
{player, fallbacks} =
evaluate_player(
contract,
stored_contract,
expected_salaries,
current_season,
salary_cap,
override,
baseline_score
)
{player, Enum.reverse(fallbacks, accumulated)}
end)
team_salary = Enum.sum(Enum.map(players, & &1.discounted_remaining_salary))
players = Enum.map(players, &with_salary_weight(&1, team_salary))
{weighted_average, team_fallbacks} =
weighted_average(players, team_salary, salary_cap, current_season)
{low_mobility, quantile_fallbacks} =
case low_mobility_exposure(players) do
{:ok, exposure} ->
{exposure, []}
{:fallback, :zero_guaranteed_salary} ->
{weighted_average,
[
%{
input: :guaranteed_remaining_salary,
method: :weighted_average_low_mobility,
note:
"Guaranteed remaining salary is zero; Low-Mobility Exposure uses the weighted average."
}
]}
end
team_score = clamp(weighted_average * 0.70 + low_mobility * 0.30)
%MetricResult{
key: :contract_mobility,
version: ModelVersion.model_version(),
score: team_score,
inputs: %{
active_contract_count: length(players),
current_season: current_season,
salary_cap: salary_cap,
discounted_remaining_salary: team_salary,
guaranteed_remaining_salary:
Enum.sum(Enum.map(players, & &1.guaranteed_remaining_salary)),
weighted_mobility_average: weighted_average,
low_mobility_exposure: low_mobility
},
supporting: supporting(players),
weights: %{
player: %{marketability: 0.60, salary_utility: 0.25, term_utility: 0.15},
team: %{weighted_average: 0.70, low_mobility_exposure: 0.30},
players: :discounted_remaining_salary_share,
low_mobility_quantile: 0.25
},
fallbacks: Enum.reverse(player_fallbacks) ++ team_fallbacks ++ quantile_fallbacks,
explanation:
"Contract Mobility blends marketability, salary-matching utility, term, and stored " <>
"trade restrictions across #{length(players)} active contracts, including the " <>
"least-mobile 25% of guaranteed remaining salary."
}
end
@doc """
Scores current salary as a share of the current salary cap.
Boundaries enter the higher salary-share band (D-25).
"""
@spec salary_utility(number()) :: float()
def salary_utility(share) when is_number(share) do
cond do
share < 0.02 -> 70.0
share < 0.08 -> 90.0
share < 0.18 -> 100.0
share < 0.28 -> 80.0
share < 0.35 -> 60.0
true -> 40.0
end
end
@doc "Scores the count of remaining guaranteed or player-controlled years."
@spec term_utility(non_neg_integer()) :: float()
def term_utility(years) when is_integer(years) and years <= 1, do: 100.0
def term_utility(2), do: 85.0
def term_utility(3), do: 65.0
def term_utility(4), do: 50.0
def term_utility(years) when is_integer(years) and years >= 5, do: 35.0
@doc """
Returns the guaranteed-salary-weighted mobility of the bottom 25%.
The contract crossing the 25% boundary is included only for the dollars
needed to reach the boundary.
"""
@spec low_mobility_exposure([map()]) ::
{:ok, float()} | {:fallback, :zero_guaranteed_salary}
def low_mobility_exposure(players) when is_list(players) do
total = Enum.sum(Enum.map(players, &guaranteed_salary/1))
if total > 0 do
target = total * 0.25
{weighted_score, _taken} =
players
|> Enum.sort_by(&{fetch_any(&1, :score), fetch_any(&1, :player_id)})
|> Enum.reduce_while({0.0, 0.0}, fn player, {score, taken} ->
available = guaranteed_salary(player)
included = min(available, target - taken)
next = {score + fetch_any(player, :score) * included, taken + included}
if elem(next, 1) >= target, do: {:halt, next}, else: {:cont, next}
end)
{:ok, weighted_score / target}
else
{:fallback, :zero_guaranteed_salary}
end
end
defp evaluate_player(
contract,
stored_contract,
expected_salaries,
current_season,
salary_cap,
override,
baseline_score
) do
value =
ContractValue.player_value(
contract,
expected_salaries,
current_season,
override,
stored_contract,
baseline_score
)
seasons = Shared.remaining_seasons(contract)
current = Enum.find(seasons, &(&1.season == current_season)) || List.first(seasons)
{salary_score, salary_fallbacks} = player_salary_utility(value.player_id, current, salary_cap)
guaranteed_years = Enum.count(seasons, &guaranteed_year?/1)
term_score = term_utility(guaranteed_years)
{modifier, restrictions, restriction_fallbacks} =
restriction_modifier(stored_contract, seasons)
base = value.score * 0.60 + salary_score * 0.25 + term_score * 0.15
player = %{
player_id: value.player_id,
name: value.name,
score: clamp(base * modifier),
marketability: value.score,
salary_utility: salary_score,
term_utility: term_score,
guaranteed_years_remaining: guaranteed_years,
restriction_modifier: modifier,
restrictions: restrictions,
discounted_remaining_salary: value.discounted_remaining_salary,
guaranteed_remaining_salary: guaranteed_remaining_salary(seasons),
salary_weight: 0.0,
valuation_available?: value.valuation_available?
}
fallbacks =
unavailable_value_fallback(value) ++ salary_fallbacks ++ restriction_fallbacks
{player, fallbacks}
end
defp player_salary_utility(_player_id, %{salary: salary}, cap)
when is_number(salary) and is_number(cap) and cap > 0 do
{salary_utility(salary / cap), []}
end
defp player_salary_utility(player_id, _current, _cap) do
{@neutral_score,
[
%{
input: player_id,
method: :neutral_salary_utility,
note: "Current salary or salary cap was unavailable; Salary Utility is neutral."
}
]}
end
defp unavailable_value_fallback(%{valuation_available?: true}), do: []
defp unavailable_value_fallback(player) do
[
%{
input: player.player_id,
method: :neutral_unavailable_expected_salary,
note: "Expected salary was unavailable; Contract Mobility marketability is neutral at 50."
}
]
end
defp restriction_modifier(contract, seasons) do
{no_trade_clause?, no_trade_fallbacks} = no_trade_clause(contract)
{trade_bonus?, trade_bonus_fallbacks} = significant_trade_bonus(contract)
player_option? = Enum.any?(seasons, &player_option?/1)
restrictions =
[]
|> maybe_add_restriction(player_option?, :player_option)
|> maybe_add_restriction(trade_bonus?, :significant_trade_bonus)
|> maybe_add_restriction(no_trade_clause?, :no_trade_clause)
|> Enum.reverse()
modifier =
Enum.reduce(restrictions, 1.0, fn
:no_trade_clause, value -> value * 0.5
_restriction, value -> value * 0.9
end)
{modifier, restrictions, no_trade_fallbacks ++ trade_bonus_fallbacks}
end
defp no_trade_clause(contract) when is_map(contract) do
if has_any_key?(contract, :no_trade_clause) do
{fetch_any(contract, :no_trade_clause) == true, []}
else
{false, [not_stored_fallback(:no_trade_clause)]}
end
end
defp no_trade_clause(_contract), do: {false, [not_stored_fallback(:no_trade_clause)]}
defp significant_trade_bonus(contract) when is_map(contract) do
rows = fetch_any(contract, :seasons) || fetch_any(contract, :entries) || []
stored = Enum.map(rows, &fetch_any(&1, :trade_bonus_pct)) |> Enum.reject(&is_nil/1)
if stored == [] do
{false, [not_stored_fallback(:trade_bonus_pct)]}
else
{Enum.any?(stored, &(decimal_number(&1) >= 5.0)), []}
end
end
defp significant_trade_bonus(_contract),
do: {false, [not_stored_fallback(:trade_bonus_pct)]}
defp not_stored_fallback(field) do
%{
input: field,
method: :restriction_not_stored,
note: "#{field} was not stored; no Contract Mobility restriction was applied."
}
end
defp decimal_number(%Decimal{} = value), do: Decimal.to_float(value)
defp decimal_number(value) when is_number(value), do: value * 1.0
defp decimal_number(_value), do: 0.0
defp guaranteed_year?(season) do
player_option?(season) or
(not team_option?(season) and is_number(season.guaranteed) and season.guaranteed > 0)
end
defp guaranteed_remaining_salary(seasons) do
Enum.reduce(seasons, 0.0, fn season, total ->
guaranteed =
cond do
player_option?(season) -> max(season.guaranteed || 0, season.salary)
team_option?(season) -> 0
is_number(season.guaranteed) -> max(season.guaranteed, 0)
true -> 0
end
total + guaranteed
end)
end
defp player_option?(season) do
normalize_option_type(fetch_any(season, :option_type)) in ["player", "player_option"]
end
defp team_option?(season) do
normalize_option_type(fetch_any(season, :option_type)) in ["team", "team_option"]
end
defp normalize_option_type(nil), do: nil
defp normalize_option_type(value) when is_atom(value), do: Atom.to_string(value)
defp normalize_option_type(value) when is_binary(value), do: String.downcase(value)
defp normalize_option_type(_value), do: nil
defp with_salary_weight(player, team_salary) do
Map.put(
player,
:salary_weight,
Shared.contract_salary_weight(player.discounted_remaining_salary, team_salary)
)
end
defp weighted_average(players, team_salary, salary_cap, current_season) do
cond do
team_salary <= 0 ->
{@neutral_score,
[
%{
input: :active_contracts,
method: :neutral_contract_mobility,
note: "No active remaining salary was available; Contract Mobility is neutral."
}
]}
not (is_number(salary_cap) and salary_cap > 0) ->
{Enum.sum(Enum.map(players, &(&1.score * &1.salary_weight))),
[
%{
input: current_season || :current_season,
method: :salary_cap_not_stored,
note:
"The current salary cap was not stored; player Salary Utility scores are neutral."
}
]}
true ->
{Enum.sum(Enum.map(players, &(&1.score * &1.salary_weight))), []}
end
end
defp supporting(players) do
%{
players: players,
most_movable: ranked(players, :desc),
least_movable: ranked(players, :asc),
salary_below_40:
players
|> Enum.filter(&(&1.score < 40))
|> Enum.sum_by(& &1.discounted_remaining_salary),
useful_mid_sized_contracts: Enum.count(players, &(&1.salary_utility in [90.0, 100.0]))
}
end
defp ranked(players, direction) do
players
|> Enum.sort_by(&{&1.score, &1.player_id}, direction)
|> Enum.take(@supporting_limit)
|> Enum.map(
&Map.take(&1, [
:player_id,
:name,
:score,
:marketability,
:salary_utility,
:term_utility,
:restriction_modifier,
:salary_weight
])
)
end
defp stored_contract(scenario_state, contract) do
player_id = fetch_any(contract, :player_id)
scenario_state
|> fetch_any(:player_rows)
|> List.wrap()
|> Enum.find_value(fn row ->
if row_player_id(row) == player_id, do: fetch_any(row, :contract)
end)
|> case do
nil -> fetch_any(contract, :contract) || contract
stored -> stored
end
end
defp row_player_id(row) do
fetch_any(row, :player_id) ||
row |> fetch_any(:player) |> fetch_any(:id)
end
defp current_season(scenario_state, contracts) do
fetch_any(scenario_state, :season) ||
contracts
|> Enum.flat_map(&Shared.remaining_seasons/1)
|> Enum.map(& &1.season)
|> Enum.min(fn -> nil end)
end
defp salary_cap(thresholds, nil) when is_map(thresholds), do: nil
defp salary_cap(thresholds, season) when is_map(thresholds) do
thresholds |> fetch_any(season) |> fetch_any(:salary_cap)
end
defp salary_cap(_thresholds, _season), do: nil
defp guaranteed_salary(player) do
value = fetch_any(player, :guaranteed_remaining_salary)
if is_number(value) and value > 0, do: value, else: 0
end
defp maybe_add_restriction(restrictions, true, restriction),
do: [restriction | restrictions]
defp maybe_add_restriction(restrictions, _present, _restriction), do: restrictions
defp has_any_key?(map, key) do
Map.has_key?(map, key) or Map.has_key?(map, Atom.to_string(key))
end
defp fetch_any(map, key) when is_map(map) do
case Map.fetch(map, key) do
{:ok, value} -> value
:error -> Map.get(map, to_string(key))
end
end
defp fetch_any(_other, _key), do: nil
defp clamp(value), do: value |> max(0.0) |> min(100.0)
end
contract_optionality.exElixir
lib/trade_machine/gm_grade_v2/pillars/contract_health/contract_optionality.ex
Scores how much future flexibility the final roster's contracts preserve.
Contract status comes exclusively from
TradeMachine.GMGradeV2.ControlProjection
; this metric only maps that shared
evidence to AVC's optionality scores. Each player's future contract seasons
are weighted by salary. An expiring contract with no future season is neutral
at 50, rather than being rewarded for disappearing or penalized for providing
no control.
Team Optionality uses the same discounted remaining-salary player weights as every other Contract Health metric. Current salary therefore determines how much an expiring contract's neutral score matters, while only future seasons determine its player optionality score.
SHA-256 699d4536b5b897ed8fa3beabd4327d73130a4fb55ad1e045b203b04302dfdecc
defmodule TradeMachine.GMGradeV2.Pillars.ContractHealth.ContractOptionality do
@moduledoc """
Scores how much future flexibility the final roster's contracts preserve.
Contract status comes exclusively from
`TradeMachine.GMGradeV2.ControlProjection`; this metric only maps that shared
evidence to AVC's optionality scores. Each player's future contract seasons
are weighted by salary. An expiring contract with no future season is neutral
at 50, rather than being rewarded for disappearing or penalized for providing
no control.
Team Optionality uses the same discounted remaining-salary player weights as
every other Contract Health metric. Current salary therefore determines how
much an expiring contract's neutral score matters, while only future seasons
determine its player optionality score.
"""
alias TradeMachine.GMGradeV2.ChallengeSnapshot
alias TradeMachine.GMGradeV2.ControlProjection
alias TradeMachine.GMGradeV2.ModelVersion
alias TradeMachine.GMGradeV2.Pillars.ContractHealth.Shared
alias TradeMachine.GMGradeV2.Result.MetricResult
@neutral_score 50.0
@supporting_limit 3
@contract_statuses [
:fully_guaranteed,
:team_option,
:non_guaranteed,
:partially_guaranteed,
:player_option
]
@doc "Returns a full-precision Contract Optionality metric result."
@spec score(map(), ChallengeSnapshot.t()) :: MetricResult.t()
def score(scenario_state, %ChallengeSnapshot{}) when is_map(scenario_state) do
override = Map.get(scenario_state, :constants_override, %{})
contracts = Shared.active_contracts(scenario_state)
current_season = current_season(scenario_state, contracts)
modifications = fetch_any(scenario_state, :modifications) || %{}
{players, player_fallbacks} =
Enum.map_reduce(contracts, [], fn contract, fallbacks ->
{player, player_fallbacks} =
evaluate_player(
contract,
Shared.source_player(scenario_state, contract),
current_season,
modifications,
override
)
{player, Enum.reverse(player_fallbacks, fallbacks)}
end)
team_salary = Enum.sum(Enum.map(players, & &1.discounted_remaining_salary))
players = Enum.map(players, &with_salary_weight(&1, team_salary))
{team_score, team_fallbacks} =
if team_salary > 0 do
{Enum.sum(Enum.map(players, &(&1.score * &1.salary_weight))), []}
else
{@neutral_score,
[
%{
input: :active_contracts,
method: :neutral_contract_optionality,
note:
"No active remaining salary was available to weight; Contract Optionality is neutral."
}
]}
end
%MetricResult{
key: :contract_optionality,
version: ModelVersion.model_version(),
score: team_score,
inputs: %{
active_contract_count: length(players),
current_season: current_season,
discounted_remaining_salary: team_salary
},
supporting: supporting(players),
weights: %{
player_seasons: :future_salary_share,
players: :discounted_remaining_salary_share
},
fallbacks: Enum.reverse(player_fallbacks) ++ team_fallbacks,
explanation:
"Contract Optionality scores future team control across #{length(players)} active " <>
"contracts, weighted by future salary within each contract and discounted " <>
"remaining salary across the roster."
}
end
@doc "Maps one `ControlProjection` contract row to AVC's optionality score."
@spec control_score(map()) :: float()
def control_score(%{status: :team_option}), do: 100.0
def control_score(%{status: :non_guaranteed}), do: 95.0
def control_score(%{status: :partially_guaranteed, guaranteed_pct: pct})
when is_number(pct) do
50.0 + 45.0 * (1.0 - clamp(pct, 0.0, 1.0))
end
def control_score(%{status: :fully_guaranteed}), do: 50.0
def control_score(%{status: :player_option}), do: 20.0
defp evaluate_player(contract, source_player, current_season, modifications, override) do
player_id = fetch_any!(contract, :player_id)
seasons = Shared.remaining_seasons(contract)
future_seasons = Enum.filter(seasons, &(is_nil(current_season) or &1.season > current_season))
discounted_salary = Shared.discounted_remaining_salary(seasons, override)
cond do
future_seasons == [] ->
{%{
player_id: player_id,
name: fetch_any(contract, :name),
score: @neutral_score,
discounted_remaining_salary: discounted_salary,
salary_weight: 0.0,
expiring?: true,
seasons: []
}, []}
is_nil(source_player) ->
{neutral_player(contract, future_seasons, discounted_salary),
[missing_projection_fallback(player_id)]}
true ->
labels = Enum.map(future_seasons, & &1.season)
{control_player, control_modifications} =
Shared.control_projection_input(source_player, contract, modifications)
controls =
ControlProjection.project(control_player, labels, modifications: control_modifications)
case evaluated_seasons(future_seasons, controls) do
{:ok, evaluated} ->
{%{
player_id: player_id,
name: fetch_any(contract, :name),
score: salary_weighted_score(evaluated),
discounted_remaining_salary: discounted_salary,
salary_weight: 0.0,
expiring?: false,
seasons: evaluated
}, []}
:unavailable ->
{neutral_player(contract, future_seasons, discounted_salary),
[missing_projection_fallback(player_id)]}
end
end
end
defp evaluated_seasons(future_seasons, controls) do
Enum.reduce_while(future_seasons, {:ok, []}, fn season, {:ok, evaluated} ->
control = Map.get(controls, season.season)
if is_map(control) and fetch_any(control, :status) in @contract_statuses do
row = %{
season: season.season,
salary: season.salary,
status: fetch_any(control, :status),
guaranteed_pct: fetch_any(control, :guaranteed_pct),
score: control_score(control)
}
{:cont, {:ok, [row | evaluated]}}
else
{:halt, :unavailable}
end
end)
|> case do
{:ok, evaluated} -> {:ok, Enum.reverse(evaluated)}
:unavailable -> :unavailable
end
end
defp salary_weighted_score(seasons) do
total_salary = Enum.sum(Enum.map(seasons, & &1.salary))
Enum.sum(Enum.map(seasons, &(&1.score * &1.salary))) / total_salary
end
defp neutral_player(contract, future_seasons, discounted_salary) do
%{
player_id: fetch_any!(contract, :player_id),
name: fetch_any(contract, :name),
score: @neutral_score,
discounted_remaining_salary: discounted_salary,
salary_weight: 0.0,
expiring?: false,
seasons:
Enum.map(future_seasons, fn season ->
%{
season: season.season,
salary: season.salary,
status: :unavailable,
guaranteed_pct: season.guaranteed_pct,
score: @neutral_score
}
end)
}
end
defp missing_projection_fallback(player_id) do
%{
input: player_id,
method: :neutral_unavailable_control_projection,
note:
"Future contract control could not be matched to the shared Control Projection; " <>
"Contract Optionality is neutral at 50."
}
end
defp with_salary_weight(player, team_salary) do
Map.put(
player,
:salary_weight,
Shared.contract_salary_weight(player.discounted_remaining_salary, team_salary)
)
end
defp supporting(players) do
%{
players: players,
most_optional: ranked(players, :desc),
least_optional: ranked(players, :asc),
expiring_contract_count: Enum.count(players, & &1.expiring?)
}
end
defp ranked(players, direction) do
players
|> Enum.sort_by(&{&1.score, &1.player_id}, direction)
|> Enum.take(@supporting_limit)
|> Enum.map(&Map.take(&1, [:player_id, :name, :score, :salary_weight, :expiring?]))
end
defp current_season(scenario_state, contracts) do
fetch_any(scenario_state, :season) ||
contracts
|> Enum.flat_map(&Shared.remaining_seasons/1)
|> Enum.map(& &1.season)
|> Enum.min(fn -> nil end)
end
defp fetch_any!(map, key) do
case fetch_any(map, key) do
nil -> raise KeyError, key: key, term: map
value -> value
end
end
defp fetch_any(map, key) when is_map(map) do
case Map.fetch(map, key) do
{:ok, value} -> value
:error -> Map.get(map, to_string(key))
end
end
defp fetch_any(_other, _key), do: nil
defp clamp(value, low, high), do: value |> max(low) |> min(high)
end
contract_value.exElixir
lib/trade_machine/gm_grade_v2/pillars/contract_health/contract_value.ex
Scores the value of the active contracts on a scenario's final roster.
Each contract season compares actual salary with the confidence-adjusted expected salary frozen in the challenge snapshot:
surplus = adjusted_expected - actual
ratio = surplus / max(adjusted_expected, actual)
score = 50 + 50 * clamp(ratio, -1, 1)
The formula is intentionally authoritative over the worked examples in AVC's
source document (D-05). A player's season scores use the versioned
ModelVersion.contract_season_weights/0
; the team then weights player scores
by each contract's share of discounted remaining salary.
A valuation must cover every remaining contract season. If any season is unavailable, the whole player receives 50 and a fallback is recorded. This avoids silently grading only the convenient portion of a contract. Surplus and deficit totals omit neutralized players because no projected dollar difference exists for them.
Active standard rookie-scale seasons with frozen first-round provenance use the snapshot's draft-prior/market expected salary. When that provenance was unavailable at snapshot creation, the versioned structural fallback applies without inventing expected salary, surplus, or deficit. Extension seasons and non-rookie contracts remain on the ordinary expected-salary path.
score/2
reads scenario-adjusted rows from
scenario_state.payroll_projection
.
Tests and other pure callers may instead provide a
:contracts
list with the
same player-row shape. Dead money, cap holds, declined options, and unsigned
rights are excluded by
Shared.remaining_seasons/1
; off-roster rows are
excluded here.
SHA-256 773ea39d980f661d1c2d7a5b174efef2adc9979222e4cbab54b94bdea0c5bbff
defmodule TradeMachine.GMGradeV2.Pillars.ContractHealth.ContractValue do
@moduledoc """
Scores the value of the active contracts on a scenario's final roster.
Each contract season compares actual salary with the confidence-adjusted
expected salary frozen in the challenge snapshot:
surplus = adjusted_expected - actual
ratio = surplus / max(adjusted_expected, actual)
score = 50 + 50 * clamp(ratio, -1, 1)
The formula is intentionally authoritative over the worked examples in AVC's
source document (D-05). A player's season scores use the versioned
`ModelVersion.contract_season_weights/0`; the team then weights player scores
by each contract's share of discounted remaining salary.
A valuation must cover every remaining contract season. If any season is
unavailable, the whole player receives 50 and a fallback is recorded. This
avoids silently grading only the convenient portion of a contract. Surplus
and deficit totals omit neutralized players because no projected dollar
difference exists for them.
Active standard rookie-scale seasons with frozen first-round provenance use
the snapshot's draft-prior/market expected salary. When that provenance was
unavailable at snapshot creation, the versioned structural fallback applies
without inventing expected salary, surplus, or deficit. Extension seasons and
non-rookie contracts remain on the ordinary expected-salary path.
`score/2` reads scenario-adjusted rows from `scenario_state.payroll_projection`.
Tests and other pure callers may instead provide a `:contracts` list with the
same player-row shape. Dead money, cap holds, declined options, and unsigned
rights are excluded by `Shared.remaining_seasons/1`; off-roster rows are
excluded here.
"""
alias TradeMachine.GMGradeV2.ChallengeSnapshot
alias TradeMachine.GMGradeV2.ExpectedSalary
alias TradeMachine.GMGradeV2.ModelVersion
alias TradeMachine.GMGradeV2.Pillars.ContractHealth.Shared
alias TradeMachine.GMGradeV2.Result.MetricResult
alias TradeMachine.Valuation.ContractClassification
@neutral_score 50.0
@supporting_limit 3
@typedoc "One player's fully evaluated Contract Value contribution."
@type player_value :: %{
player_id: String.t(),
name: String.t() | nil,
score: float(),
discounted_remaining_salary: float(),
salary_weight: float(),
projected_surplus: number(),
projected_deficit: number(),
seasons: [map()],
valuation_available?: boolean(),
valuation_method: atom(),
contract_classifications: [map()]
}
@doc "Returns a full-precision Contract Value metric result."
@spec score(map(), ChallengeSnapshot.t()) :: MetricResult.t()
def score(scenario_state, %ChallengeSnapshot{} = snapshot) when is_map(scenario_state) do
override = Map.get(scenario_state, :constants_override, %{})
contracts = Shared.active_contracts(scenario_state)
expected_salaries = ChallengeSnapshot.section(snapshot, :expected_salaries)
current_season = current_season(scenario_state, contracts)
policy_baseline = rookie_policy_baseline(snapshot)
evaluated =
Enum.map(contracts, fn contract ->
classification_subject = Shared.source_player(scenario_state, contract) || contract
player_id = fetch_any!(contract, :player_id)
baseline_score =
ModelVersion.rookie_contract_baseline_for_snapshot(snapshot.data, player_id)
player_value(
contract,
expected_salaries,
current_season,
override,
classification_subject,
baseline_score
)
end)
team_salary = Enum.sum(Enum.map(evaluated, & &1.discounted_remaining_salary))
players = Enum.map(evaluated, &with_salary_weight(&1, team_salary))
fallbacks = player_fallbacks(players)
{team_score, fallbacks} =
if team_salary > 0 do
{Enum.reduce(players, 0.0, &(&2 + &1.score * &1.salary_weight)), fallbacks}
else
{@neutral_score,
fallbacks ++
[
%{
input: :active_contracts,
method: :neutral_contract_value,
note:
"No active remaining salary was available to weight; Contract Value is neutral."
}
]}
end
%MetricResult{
key: :contract_value,
version: ModelVersion.model_version(),
score: team_score,
inputs: %{
active_contract_count: length(players),
discounted_remaining_salary: team_salary
},
supporting: supporting(players),
weights: %{
contract_seasons: ModelVersion.contract_season_weights(override),
rookie_scale_fallback_baseline: policy_baseline,
players: :discounted_remaining_salary_share
},
fallbacks: fallbacks,
explanation:
"Contract Value compares confidence-adjusted market salary with actual salary " <>
"across #{length(players)} active contracts, weighted by discounted remaining salary."
}
end
@doc """
Evaluates one projection/contract row against frozen expected salaries.
The returned map deliberately remains public: Contract Mobility reuses its
`:score` as AVC's marketability component instead of reimplementing Contract
Value.
"""
@spec player_value(map(), map()) :: player_value()
def player_value(contract, expected_salaries, override \\ %{})
when is_map(contract) and is_map(expected_salaries) do
current_season =
contract
|> Shared.remaining_seasons()
|> List.first()
|> case do
%{season: season} -> season
_other -> nil
end
classification_subject = fetch_any(contract, :player) || contract
player_value(
contract,
expected_salaries,
current_season,
override,
classification_subject,
current_policy_baseline(classification_subject, override)
)
end
@doc false
@spec player_value(map(), map(), String.t() | nil, map(), map(), number() | nil) ::
player_value()
def player_value(
contract,
expected_salaries,
current_season,
override,
classification_subject,
baseline_score
)
when is_map(contract) and is_map(expected_salaries) and is_map(classification_subject) do
player_id = fetch_any!(contract, :player_id)
seasons = Shared.remaining_seasons(contract)
discounted_salary = Shared.discounted_remaining_salary(seasons, override)
case evaluated_seasons(
seasons,
expected_salaries,
player_id,
classification_subject,
baseline_score
) do
{:ok, evaluated} ->
%{
player_id: player_id,
name: fetch_any(contract, :name),
score: weighted_player_score(evaluated, override),
discounted_remaining_salary: discounted_salary,
salary_weight: 0.0,
projected_surplus: sum_positive_surplus(evaluated),
projected_deficit: sum_deficit(evaluated),
seasons: evaluated,
valuation_available?: true,
valuation_method: valuation_method(evaluated),
contract_classifications: contract_classifications(evaluated)
}
:unavailable ->
classifications =
classify_seasons(seasons, classification_subject, current_season)
%{
player_id: player_id,
name: fetch_any(contract, :name),
score: @neutral_score,
discounted_remaining_salary: discounted_salary,
salary_weight: 0.0,
projected_surplus: 0,
projected_deficit: 0,
seasons: neutral_seasons(seasons, classifications),
valuation_available?: false,
valuation_method: :neutral_unavailable_expected_salary,
contract_classifications: classifications
}
end
end
@doc "Applies AVC's bounded per-season Contract Value formula."
@spec season_score(number(), number()) :: float()
def season_score(actual_salary, adjusted_expected)
when is_number(actual_salary) and actual_salary >= 0 and
is_number(adjusted_expected) and adjusted_expected >= 0 do
denominator = max(actual_salary, adjusted_expected)
ratio =
if denominator == 0 do
0.0
else
(adjusted_expected - actual_salary) / denominator
end
50.0 + 50.0 * clamp(ratio, -1.0, 1.0)
end
defp evaluated_seasons(
seasons,
expected_salaries,
player_id,
classification_subject,
baseline_score
) do
seasons
|> Enum.reduce_while({:ok, []}, fn season, {:ok, evaluated} ->
classification = ContractClassification.classify(classification_subject, season.season)
if is_number(baseline_score) and classification.active_standard_rookie_scale? do
value = %{
season: season.season,
salary: season.salary,
expected: nil,
confidence: nil,
adjusted_expected: nil,
surplus: 0,
score: baseline_score,
valuation_method: :rookie_scale_baseline,
contract_classification: classification
}
{:cont, {:ok, [value | evaluated]}}
else
case ExpectedSalary.season_expectation(expected_salaries, player_id, season.season) do
{:ok, %{expected: expected, confidence: confidence}} ->
adjusted =
Shared.confidence_adjusted_expected(season.salary, expected, confidence)
value = %{
season: season.season,
salary: season.salary,
expected: expected,
confidence: confidence,
adjusted_expected: adjusted,
surplus: adjusted - season.salary,
score: season_score(season.salary, adjusted),
valuation_method: :expected_salary,
contract_classification: classification
}
{:cont, {:ok, [value | evaluated]}}
{:error, :unavailable} ->
{:halt, :unavailable}
end
end
end)
|> case do
{:ok, evaluated} -> {:ok, Enum.reverse(evaluated)}
:unavailable -> :unavailable
end
end
defp weighted_player_score([], _override), do: @neutral_score
defp weighted_player_score(seasons, override) do
weights = ModelVersion.contract_season_weights(override)
final_weight = List.last(weights) || 0.0
{weighted_score, total_weight} =
seasons
|> Enum.with_index()
|> Enum.reduce({0.0, 0.0}, fn {season, index}, {score, total} ->
weight = Enum.at(weights, index, final_weight)
{score + season.score * weight, total + weight}
end)
if total_weight > 0, do: weighted_score / total_weight, else: @neutral_score
end
defp neutral_seasons(seasons, classifications) do
Enum.map(seasons, fn season ->
classification = Enum.find(classifications, &(&1.season == season.season))
%{
season: season.season,
salary: season.salary,
expected: nil,
confidence: nil,
adjusted_expected: nil,
surplus: 0,
score: @neutral_score,
valuation_method: :neutral_unavailable_expected_salary,
contract_classification: classification && classification.result
}
end)
end
defp with_salary_weight(player, team_salary) do
Map.put(
player,
:salary_weight,
Shared.contract_salary_weight(player.discounted_remaining_salary, team_salary)
)
end
defp supporting(players) do
%{
players: players,
best_contracts: ranked(players, :desc),
worst_contracts: ranked(players, :asc),
total_projected_surplus: Enum.sum(Enum.map(players, & &1.projected_surplus)),
total_projected_deficit: Enum.sum(Enum.map(players, & &1.projected_deficit))
}
end
defp ranked(players, direction) do
players
|> Enum.sort_by(&{&1.score, &1.player_id}, direction)
|> Enum.take(@supporting_limit)
|> Enum.map(&Map.take(&1, [:player_id, :name, :score, :salary_weight]))
end
defp player_fallbacks(players) do
Enum.flat_map(players, fn player ->
baseline_seasons =
Enum.filter(player.seasons, &(&1.valuation_method == :rookie_scale_baseline))
rookie =
if player.valuation_available? and baseline_seasons != [] do
baseline_seasons
|> Enum.group_by(& &1.contract_classification.method)
|> Enum.map(fn {method, classifications} ->
baseline_score = hd(classifications).score
%{
input: player.player_id,
method: :rookie_scale_baseline,
classification_method: method,
classification: :active_standard_rookie_scale,
baseline_score: baseline_score,
seasons: Enum.map(classifications, & &1.season),
note:
"Active standard rookie-scale seasons use the strong #{baseline_score}-point " <>
"structural baseline; missing production does not create projected overpayment."
}
end)
else
[]
end
unavailable =
if player.valuation_available? do
[]
else
[
%{
input: player.player_id,
method: :neutral_unavailable_expected_salary,
note:
"Expected salary was unavailable for at least one remaining non-rookie season; " <>
"the player's Contract Value is neutral at 50."
}
]
end
rookie ++ unavailable
end)
end
defp valuation_method(seasons) do
case seasons |> Enum.map(& &1.valuation_method) |> Enum.uniq() do
[:rookie_scale_baseline] -> :rookie_scale_baseline
[:expected_salary] -> :expected_salary
_mixed -> :expected_salary_with_rookie_scale_baseline
end
end
defp contract_classifications(seasons) do
Enum.map(seasons, fn season ->
%{season: season.season, result: season.contract_classification}
end)
end
defp classify_seasons(seasons, subject, current_season) do
labels =
case seasons do
[] when is_binary(current_season) -> [current_season]
_other -> Enum.map(seasons, & &1.season)
end
Enum.map(labels, fn season ->
%{season: season, result: ContractClassification.classify(subject, season)}
end)
end
defp current_season(scenario_state, contracts) do
fetch_any(scenario_state, :season) ||
contracts
|> Enum.flat_map(&Shared.remaining_seasons/1)
|> Enum.map(& &1.season)
|> Enum.min(fn -> nil end)
end
defp rookie_policy_baseline(snapshot) do
policy = ModelVersion.rookie_contract_policy_for_snapshot(snapshot.data)
fetch_any(policy || %{}, :baseline) || fetch_any(policy || %{}, :fallback_baseline)
end
defp current_policy_baseline(subject, override) do
policy = ModelVersion.rookie_contract_policy(override)
case TradeMachine.Valuation.RookieContractPolicy.draft_prior(subject, policy) do
:error -> policy.fallback_baseline
{:ok, _prior} -> nil
end
end
defp sum_positive_surplus(seasons) do
seasons
|> Enum.map(& &1.surplus)
|> Enum.filter(&(&1 > 0))
|> Enum.sum()
end
defp sum_deficit(seasons) do
seasons
|> Enum.map(& &1.surplus)
|> Enum.filter(&(&1 < 0))
|> Enum.reduce(0, &(&2 - &1))
end
defp clamp(value, minimum, maximum), do: value |> max(minimum) |> min(maximum)
defp fetch_any!(map, key) do
case fetch_any(map, key) do
nil -> raise KeyError, key: key, term: map
value -> value
end
end
defp fetch_any(map, key) when is_map(map) do
case Map.fetch(map, key) do
{:ok, value} -> value
:error -> Map.get(map, Atom.to_string(key))
end
end
end
long_term_risk.exElixir
lib/trade_machine/gm_grade_v2/pillars/contract_health/long_term_risk.ex
Scores dangerous future overpayments on the final roster's active contracts.
The current season is excluded. The evaluated horizon is the next four
seasons in the frozen projection, matching
ModelVersion.risk_season_weights/0
; all projected caps in that horizon stay
in the denominator even when the roster has no salary in one of them. This
keeps the denominator from changing with contract length.
A future season's exposure is:
max(0, actual - confidence_adjusted_expected)
* guarantee_exposure_factor
* future_season_weight
Player options are fully exposed, team options and explicitly non-guaranteed seasons have no exposure, and partial guarantees use their stored guarantee percentage. Expected-salary projections already include AVC's age curve, so this metric applies no second age penalty.
Active standard rookie-scale seasons instead receive the versioned strong structural baseline and contribute no projected overpayment. The shared classifier runs per season, so extension seasons and every non-rookie deal continue through the expected-salary and guarantee-exposure path.
Salary caps come from the challenge snapshot's frozen
thresholds
section.
For snapshots created before the full horizon was frozen, missing future rows
fall back to
CapState
's deterministic projection derived from that same
frozen current-season row. Rows marked
estimated
are used as-is, with a
visible fallback record.
SHA-256 bf3b7901c844316826ea2bb49e29e11e70772473b9fe172b10e42dbfd07edb18
defmodule TradeMachine.GMGradeV2.Pillars.ContractHealth.LongTermRisk do
@moduledoc """
Scores dangerous future overpayments on the final roster's active contracts.
The current season is excluded. The evaluated horizon is the next four
seasons in the frozen projection, matching
`ModelVersion.risk_season_weights/0`; all projected caps in that horizon stay
in the denominator even when the roster has no salary in one of them. This
keeps the denominator from changing with contract length.
A future season's exposure is:
max(0, actual - confidence_adjusted_expected)
* guarantee_exposure_factor
* future_season_weight
Player options are fully exposed, team options and explicitly
non-guaranteed seasons have no exposure, and partial guarantees use their
stored guarantee percentage. Expected-salary projections already include
AVC's age curve, so this metric applies no second age penalty.
Active standard rookie-scale seasons instead receive the versioned strong
structural baseline and contribute no projected overpayment. The shared
classifier runs per season, so extension seasons and every non-rookie deal
continue through the expected-salary and guarantee-exposure path.
Salary caps come from the challenge snapshot's frozen `thresholds` section.
For snapshots created before the full horizon was frozen, missing future rows
fall back to `CapState`'s deterministic projection derived from that same
frozen current-season row. Rows marked `estimated` are used as-is, with a
visible fallback record.
"""
alias TradeMachine.GMGradeV2.ChallengeSnapshot
alias TradeMachine.GMGradeV2.ExpectedSalary
alias TradeMachine.GMGradeV2.ModelVersion
alias TradeMachine.GMGradeV2.Pillars.ContractHealth.Shared
alias TradeMachine.GMGradeV2.Result.MetricResult
alias TradeMachine.Valuation.ContractClassification
@neutral_player_score 50.0
@no_risk_score 100.0
@supporting_limit 3
@curve [
{0.00, 100.0},
{0.05, 85.0},
{0.10, 70.0},
{0.15, 50.0},
{0.20, 30.0},
{0.25, 0.0}
]
@doc "Returns a full-precision Long-Term Risk metric result."
@spec score(map(), ChallengeSnapshot.t()) :: MetricResult.t()
def score(scenario_state, %ChallengeSnapshot{} = snapshot) when is_map(scenario_state) do
override = Map.get(scenario_state, :constants_override, %{})
expected_salaries = ChallengeSnapshot.section(snapshot, :expected_salaries)
thresholds = effective_thresholds(scenario_state, snapshot)
horizon = future_horizon(scenario_state, thresholds, override)
contracts = Shared.active_contracts(scenario_state)
policy_baseline = rookie_policy_baseline(snapshot)
{players, player_fallbacks} =
contracts
|> Enum.map(fn contract ->
classification_subject = Shared.source_player(scenario_state, contract) || contract
player_id = fetch_any!(contract, :player_id)
baseline_score =
ModelVersion.rookie_contract_baseline_for_snapshot(snapshot.data, player_id)
evaluate_player(
contract,
classification_subject,
horizon,
expected_salaries,
baseline_score
)
end)
|> Enum.map_reduce([], fn {player, fallbacks}, accumulated ->
{player, Enum.reverse(fallbacks, accumulated)}
end)
cap_rows = Enum.map(horizon, &cap_row!(&1, thresholds))
denominator = Enum.sum(Enum.map(cap_rows, & &1.salary_cap))
at_risk_dollars = Enum.sum(Enum.map(players, & &1.exposure))
burden = if denominator > 0, do: at_risk_dollars / denominator, else: 0.0
no_future_seasons? = horizon == []
fallbacks =
player_fallbacks
|> Enum.reverse()
|> Kernel.++(estimated_cap_fallbacks(cap_rows))
|> maybe_add_empty_horizon_fallback(no_future_seasons?)
%MetricResult{
key: :long_term_risk,
version: ModelVersion.model_version(),
score: if(no_future_seasons?, do: @no_risk_score, else: burden_score(burden)),
inputs: %{
future_risk_burden: burden,
projected_cap_denominator: denominator,
evaluated_seasons: Enum.map(horizon, & &1.season),
active_contract_count: length(players)
},
supporting: supporting(players, horizon),
weights: %{
future_seasons: ModelVersion.risk_season_weights(override),
rookie_scale_fallback_baseline: policy_baseline
},
fallbacks: fallbacks,
explanation: explanation(no_future_seasons?, at_risk_dollars, length(horizon))
}
end
@doc "Maps AVC's Future Risk Burden curve to a 0–100 score."
@spec burden_score(number()) :: float()
def burden_score(burden) when is_number(burden) and burden <= 0, do: @no_risk_score
def burden_score(burden) when is_number(burden) and burden >= 0.25, do: 0.0
def burden_score(burden) when is_number(burden) do
{{left_burden, left_score}, {right_burden, right_score}} =
@curve
|> Enum.chunk_every(2, 1, :discard)
|> Enum.find(fn [{left, _}, {right, _}] ->
burden >= left and burden <= right
end)
|> List.to_tuple()
position = (burden - left_burden) / (right_burden - left_burden)
left_score + position * (right_score - left_score)
end
@doc "Returns AVC's downside-exposure factor for one normalized contract season."
@spec exposure_factor(map()) :: float() | nil
def exposure_factor(season) when is_map(season) do
option_type = season |> fetch_any(:option_type) |> normalize_option_type()
guaranteed_pct = fetch_any(season, :guaranteed_pct)
cond do
option_type in ["player", "player_option"] -> 1.0
option_type in ["team", "team_option"] -> 0.0
is_number(guaranteed_pct) -> guaranteed_pct |> max(0.0) |> min(1.0)
true -> nil
end
end
defp future_horizon(scenario_state, thresholds, override) do
weights = ModelVersion.risk_season_weights(override)
current_season = fetch_any(scenario_state, :season)
scenario_state
|> projected_seasons(thresholds)
|> Enum.filter(&(is_nil(current_season) or &1 > current_season))
|> Enum.take(length(weights))
|> Enum.with_index()
|> Enum.map(fn {season, index} ->
%{season: season, weight: Enum.at(weights, index)}
end)
end
defp effective_thresholds(scenario_state, snapshot) do
frozen = ChallengeSnapshot.section(snapshot, :thresholds)
projected =
case fetch_any(scenario_state, :cap_state) do
cap_state when is_map(cap_state) ->
case fetch_any(cap_state, :thresholds) do
thresholds when is_map(thresholds) -> thresholds
_ -> %{}
end
_ ->
%{}
end
# A stored row is authoritative. The derived horizon fills only labels that
# older snapshots omitted.
Map.merge(projected, frozen)
end
defp projected_seasons(scenario_state, thresholds) do
seasons =
fetch_any(scenario_state, :seasons) ||
scenario_state |> fetch_any(:payroll_projection) |> fetch_any(:seasons) ||
Map.keys(thresholds)
seasons
|> Enum.filter(&is_binary/1)
|> Enum.uniq()
|> Enum.sort()
end
defp evaluate_player(
contract,
classification_subject,
horizon,
expected_salaries,
baseline_score
) do
player_id = fetch_any!(contract, :player_id)
seasons_by_label = Shared.remaining_seasons(contract) |> Map.new(&{&1.season, &1})
{seasons, fallback_reasons} =
Enum.map_reduce(horizon, [], fn future, fallback_reasons ->
case Map.get(seasons_by_label, future.season) do
nil ->
{nil, fallback_reasons}
season ->
classification =
ContractClassification.classify(classification_subject, season.season)
case evaluate_season(
season,
future,
expected_salaries,
player_id,
classification,
baseline_score
) do
{:ok, evaluated} ->
{evaluated, fallback_reasons}
{:unavailable, reason} ->
{neutral_season(season, future), [reason | fallback_reasons]}
end
end
end)
seasons = Enum.reject(seasons, &is_nil/1)
unavailable? = fallback_reasons != []
classifications = contract_classifications(seasons)
rookie_baseline? = Enum.any?(seasons, &(&1.valuation_method == :rookie_scale_baseline))
player = %{
player_id: player_id,
name: fetch_any(contract, :name),
score:
cond do
unavailable? -> @neutral_player_score
rookie_baseline? -> baseline_score
true -> nil
end,
exposure: Enum.sum(Enum.map(seasons, & &1.exposure)),
projected_overpayment: Enum.sum(Enum.map(seasons, & &1.projected_overpayment)),
valuation_available?: not unavailable?,
valuation_method: if(rookie_baseline?, do: :rookie_scale_baseline, else: :expected_salary),
contract_classifications: classifications,
seasons: seasons
}
fallbacks =
fallback_reasons
|> Enum.uniq()
|> Enum.map(fn
:expected_salary ->
%{
input: player_id,
method: :neutral_unavailable_expected_salary,
note:
"Expected salary was unavailable for at least one future contract season; " <>
"that season contributes no invented downside exposure and the player is recorded neutral at 50."
}
:guarantee ->
%{
input: player_id,
method: :neutral_unknown_guarantee,
note:
"Guarantee data was missing for at least one future contract season; " <>
"that season contributes no invented downside exposure and the player is recorded neutral at 50."
}
end)
|> Kernel.++(rookie_baseline_fallbacks(player_id, classifications, baseline_score))
{player, fallbacks}
end
defp evaluate_season(
season,
future,
expected_salaries,
player_id,
classification,
baseline_score
) do
if is_number(baseline_score) and classification.active_standard_rookie_scale? do
{:ok,
%{
season: season.season,
salary: season.salary,
adjusted_expected: nil,
projected_overpayment: 0,
exposure_factor: exposure_factor(season),
future_weight: future.weight,
exposure: 0,
score: baseline_score,
valuation_method: :rookie_scale_baseline,
contract_classification: classification
}}
else
with factor when is_number(factor) <- exposure_factor(season),
{:ok, %{expected: expected, confidence: confidence}} <-
ExpectedSalary.season_expectation(expected_salaries, player_id, season.season) do
adjusted = Shared.confidence_adjusted_expected(season.salary, expected, confidence)
overpayment = max(0, season.salary - adjusted)
{:ok,
%{
season: season.season,
salary: season.salary,
adjusted_expected: adjusted,
projected_overpayment: overpayment,
exposure_factor: factor,
future_weight: future.weight,
exposure: overpayment * factor * future.weight,
score: nil,
valuation_method: :expected_salary,
contract_classification: classification
}}
else
nil ->
{:unavailable, :guarantee}
{:error, :unavailable} ->
{:unavailable, :expected_salary}
end
end
end
defp neutral_season(season, future) do
%{
season: season.season,
salary: season.salary,
adjusted_expected: nil,
projected_overpayment: 0,
exposure_factor: exposure_factor(season),
future_weight: future.weight,
exposure: 0,
score: @neutral_player_score,
valuation_method: :neutral_unavailable_input,
contract_classification: nil
}
end
defp contract_classifications(seasons) do
for season <- seasons, not is_nil(season.contract_classification) do
%{season: season.season, result: season.contract_classification}
end
end
defp rookie_baseline_fallbacks(player_id, classifications, baseline_score) do
if is_number(baseline_score) do
classifications
|> Enum.filter(& &1.result.active_standard_rookie_scale?)
|> Enum.group_by(& &1.result.method)
|> Enum.map(fn {method, classified} ->
%{
input: player_id,
method: :rookie_scale_baseline,
classification_method: method,
classification: :active_standard_rookie_scale,
baseline_score: baseline_score,
seasons: Enum.map(classified, & &1.season),
note:
"Active standard rookie-scale seasons use the strong #{baseline_score}-point " <>
"structural baseline and contribute no projected overpayment."
}
end)
else
[]
end
end
defp rookie_policy_baseline(snapshot) do
policy = ModelVersion.rookie_contract_policy_for_snapshot(snapshot.data)
fetch_any(policy || %{}, :baseline) || fetch_any(policy || %{}, :fallback_baseline)
end
defp cap_row!(future, thresholds) do
row = Map.get(thresholds, future.season)
salary_cap = fetch_any(row, :salary_cap)
unless is_number(salary_cap) and salary_cap > 0 do
raise ArgumentError,
"frozen snapshot has no positive salary cap for future season #{future.season}"
end
%{
season: future.season,
salary_cap: salary_cap,
source: fetch_any(row, :source)
}
end
defp estimated_cap_fallbacks(cap_rows) do
for row <- cap_rows, normalize_source(row.source) == "estimated" do
%{
input: row.season,
method: :snapshot_escalated_salary_cap,
note:
"No official threshold row was available; Long-Term Risk used the frozen cap snapshot's recorded escalation fallback."
}
end
end
defp maybe_add_empty_horizon_fallback(fallbacks, false), do: fallbacks
defp maybe_add_empty_horizon_fallback(fallbacks, true) do
fallbacks ++
[
%{
input: :future_seasons,
method: :no_future_risk,
note: "The frozen projection contains no future seasons; Long-Term Risk is 100."
}
]
end
defp supporting(players, horizon) do
season_exposure =
Map.new(horizon, fn future ->
exposure =
players
|> Enum.flat_map(& &1.seasons)
|> Enum.filter(&(&1.season == future.season))
|> Enum.map(& &1.exposure)
|> Enum.sum()
{future.season, exposure}
end)
%{
at_risk_dollars: Enum.sum(Map.values(season_exposure)),
riskiest_season: riskiest_season(season_exposure),
top_exposure_players: top_exposure_players(players),
seasons: season_exposure,
players: players
}
end
defp riskiest_season(season_exposure) when map_size(season_exposure) == 0, do: nil
defp riskiest_season(season_exposure) do
{season, exposure} =
season_exposure
|> Enum.sort_by(fn {season, exposure} -> {-exposure, season} end)
|> hd()
%{season: season, exposure: exposure}
end
defp top_exposure_players(players) do
players
|> Enum.filter(&(&1.exposure > 0))
|> Enum.sort_by(&{-&1.exposure, &1.player_id})
|> Enum.take(@supporting_limit)
|> Enum.map(&Map.take(&1, [:player_id, :name, :exposure, :projected_overpayment]))
end
defp explanation(true, _at_risk_dollars, _season_count) do
"The frozen projection contains no future seasons, so there is no long-term contract risk."
end
defp explanation(false, at_risk_dollars, season_count) do
"Long-Term Risk measures #{at_risk_dollars} weighted dollars of projected " <>
"overpayment across #{season_count} frozen future salary-cap seasons."
end
defp normalize_option_type(value) when is_atom(value), do: Atom.to_string(value)
defp normalize_option_type(value) when is_binary(value), do: value
defp normalize_option_type(_value), do: nil
defp normalize_source(value) when is_atom(value), do: Atom.to_string(value)
defp normalize_source(value) when is_binary(value), do: value
defp normalize_source(_value), do: nil
defp fetch_any!(map, key) do
case fetch_any(map, key) do
nil -> raise KeyError, key: key, term: map
value -> value
end
end
defp fetch_any(map, key) when is_map(map) do
case Map.fetch(map, key) do
{:ok, value} -> value
:error -> Map.get(map, Atom.to_string(key))
end
end
defp fetch_any(_other, _key), do: nil
end