# UFC fight model — web spec (ufc-1.0)

**Paper testing only.** On 2024–26 out-of-sample bouts the model's log-loss was worse than the no-vig bookmaker line, and model-only "value" bets lost money. Treat it as a second opinion.

Files: `ufc_model.json` (state), `model.js` (ES module: `loadModel`, `predict`, `searchFighters`, `checkVectors`), `test_vectors.json` (12 cases generated by the Python `core.predict`), `check_vectors.mjs` (Node check).

## Data
- ufcstats.com bouts (Greco1899 scrape) through `data_through`. Only UFC bouts; regional/pre-UFC records are not included.
- Exported fighters: everyone with a UFC bout since 2016-01-01. UFC debutants are not in the data and can't be predicted.
- Per fighter (state after their last bout): Glicko rating `r` and deviation `rd`, first/last bout dates, bouts `n`, cage minutes `mins`, decayed minutes `dmins`, wins / KO wins / sub wins, KO-loss and knockdown-absorbed dates (last 2 years), DOB, last division `wc`, and for each stat k in {sig, sig_att, dist, clinch, ground, sub, td, td_att, kd, ctrl} `a[k] = [offense, offense_expected, defense, defense_expected]` (opponent-adjusted accumulators).
- `base[wclass][k]`, `gb[women][k]` = [sum, minutes] division and gender baselines.

## Pre-fight state at date D (must be after data_through)
- days = D − last; decay = 0.5^(days / (3·365.25)) applied to all accumulators and dmins.
- RD = min(√(rd² + C²·days/30), RD0) with C, RD0 from `params`.
- Rate baseline: gm = (gb₀+1)/(gb₁+10); b_k = (base₀ + 300·gm)/(base₁ + 300).
- Indices: oi_k = (o + M0·b_k)/(oe + M0·b_k), di_k = (d + M0·b_k)/(de + M0·b_k). Expected rate of A vs B: b_k·oi_k(A)·di_k(B).
- Smoothed rates: winrate (w+2)/(n+4), fin (kw+sw+1)/(n+4), ko (kw+.5)/(n+4), sub (sw+.3)/(n+4).

## Winner
p_A = logistic(Σ coef·x), no intercept, cols in `model.cols`: Glicko term g(√(rdA²+rdB²))·(rA−rB)·ln10/400, age diff, age floored at 32, positional rate diffs (distance/clinch/ground), experience (log1p bouts, log1p minutes, UFC years, debut), win/finish rate diffs, sub threat, RD diff/100, striking net and volume. Missing → 0.
Confidence: LIMITED EVIDENCE if min(n) < 3 or max(RD) > 120; MODERATE if min(n) < 6; else STANDARD.
With odds: no-vig market prob, EV per $1, and a diagnostic blend logistic(1.052·logit(market) + 0.246·logit(model)).

## Props (experimental)
- Per-minute finish hazard h_t = logistic(z·haz[0:11] + haz[11+t]), z = standardized covariates (fin, ko_l, kd_abs, kd_exp, sub_exp, sig_exp, women, heavy, light, mismatch=|logit p_A|, age−30). T = 15 or 25 minutes.
- P(distance) = Π(1−h_t); finish minute pmf pf_t; round totals over/under at L·5 minutes (linear within the minute).
- Method: P(A finishes | finish) from `win_fin`, KO share from `ko`; constrained so A's finishes ≤ p_A.
- Stats: count ~ NB(mean = cal_k·rate·minutes, size = disp_k), mixed over fight length (8 points per finish minute + the distance atom). Quantiles are exact mixture quantiles (smallest k with CDF ≥ q). Control time is in 5-second units.
- `extrapolation` lists covariates more than 4 SD from the training mean (usually fighters with very few UFC minutes) — props are unreliable then.
