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