Cap & flexibility
Current cap position, future flexibility, tax/apron exposure, and transaction flexibility use the cap engine's financial state and scoring curves.
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 |
|---|---|
| Current cap position | 20.0% |
| Tax apron exposure | 30.0% |
| Future flexibility | 30.0% |
| Transaction flexibility | 20.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.
cap_flexibility.exElixir
lib/trade_machine/gm_grade_v2/pillars/cap_flexibility.ex
Aggregates AVC's four Cap & Flexibility metrics from a
CapState
snapshot.
Current Cap Position, Tax & Apron Exposure, Future Flexibility, and Transaction Flexibility retain full precision through the fixed 20/30/30/20 weighted sum.
SHA-256 6bf1f63648b73e4994ca2ed491fe63f40c9436e1a364d42f99c5b3bd2c1f114d
defmodule TradeMachine.GMGradeV2.Pillars.CapFlexibility do
@moduledoc """
Aggregates AVC's four Cap & Flexibility metrics from a `CapState` snapshot.
Current Cap Position, Tax & Apron Exposure, Future Flexibility, and
Transaction Flexibility retain full precision through the fixed 20/30/30/20
weighted sum.
"""
alias TradeMachine.GMGradeV2.ModelVersion
alias TradeMachine.GMGradeV2.Pillars.CapFlexibility.CurrentCapPosition
alias TradeMachine.GMGradeV2.Pillars.CapFlexibility.FutureFlexibility
alias TradeMachine.GMGradeV2.Pillars.CapFlexibility.TaxApronExposure
alias TradeMachine.GMGradeV2.Pillars.CapFlexibility.TransactionFlexibility
alias TradeMachine.GMGradeV2.Result.MetricResult
alias TradeMachine.GMGradeV2.Result.PillarResult
@metric_weights %{
current_cap_position: 0.20,
tax_apron_exposure: 0.30,
future_flexibility: 0.30,
transaction_flexibility: 0.20
}
@metric_modules [
CurrentCapPosition,
TaxApronExposure,
FutureFlexibility,
TransactionFlexibility
]
if abs(Enum.sum(Map.values(@metric_weights)) - 1.0) > 1.0e-9 do
raise "Cap & Flexibility metric weights must sum to 1.0"
end
@doc "Returns the full-precision Cap & Flexibility pillar result."
@spec score(map()) :: PillarResult.t()
def score(cap_state) when is_map(cap_state) do
metric_results = Enum.map(@metric_modules, & &1.score(cap_state))
{strongest, weakest} = extrema(metric_results)
%PillarResult{
key: :cap_flexibility,
version: ModelVersion.model_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
@doc "Combines Cap & Flexibility metric results at AVC's fixed weights."
@spec weighted_score([MetricResult.t()]) :: float()
def weighted_score(metric_results) when is_list(metric_results) do
# Intentional AVC emphasis: CapState's Effective Committed Salary already
# includes dead money, and Future Flexibility also applies the separate Dead
# Money Score. Do not remove this deliberate double-count.
Enum.reduce(metric_results, 0.0, fn result, total ->
total + 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
"Cap & Flexibility combines four cap-state metrics; " <>
"#{label(strongest_key)} is strongest at #{strongest_score}, while " <>
"#{label(weakest_key)} is weakest at #{weakest_score}."
end
defp label(:current_cap_position), do: "Current Cap Position"
defp label(:tax_apron_exposure), do: "Tax & Apron Exposure"
defp label(:future_flexibility), do: "Future Flexibility"
defp label(:transaction_flexibility), do: "Transaction Flexibility"
end
current_cap_position.exElixir
lib/trade_machine/gm_grade_v2/pillars/cap_flexibility/current_cap_position.ex
Scores AVC's Current Cap Position metric from a validated
CapState
snapshot.
The financial zone and dollar cushions are engine-owned facts. This module only normalizes the applicable cushion by the season's salary cap and applies AVC's five scoring curves, including their intentional threshold cliffs.
SHA-256 86523b0ebfc798130d4b957dad593e8019cf1c39fc8ad1f9e2f21102e1d472b8
defmodule TradeMachine.GMGradeV2.Pillars.CapFlexibility.CurrentCapPosition do
@moduledoc """
Scores AVC's Current Cap Position metric from a validated `CapState` snapshot.
The financial zone and dollar cushions are engine-owned facts. This module
only normalizes the applicable cushion by the season's salary cap and applies
AVC's five scoring curves, including their intentional threshold cliffs.
"""
alias TradeMachine.GMGradeV2.ModelVersion
alias TradeMachine.GMGradeV2.Result.MetricResult
@doc "Returns the full-precision Current Cap Position metric result."
@spec score(map()) :: MetricResult.t()
def score(%{
season: season,
financial_zone: zone,
thresholds: thresholds,
cushions: cushions
}) do
salary_cap = thresholds |> Map.fetch!(season) |> Map.fetch!(:salary_cap)
score = zone_score(zone, cushions, salary_cap)
%MetricResult{
key: :current_cap_position,
version: ModelVersion.model_version(),
score: score,
inputs: %{salary_cap: salary_cap},
supporting: Map.put(cushions, :financial_zone, zone),
explanation:
"Current Cap Position scores the #{label(zone)} zone using its engine-calculated " <>
"threshold cushion relative to the season's salary cap."
}
end
@doc "Applies AVC's financial-zone curve to engine-calculated cushions."
@spec zone_score(atom(), map(), pos_integer()) :: float()
def zone_score(:below_cap, cushions, salary_cap) do
60.0 + min(40.0, ratio(cushions.cap_room, salary_cap) * 200.0)
end
def zone_score(:over_cap_below_tax, cushions, salary_cap) do
50.0 + min(10.0, ratio(cushions.tax_cushion, salary_cap) * 100.0)
end
def zone_score(:taxpayer_below_first_apron, cushions, salary_cap) do
30.0 + min(20.0, ratio(cushions.first_apron_cushion, salary_cap) * 200.0)
end
def zone_score(:between_aprons, cushions, salary_cap) do
10.0 + min(20.0, ratio(cushions.second_apron_cushion, salary_cap) * 200.0)
end
def zone_score(:above_second_apron, cushions, salary_cap) do
max(0.0, 10.0 - ratio(cushions.second_apron_excess, salary_cap) * 200.0)
end
defp ratio(amount, salary_cap)
when is_number(amount) and amount >= 0 and is_number(salary_cap) and salary_cap > 0,
do: amount / salary_cap
defp label(:below_cap), do: "below-cap"
defp label(:over_cap_below_tax), do: "over-cap, below-tax"
defp label(:taxpayer_below_first_apron), do: "taxpayer, below-first-apron"
defp label(:between_aprons), do: "between-aprons"
defp label(:above_second_apron), do: "above-second-apron"
end
future_flexibility.exElixir
lib/trade_machine/gm_grade_v2/pillars/cap_flexibility/future_flexibility.ex
Scores AVC's three-season Future Flexibility metric from
CapState
.
Effective committed salary, dead money, and projected thresholds are consumed exactly as the cap snapshot reports them. Each future season combines the commitment and dead-money curves at 80/20, then the three seasons combine at 50/30/20.
SHA-256 d152c3ced49c9afcc6cc4b3c417a4fb6e7c28bc852fc0c83b3c85cb99910a425
defmodule TradeMachine.GMGradeV2.Pillars.CapFlexibility.FutureFlexibility do
@moduledoc """
Scores AVC's three-season Future Flexibility metric from `CapState`.
Effective committed salary, dead money, and projected thresholds are consumed
exactly as the cap snapshot reports them. Each future season combines the
commitment and dead-money curves at 80/20, then the three seasons combine at
50/30/20.
"""
alias TradeMachine.GMGradeV2.ModelVersion
alias TradeMachine.GMGradeV2.Result.MetricResult
@commitment_curve [
{0.50, 100.0},
{0.60, 90.0},
{0.70, 80.0},
{0.80, 70.0},
{0.90, 60.0},
{1.00, 50.0},
{1.10, 35.0},
{1.20, 20.0},
{1.30, 0.0}
]
@dead_money_curve [
{0.00, 100.0},
{0.01, 90.0},
{0.03, 75.0},
{0.05, 55.0},
{0.08, 30.0},
{0.10, 0.0}
]
@component_weights %{commitment: 0.80, dead_money: 0.20}
@season_weights [0.50, 0.30, 0.20]
if abs(Enum.sum(Map.values(@component_weights)) - 1.0) > 1.0e-9 or
abs(Enum.sum(@season_weights) - 1.0) > 1.0e-9 do
raise "Future Flexibility weights must sum to 1.0"
end
@doc "Returns the full-precision Future Flexibility metric result."
@spec score(map()) :: MetricResult.t()
def score(%{future_seasons: future_seasons}) do
seasons =
case future_seasons do
[first, second, third] ->
[first, second, third]
other ->
raise ArgumentError,
"Future Flexibility requires exactly 3 seasons, got: #{length(other)}"
end
supporting = Enum.map(seasons, &evaluate_season/1)
season_scores = Enum.map(supporting, & &1.score)
%MetricResult{
key: :future_flexibility,
version: ModelVersion.model_version(),
score: combine_seasons(season_scores),
inputs: %{future_season_count: length(seasons)},
supporting: %{seasons: supporting},
fallbacks: estimated_cap_fallbacks(supporting),
weights: %{
commitment: @component_weights.commitment,
dead_money: @component_weights.dead_money,
future_seasons: @season_weights
},
explanation:
"Future Flexibility scores unavoidable commitments and dead money over the " <>
"next three cap-state seasons, weighted 50/30/20."
}
end
@doc "Applies AVC's piecewise-linear commitment-ratio curve."
@spec commitment_score(number()) :: float()
def commitment_score(ratio) when is_number(ratio) and ratio >= 0 do
interpolate(ratio, @commitment_curve)
end
@doc "Applies AVC's piecewise-linear dead-money-ratio curve."
@spec dead_money_score(number()) :: float()
def dead_money_score(ratio) when is_number(ratio) and ratio >= 0 do
interpolate(ratio, @dead_money_curve)
end
@doc "Combines one future season's commitment and dead-money scores."
@spec season_score(number(), number()) :: float()
def season_score(commitment, dead_money)
when is_number(commitment) and is_number(dead_money) do
commitment * @component_weights.commitment +
dead_money * @component_weights.dead_money
end
@doc "Combines the next three season scores at AVC's 50/30/20 weights."
@spec combine_seasons([number()]) :: float()
def combine_seasons([_first, _second, _third] = scores) do
@season_weights
|> Enum.zip(scores)
|> Enum.reduce(0.0, fn {weight, score}, total -> total + weight * score end)
end
defp evaluate_season(%{
season: season,
thresholds: thresholds,
effective_committed_salary: committed,
dead_money: dead_money
}) do
salary_cap = Map.fetch!(thresholds, :salary_cap)
commitment_ratio = committed / salary_cap
dead_money_ratio = dead_money / salary_cap
commitment = commitment_score(commitment_ratio)
dead_money_component = dead_money_score(dead_money_ratio)
%{
season: season,
salary_cap: salary_cap,
threshold_source: Map.get(thresholds, :source),
effective_committed_salary: committed,
commitment_ratio: commitment_ratio,
commitment_score: commitment,
dead_money: dead_money,
dead_money_ratio: dead_money_ratio,
dead_money_score: dead_money_component,
score: season_score(commitment, dead_money_component)
}
end
defp estimated_cap_fallbacks(seasons) do
for season <- seasons, season.threshold_source == :estimated do
%{
input: season.season,
method: :snapshot_escalated_salary_cap,
note:
"No official threshold row was available; Future Flexibility used the frozen cap snapshot's recorded escalation fallback."
}
end
end
defp interpolate(value, [{minimum, score} | _rest]) when value <= minimum, do: score
defp interpolate(value, points) do
case Enum.chunk_every(points, 2, 1, :discard)
|> Enum.find(fn [{lower, _}, {upper, _}] -> value <= upper and value >= lower end) do
[{lower, lower_score}, {upper, upper_score}] ->
lower_score + (upper_score - lower_score) * (value - lower) / (upper - lower)
nil ->
points |> List.last() |> elem(1)
end
end
end
tax_apron_exposure.exElixir
lib/trade_machine/gm_grade_v2/pillars/cap_flexibility/tax_apron_exposure.ex
Scores AVC's Tax & Apron Exposure metric from a validated
CapState
.
CapState
owns the tax bill, repeater provenance, financial zone, and signed
threshold distances. This metric only normalizes the projected bill by the
salary cap and applies the curve for the already-classified zone.
SHA-256 8326e562ad5dbc139dd874d11398167e6f84e4555e6c59e78834c62b8ebb15ff
defmodule TradeMachine.GMGradeV2.Pillars.CapFlexibility.TaxApronExposure do
@moduledoc """
Scores AVC's Tax & Apron Exposure metric from a validated `CapState`.
`CapState` owns the tax bill, repeater provenance, financial zone, and signed
threshold distances. This metric only normalizes the projected bill by the
salary cap and applies the curve for the already-classified zone.
"""
alias TradeMachine.GMGradeV2.ModelVersion
alias TradeMachine.GMGradeV2.Result.MetricResult
@doc "Returns the full-precision Tax & Apron Exposure metric result."
@spec score(map()) :: MetricResult.t()
def score(%{
season: season,
financial_zone: zone,
taxpayer: taxpayer?,
thresholds: thresholds,
cushions: cushions,
luxury_tax: luxury_tax
}) do
salary_cap = thresholds |> Map.fetch!(season) |> Map.fetch!(:salary_cap)
tax_bill_ratio = luxury_tax.bill / salary_cap
%MetricResult{
key: :tax_apron_exposure,
version: ModelVersion.model_version(),
score: exposure_score(zone, tax_bill_ratio),
inputs: %{
financial_zone: zone,
salary_cap: salary_cap,
projected_tax_bill: luxury_tax.bill,
tax_bill_ratio: tax_bill_ratio
},
supporting: %{
taxpayer: taxpayer?,
repeater: luxury_tax.repeater,
tax_cushion: cushions.tax_cushion,
first_apron_cushion: cushions.first_apron_cushion,
second_apron_cushion: cushions.second_apron_cushion
},
explanation:
"Tax & Apron Exposure scores the #{label(zone)} zone using the projected " <>
"engine tax bill and recorded repeater assumption."
}
end
@doc "Applies AVC's zone-specific tax-bill penalty."
@spec exposure_score(atom(), number()) :: float()
def exposure_score(zone, _tax_bill_ratio)
when zone in [:below_cap, :over_cap_below_tax],
do: 100.0
def exposure_score(:taxpayer_below_first_apron, tax_bill_ratio) do
80.0 - min(20.0, tax_bill_ratio * 50.0)
end
def exposure_score(:between_aprons, tax_bill_ratio) do
45.0 - min(15.0, tax_bill_ratio * 37.5)
end
def exposure_score(:above_second_apron, tax_bill_ratio) do
15.0 - min(15.0, tax_bill_ratio * 37.5)
end
defp label(:below_cap), do: "below-cap"
defp label(:over_cap_below_tax), do: "over-cap, below-tax"
defp label(:taxpayer_below_first_apron), do: "taxpayer, below-first-apron"
defp label(:between_aprons), do: "between-aprons"
defp label(:above_second_apron), do: "above-second-apron"
end
transaction_flexibility.exElixir
lib/trade_machine/gm_grade_v2/pillars/cap_flexibility/transaction_flexibility.ex
Scores AVC's four supporting Transaction Flexibility calculations.
Signing mechanisms, capability flags, usable trade exceptions, roster space,
and waivability are consumed from
CapState
; no CBA rule is re-derived here.
The one time-based calculation discounts a TPE's recorded remaining amount
against the snapshot's frozen
as_of_date
.
CapState.signing_mechanisms.best
is deliberately authoritative even where
05-D58-APRON-MLE
records that the engine overstates apron-team MLE access.
Narrowing it here would violate D-55 by restating the engine's CBA rule.
SHA-256 1619b301e4988064367fbdaffdd41cab696152d48e41ebf7592106a5a813a4b6
defmodule TradeMachine.GMGradeV2.Pillars.CapFlexibility.TransactionFlexibility do
@moduledoc """
Scores AVC's four supporting Transaction Flexibility calculations.
Signing mechanisms, capability flags, usable trade exceptions, roster space,
and waivability are consumed from `CapState`; no CBA rule is re-derived here.
The one time-based calculation discounts a TPE's recorded remaining amount
against the snapshot's frozen `as_of_date`.
`CapState.signing_mechanisms.best` is deliberately authoritative even where
`05-D58-APRON-MLE` records that the engine overstates apron-team MLE access.
Narrowing it here would violate D-55 by restating the engine's CBA rule.
"""
alias TradeMachine.GMGradeV2.ModelVersion
alias TradeMachine.GMGradeV2.Result.MetricResult
@component_weights %{
signing_tools: 0.30,
trade_tools: 0.40,
trade_exception_capacity: 0.20,
roster_maneuverability: 0.10
}
@trade_penalties %{
can_aggregate: 25.0,
can_take_back_more: 20.0,
can_receive_sign_and_trade: 15.0,
can_use_existing_tpe: 15.0,
can_send_cash: 5.0
}
@doc "Returns the full-precision Transaction Flexibility metric result."
@spec score(map()) :: MetricResult.t()
def score(%{
season: season,
as_of_date: %Date{} = as_of_date,
thresholds: thresholds,
signing_mechanisms: signing_mechanisms,
capabilities: capabilities,
trade_exceptions: trade_exceptions,
roster: roster
}) do
ntmle = thresholds |> Map.fetch!(season) |> Map.fetch!(:non_taxpayer_mle)
components = %{
signing_tools: signing_tools(signing_mechanisms, ntmle),
trade_tools: trade_tools(capabilities),
trade_exception_capacity: trade_exception_capacity(trade_exceptions, ntmle, as_of_date),
roster_maneuverability: roster_maneuverability(roster)
}
%MetricResult{
key: :transaction_flexibility,
version: ModelVersion.model_version(),
score: combine_components(components),
inputs: %{
as_of_date: as_of_date,
non_taxpayer_mle: ntmle,
largest_usable_signing_mechanism: signing_mechanisms.best
},
supporting: components,
weights: @component_weights,
explanation:
"Transaction Flexibility combines the cap state's usable signing, trade, " <>
"trade-exception, and roster pathways at AVC's 30/40/20/10 weights."
}
end
@doc "Scores the largest usable CapState signing mechanism against the NTMLE."
@spec signing_tools(map(), pos_integer()) :: float()
def signing_tools(%{best: nil}, _ntmle), do: 0.0
def signing_tools(%{best: %{type: :minimum}}, _ntmle), do: 20.0
def signing_tools(%{best: %{remaining: remaining}}, ntmle)
when is_number(remaining) and remaining > 0 and is_number(ntmle) and ntmle > 0 do
min(100.0, 20.0 + 80.0 * remaining / ntmle)
end
@doc "Scores the five Capabilities flags using AVC's fixed penalties."
@spec trade_tools(map()) :: float()
def trade_tools(capabilities) when is_map(capabilities) do
penalty =
Enum.reduce(@trade_penalties, 0.0, fn {capability, points}, total ->
if Map.fetch!(capabilities, capability), do: total, else: total + points
end)
max(0.0, 100.0 - penalty)
end
@doc """
Scores the largest and second-largest practical TPEs after expiration discounts.
A TPE must be marked usable by CapState and carry at least 5% of the NTMLE in
remaining nominal value before its expiration multiplier is applied.
"""
@spec trade_exception_capacity([map()], pos_integer(), Date.t()) :: float()
def trade_exception_capacity(trade_exceptions, ntmle, %Date{} = as_of_date)
when is_list(trade_exceptions) and is_number(ntmle) and ntmle > 0 do
# Deliberate overlap with Trade Tools: CapState's `:usable` already folds in
# `can_use_existing_tpe`, so a barred team loses both the general trade
# capability and the practical capacity of TPEs it cannot consume. These
# are separate AVC supporting calculations, not duplicate arithmetic.
[largest, second | _rest] =
trade_exceptions
|> Enum.filter(fn tpe ->
Map.fetch!(tpe, :usable) and Map.fetch!(tpe, :remaining_amount) >= ntmle * 0.05
end)
|> Enum.map(fn tpe ->
days = Date.diff(Map.fetch!(tpe, :expiration_date), as_of_date)
Map.fetch!(tpe, :remaining_amount) * expiration_multiplier(days)
end)
|> Enum.sort(:desc)
|> Kernel.++([0.0, 0.0])
100.0 * min(1.0, (largest + 0.25 * second) / ntmle)
end
@doc "Returns AVC's expiration multiplier for a TPE's remaining whole days."
@spec expiration_multiplier(integer()) :: float()
def expiration_multiplier(days) when days < 30, do: 0.40
def expiration_multiplier(days) when days <= 90, do: 0.65
def expiration_multiplier(days) when days <= 180, do: 0.85
def expiration_multiplier(_days), do: 1.00
@doc "Combines open standard spots and easily-waivable contracts at 70/30."
@spec roster_maneuverability(map()) :: float()
def roster_maneuverability(%{
open_standard_spots: open_spots,
easily_waivable: easily_waivable
}) do
open_spot_score(open_spots) * 0.70 +
waivable_score(length(easily_waivable)) * 0.30
end
@doc "Combines the four supporting calculations at AVC's 30/40/20/10 weights."
@spec combine_components(map()) :: float()
def combine_components(components) when is_map(components) do
Enum.reduce(@component_weights, 0.0, fn {component, weight}, total ->
total + Map.fetch!(components, component) * weight
end)
end
defp open_spot_score(open_spots) when open_spots in 1..2, do: 100.0
defp open_spot_score(3), do: 90.0
defp open_spot_score(0), do: 75.0
defp open_spot_score(4), do: 75.0
defp open_spot_score(open_spots) when open_spots >= 5, do: 50.0
defp waivable_score(0), do: 50.0
defp waivable_score(1), do: 75.0
defp waivable_score(2), do: 90.0
defp waivable_score(count) when count >= 3, do: 100.0
end