defmodule TradeMachine.GMGradeV2.DAV.SlotCurve do @moduledoc """ Base Draft-Slot Value: what a single draft slot is worth on AVC's ~0–100 Draft Asset Value scale. This is the one place in the DAV model where a draft slot becomes a number. Everything else in Phase 07 — slot probability distributions, protection recursion, swap enumeration, the 0.92-per-year time discount — multiplies or averages the values this module returns. Keeping the curve here (rather than inlined at each call site) is what lets a stored grade name the exact curve that produced it: the constants are versioned as `:dav` in `TradeMachine.GMGradeV2.ModelVersion.versions/0` and read back by `version/0`. ## The two formulas (AVC Pillar 5, "Base Draft-Slot Value") first_round(s) = 20 + 80 × ((31 − s) / 30) ^ 1.35 for s in 1..30 second_round(s) = 3 + 17 × ((61 − s) / 30) ^ 1.15 for s in 31..60 Each round is a floor plus a decaying premium. The first round's floor of 20 is its limit at the hypothetical slot 31, and the second-round formula evaluates at slot 31 to exactly `3 + 17 = 20` — so the rounds meet by construction, not by coincidence. The visible step from slot 30 (20.81) to slot 31 (20.00) is one slot's worth of ordinary decline, not a discontinuity; a curve that jumped at the round boundary would make the 31st pick a different kind of asset from the 30th, which is not how the market treats them. The exponents differ because the rounds are shaped differently: 1.35 makes the first round steep at the top (the first three picks are worth far more than picks 8–10), while 1.15 keeps the second round nearly linear, because one second-rounder is not dramatically better than another. ## The curve is convex — never value a mean slot `value/1` accepts an **integer** slot and raises on anything else, including a float. That is a deliberate guard, not an oversight. Because the curve is convex, the value of the average slot is *not* the average value of the slot distribution — and the gap is one-directional, always understating the asset: value(15.5) = 52.8 ← wrong Σ p(s) × value(s) over 1..30 = 55.4 ← right (uniform 1–30, same mean) AVC's notes make this the central modelling instruction for distant picks: uncertainty widens the further out a draft is, and because the curve is nonlinear that extra spread makes an unprotected future pick worth *more* than its expected slot suggests. A caller that reaches for `value(expected_slot)` has silently thrown that away. Making the fractional slot raise turns the mistake into a crash instead of a plausible-looking number. Integrate over the distribution from `TradeMachine.GMGradeV2.DAV.SlotProbability` instead. ## DAV is not `predicted_aav` `TradeMachine.Valuation.Models.PickCurve` also maps a slot to a number, and the two must never be conflated (AVC, "Existing Pick Valuation Is Related but Not DAV"). That model returns **dollars** — the expected annual production value of the player the pick becomes — off a VORP-lineage curve, with its own PV discount, flat protection haircuts and fixed swap factors. This one returns a **unitless franchise-asset score** on AVC's 0–100 scale, and its downstream machinery values probability distributions, recursive protections and incremental swap gains instead. What Phase 07 reuses from that model is its *architecture* — pure functions, versioned constants, diagnostics — never its numbers. Any bridge between the two scales would have to be calibrated; there is no identity between them. ## These are initial parameters AVC states the curves are starting points to be calibrated against historical trade prices, rookie-contract surplus value, and realised outcomes by slot. When they are retuned, bump `:dav` in `ModelVersion.versions/0` so grades already stored stay reproducible against the curve that produced them. """ alias TradeMachine.GMGradeV2.ModelVersion # First round (slots 1–30): 20 + 80 × ((31 − s)/30)^1.35. The floor is the # formula's value at the hypothetical slot 31, which is what makes it meet the # second-round curve. @first_floor 20 @first_premium 80 @first_exponent 1.35 # Second round (slots 31–60): 3 + 17 × ((61 − s)/30)^1.15. @second_floor 3 @second_premium 17 @second_exponent 1.15 @doc """ Base DAV of a draft slot, at full precision. `slot` must be an integer in 1..60. First round (1–30) and second round (31–60) use different formulas; see the moduledoc. Approximate values, matching AVC's published tables: | Slot | DAV | Slot | DAV | |-----:|-----:|-----:|-----:| | 1 | 100.0| 31 | 20.0 | | 2 | 96.4| 35 | 17.4 | | 3 | 92.9| 40 | 14.3 | | 5 | 85.9| 45 | 11.3 | | 10 | 69.4| 50 | 8.4 | | 15 | 54.2| 55 | 5.7 | | 20 | 40.6| 60 | 3.3 | | 25 | 29.1| | | | 30 | 20.8| | | Raises `ArgumentError` on a non-integer or out-of-range slot — a fractional "expected slot" must be integrated over, not valued directly (see the moduledoc on convexity). """ @spec value(integer()) :: float() def value(slot) when is_integer(slot) and slot >= 1 and slot <= 30 do @first_floor + @first_premium * :math.pow((31 - slot) / 30, @first_exponent) end def value(slot) when is_integer(slot) and slot >= 31 and slot <= 60 do @second_floor + @second_premium * :math.pow((61 - slot) / 30, @second_exponent) end def value(slot) do raise ArgumentError, "draft slot must be an integer in 1..60, got: #{inspect(slot)}. " <> "The DAV curve is convex, so an expected/fractional slot must never be " <> "valued directly — sum value(s) weighted by a slot distribution instead." end @doc "Version of the DAV constants, frozen onto grades that read this curve." @spec version() :: String.t() def version, do: Map.fetch!(ModelVersion.versions(), :dav) end