defmodule TradeMachine.GMGradeV2.Pillars.AssetManagement do @moduledoc """ Aggregates AVC's four Asset Management metrics. An uncalibrated player-involving Asset Exchange Value remains visible but is excluded under D-10. The other three weights are then renormalized from 35/20/20 to exact proportions of their 75% subtotal. """ alias TradeMachine.GMGradeV2.Pillars.AssetManagement.AssetExchangeValue alias TradeMachine.GMGradeV2.ModelVersion alias TradeMachine.GMGradeV2.Result.MetricResult alias TradeMachine.GMGradeV2.Result.PillarResult @full_weights %{ draft_capital_value: 0.35, pick_quality: 0.20, asset_optionality: 0.20, asset_exchange_value: 0.25 } @inventory_total 0.75 @provisional_weights %{ draft_capital_value: 0.35 / @inventory_total, pick_quality: 0.20 / @inventory_total, asset_optionality: 0.20 / @inventory_total, asset_exchange_value: 0.0 } if abs(Enum.sum(Map.values(@full_weights)) - 1.0) > 1.0e-9 or abs(Enum.sum(Map.values(@provisional_weights)) - 1.0) > 1.0e-9 do raise "Asset Management weights must sum to 1.0" end @doc "Aggregates the four canonical metric results in published order." @spec score([MetricResult.t()]) :: PillarResult.t() def score( [ %MetricResult{key: :draft_capital_value}, %MetricResult{key: :pick_quality}, %MetricResult{key: :asset_optionality}, %MetricResult{key: :asset_exchange_value} = exchange ] = metric_results ) do provisional? = AssetExchangeValue.status(exchange) == :provisional weights = if(provisional?, do: @provisional_weights, else: @full_weights) scored_results = if(provisional?, do: Enum.take(metric_results, 3), else: metric_results) {strongest, weakest} = extrema(scored_results) %PillarResult{ key: :asset_management, version: ModelVersion.model_version(), score: weighted_score(scored_results, weights), metric_results: metric_results, metric_weights: weights, provisional?: false, strongest: strongest.key, weakest: weakest.key, explanation: explanation(provisional?, strongest, weakest) } end @doc """ Projects Asset Management for a state-only comparison. The four canonical metric slots and their full 35/20/20/25 weights are preserved, but Asset Exchange Value is fixed at neutral `50`. This deliberately ignores whether the canonical AEV result was provisional: transaction-path evidence must affect neither side of a Comparable State Score. """ @spec comparable_score([MetricResult.t()]) :: float() def comparable_score( [ %MetricResult{key: :draft_capital_value}, %MetricResult{key: :pick_quality}, %MetricResult{key: :asset_optionality}, %MetricResult{key: :asset_exchange_value} ] = metric_results ) do metric_results |> Enum.map(fn %MetricResult{key: :asset_exchange_value} = result -> %{result | score: 50.0} result -> result end) |> weighted_score(@full_weights) end defp weighted_score(results, weights) when is_list(results) do Enum.reduce(results, 0.0, fn result, total -> total + result.score * Map.fetch!(weights, result.key) end) end defp extrema(results), do: {Enum.max_by(results, & &1.score), Enum.min_by(results, & &1.score)} defp explanation( true, %MetricResult{key: strongest, score: strongest_score}, %MetricResult{key: weakest, score: weakest_score} ) do rendered_weights = [:draft_capital_value, :pick_quality, :asset_optionality] |> Enum.map_join("/", &((Map.fetch!(@provisional_weights, &1) * 100) |> Float.round(2))) "Asset Exchange Value is displayed but excluded until PTV is calibrated; " <> "the 35/20/20 inventory weights are renormalized to #{rendered_weights}. " <> "#{label(strongest)} is strongest at #{strongest_score}, while " <> "#{label(weakest)} is weakest at #{weakest_score}." end defp explanation( false, %MetricResult{key: strongest, score: strongest_score}, %MetricResult{key: weakest, score: weakest_score} ) do "Asset Management applies the full 35/20/20/25 composition; " <> "#{label(strongest)} is strongest at #{strongest_score}, while " <> "#{label(weakest)} is weakest at #{weakest_score}." end defp label(:draft_capital_value), do: "Draft Capital Value" defp label(:pick_quality), do: "Pick Quality" defp label(:asset_optionality), do: "Asset Optionality" defp label(:asset_exchange_value), do: "Asset Exchange Value" end