defmodule TradeMachine.GMGradeV2.PTV.NonlinearV1 do @moduledoc """ Interpretable nonlinear Player Transaction Value model. AVC's Player Market Value score establishes the basketball-value base. Contract Value Score then acts as a bounded multiplier, allowing an elite contract to amplify a scarce star without turning a low-impact bargain into a centerpiece. """ @behaviour TradeMachine.GMGradeV2.PTVModel alias TradeMachine.GMGradeV2.PTV alias TradeMachine.Valuation.PlayerValueCurve @impl true def value(player_snapshot, context) when is_map(player_snapshot) and is_map(context) do document = case Map.fetch(context, :coefficients_document) do {:ok, frozen} -> frozen :error -> PTV.coefficients_document() end parameters = Map.fetch!(document, "curve_parameters") quality = score!(player_snapshot, :player_market_value) contract = score!(player_snapshot, :contract_value_score) curve = PlayerValueCurve.evaluate(quality, contract, parameters) %{ ptv: curve.player_asset_value, confidence: if(Map.fetch!(document, "calibrated"), do: :model, else: :provisional), diagnostics: %{ model: __MODULE__, version: Map.fetch!(document, "version"), calibrated: Map.fetch!(document, "calibrated"), valuation_foundation: %{ player_market_value: quality, contract_value_score: contract, contract_value_tier: field(player_snapshot, :contract_value_tier), expected_salary: field(player_snapshot, :expected_salary), components: field(player_snapshot, :cvs_components) }, transforms: %{ quality_value: curve.quality_value, contract_multiplier: curve.contract_multiplier }, curve_parameters: parameters } } end @impl true def info do document = PTV.coefficients_document() %{ name: :nonlinear_v1, version: Map.fetch!(document, "version"), calibrated: Map.fetch!(document, "calibrated"), scale: :dav_equivalent, features: [:player_market_value, :contract_value_score] } end defp score!(snapshot, key) do case field(snapshot, key) do value when is_number(value) and value >= 0 and value <= 100 -> value * 1.0 value -> raise ArgumentError, "#{key} must be between 0 and 100, got: #{inspect(value)}" end end defp field(snapshot, key), do: Map.get(snapshot, key, Map.get(snapshot, Atom.to_string(key))) end