---
name: Non-price features (API-Football form stats) add NOTHING to the closing line — they make predictions worse
description: 2026-07-25. 8,704 fixtures, time-based split, Pinnacle closing as baseline. Market log loss 0.99466; market + form features 1.00297 at best regularisation; features alone 1.08992. Worse at EVERY setting, and worse than the 0.00116 noise floor of refitting the market on itself. Public stats are already in the price. Run research/nonprice_edge_test.py.
metadata:
  type: project
---

The last untested hypothesis after [[project_edge_mine_exhausted]] closed the
price-derived space: does genuinely different information -- form, shots,
possession, discipline -- know something the market does not?

**No.** 8,704 fixtures matched (API-Football features joined to football-data
Pinnacle closing odds via `daily_signals.fdco_name`), trained on the earlier 70%
by DATE and tested on the later 30%.

| model | test log loss | vs market |
|---|---|---|
| **Pinnacle closing** | **0.99466** | — |
| market logits only, recalibrated | 0.99582 | −0.00116 |
| market + form diffs (best L2, C=0.1) | 1.00297 | −0.00831 |
| API features alone | 1.08992 | −0.09526 |

Worse at every regularisation level (C = 0.001 → 1.0), and worse than the
−0.00116 noise floor from refitting the market on itself — so the features are
not merely redundant, they inject noise.

**Why this is the expected result, not a modelling failure.** Form, shots,
corners, cards and possession are PUBLIC. Pinnacle pays people to model exactly
this with better data. By the time the closing line prints, it is in the price.
The first attempt (114-feature gradient booster on 6k rows) also lost to the
market — that one was overfitting, so the regularised logistic above is the fair
test, and it agrees.

## Methodology notes worth reusing

- Rolling features in `refresh_api_football.build_features` are `.shift(1)` lagged
  before `.rolling()`, so there is no same-match leakage. Verified before use.
- **TIME-based split, never random CV.** Team form is autocorrelated and market
  quality improves over time; random folds leak both.
- The right baseline is the devigged CLOSING price, not a naive prior. "Model
  predicts football" is meaningless; "model beats the close" is the only question.

## SECOND, HARDER ATTEMPT: Elo (research/elo_model_test.py) — also dead

The user pushed back that rolling means are a weak strength representation. Fair.
Rebuilt with **Elo** (50,574 fixtures 2017-2025, walk-forward so a match is always
scored with the rating held BEFORE it, goal-difference-weighted updates, home
advantage 65) plus **rest days**. Join improved from 8,704 to **31,963** matched
fixtures by going through `fixtures_index.csv` instead of `match_features.csv`.

| model | test log loss | vs market |
|---|---|---|
| **Pinnacle closing** | **0.98501** | — |
| Elo alone (logistic) | 1.01151 | −0.02650 |
| Elo alone (GBM) | 1.01297 | −0.02796 |
| market + Elo (logistic C=0.1) | 0.98470 | **+0.00031** |

That last row *looks* like a win. **It is not: paired t = +0.71 on n=9,589. Noise.**

**The trap that nearly fooled me.** A betting simulation on the same model showed
+2.7% ROI, seemingly contradicting the log-loss null. The control settles it —
betting favourites with NO MODEL AT ALL gives:

| devigged price | n | implied | actual | ROI |
|---|---|---|---|---|
| 0.00–0.15 | 6,709 | 10.71% | 9.61% | −11.91% |
| 0.60–1.00 | 6,428 | 70.18% | 72.82% | +3.83% |
| favourites >40% | 24,920 | 54.00% | 55.10% | **+1.89%** |

The model's "edge" was just **selecting favourites**, in a table where
proportional devig understates favourites (classic favourite-longshot). And those
ROIs are at devigged prices nobody can bet — Pinnacle's overround is 3.3%, so
+1.89% is really ≈ −1.4% before any spread.

**RULE: whenever a model shows betting profit, run the no-model control on the
same slice before believing it.** A log-loss null plus a betting "win" means the
betting test is picking up a price-bucket artifact, not skill.

Elo alone landing within 2.7% of Pinnacle's close is genuinely respectable for
goal differences and rest days. It is simply not enough, and cannot be — the
close already contains it plus injuries, lineups and professional money.

## THIRD ATTEMPT: everything at once (research/full_model_test.py) — also dead

User: *"Try more models. Theres so many features bro. You need to try."* Fair —
the first two attempts tested features in ISOLATION. So: 43 features (Elo at K=10/
20/40, venue-specific Elo, rest days, rolling pts/GF/GA over 5 and 10, venue-
specific form, head-to-head goal difference, season progress), 12 model configs
(logistic across 6 regularisations, GBM across 4 depth/rate settings, RandomForest,
features-only), 31,963 fixtures, time-based split. Success criterion fixed BEFORE
running: paired t > 1.96 vs market.

**0 of 12 met it.** Best was +0.00042, t=+0.61.

**The shape of the failure is the real finding:**

| model | vs market | t |
|---|---|---|
| logistic, heavy regularisation | +0.0004 | +0.6 |
| GBM depth 2 | −0.0016 | −2.56 |
| GBM depth 4 | −0.0037 | −4.03 |
| GBM depth 6 | −0.0070 | −5.71 |
| RandomForest | −0.0038 | −3.80 |
| features only | −0.0269 | −13.15 |

**Every increase in model capacity makes it significantly WORSE.** That is the
signature of no signal — with real signal, flexibility finds more of it. The only
configs that don't hurt are regularised so hard they effectively just pass the
market probability through.

**Do not re-run this with "more features" or "a better model."** Three
independent attempts, increasingly thorough, same answer. Every feature here is
downstream of past goals and results, and Pinnacle models those professionally
before the line closes.

## What is NOT ruled out (and why I would not chase it)

**Faster information** — lineups (~1h pre-KO), injuries, weather. API-Football
serves all of it and the account is Ultra (75k req/day, ~400 used). Real mechanism:
a repricing window between news and market adjustment. But the closing line
already absorbs lineup news, so exploiting it means trading BEFORE the reprice —
competing on latency against better infrastructure — and anything found still has
to clear the 0.67c half-spread ([[project_xlow_spread_verdict]]). Three filters,
low odds, days of work. Told the user the marginal value is low.

Related: [[project_edge_mine_exhausted]], [[project_xlow_spread_verdict]],
[[project_pin_calibration_verified]].
