defmodule TradeMachine.GMGradeV2.Pillars.AssetManagement.PickQuality do @moduledoc """ Scores the strength and depth of a team's best controlled draft assets. Best Asset and Top-Three raws come from the same `DAV.Inventory` used by the frozen benchmark. Premium Asset Inventory is AVC's threshold-and-curve score. """ alias TradeMachine.GMGradeV2.Benchmark alias TradeMachine.GMGradeV2.DAV.Inventory alias TradeMachine.GMGradeV2.Percentile alias TradeMachine.GMGradeV2.Pillars.AssetManagement.JSONSafe alias TradeMachine.GMGradeV2.Result.MetricResult @weights %{best_asset: 0.40, top_three: 0.40, premium_inventory: 0.20} @premium_curve [{0.0, 0.0}, {0.5, 30.0}, {1.0, 50.0}, {2.0, 75.0}, {3.0, 90.0}, {4.0, 100.0}] if abs(Enum.sum(Map.values(@weights)) - 1.0) > 1.0e-9 do raise "Pick Quality weights must sum to 1.0" end @doc "Builds and scores one team's final seven-draft inventory." @spec score(binary(), Inventory.graph(), map(), Benchmark.t()) :: MetricResult.t() def score(team_id, graph, dav_context, %Benchmark{} = benchmark) do team_id |> Inventory.value(graph, dav_context) |> score_inventory(benchmark) end @doc "Scores an already-built Phase 7 inventory against the frozen benchmark." @spec score_inventory(Inventory.t(), Benchmark.t()) :: MetricResult.t() def score_inventory(inventory, %Benchmark{} = benchmark) do best_quality = Percentile.score(inventory.best_asset, benchmark_values(benchmark, "best_asset")) top_three_quality = Percentile.score(inventory.top_three, benchmark_values(benchmark, "top_three")) premium_units = premium_units(inventory.assets) premium_score = premium_inventory_score(premium_units) score = best_quality * @weights.best_asset + top_three_quality * @weights.top_three + premium_score * @weights.premium_inventory ranked = Enum.sort_by(inventory.assets, &rank_key/1) %MetricResult{ key: :pick_quality, version: benchmark.version, score: score, inputs: %{ best_asset_raw: inventory.best_asset, top_three_raw: inventory.top_three, premium_asset_units: premium_units }, supporting: %{ best_asset_quality: best_quality, top_three_quality: top_three_quality, premium_asset_inventory: premium_score, best_asset: ranked |> List.first() |> JSONSafe.value(), top_three_assets: ranked |> Enum.take(3) |> JSONSafe.value() }, weights: @weights, explanation: "Pick Quality combines frozen-league percentiles for the best asset and " <> "top three with #{premium_units} Premium Asset Units." } end @doc "Returns AVC Premium Asset Units for a list of valued inventory assets." @spec premium_units([map()]) :: float() def premium_units(assets) when is_list(assets) do Enum.reduce(assets, 0.0, fn asset, total -> total + premium_units_for(Map.fetch!(asset, :inventory_dav)) end) end @doc "Maps one individual DAV value to AVC Premium Asset Units." @spec premium_units_for(number()) :: float() def premium_units_for(dav) when is_number(dav) and dav >= 75, do: 1.0 def premium_units_for(dav) when is_number(dav) and dav >= 55, do: 0.75 def premium_units_for(dav) when is_number(dav) and dav >= 35, do: 0.40 def premium_units_for(dav) when is_number(dav), do: 0.0 @doc "Applies AVC's piecewise-linear Premium Asset Inventory curve." @spec premium_inventory_score(number()) :: float() def premium_inventory_score(units) when is_number(units) and units >= 0 do interpolate(units, @premium_curve) end defp benchmark_values(benchmark, metric) do benchmark |> Benchmark.section(:dav) |> Map.fetch!("metrics") |> Map.fetch!(metric) end defp rank_key(asset) do {-asset.inventory_dav, asset.year, asset.round, asset.pick_id || "", asset.swap_group_id || "", asset.rank || 0} 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 >= lower and value <= upper 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