Testing Line Movement: Why Capture Signals Can't Work in a Closing-Line Model
Filtering bets where Pinnacle moved ≥3pp toward our selection removes only 127/6,606 bets (1.9%) with zero marginal ROI. The model uses closing odds — line movement is already in the CLV. Shelved without full gate. This plus 7 other capture signal failures confirm: the CLV→ROI gap is calibration, not execution quality.
If the line has already moved toward you, the value is gone. That's the theory. And it's correct — in markets where you bet at opening prices. Our model uses closing prices. The line movement information is already in the CLV calculation. Filtering on it is redundant.
The Question
When Pinnacle's 1X2 line moves ≥3 percentage points toward the outcome our model favors between opening and closing, should we skip that bet? The hypothesis: sharp bettors have already captured the value, and by the time we'd bet (at or near closing), the edge has been arbitraged away.
This signal was first tested on 2026-03-14 with only 7% opening odds coverage (104 bets with data). It was flagged as data-limited. The match cache now has 99.8% coverage — 21,637 of 21,690 matches have both opening and closing Pinnacle odds.
What We Found
The signal is a non-event.
| Metric | With filter | Without filter | Delta |
|---|---|---|---|
| Bets | 6,479 | 6,606 | -127 |
| CLV | +11.2% | +11.2% | +0.0pp |
| ROI | -3.0% | -3.0% | -0.0pp |
At the 3pp threshold, only 127 bets are removed — 1.9% of the pool. Pinnacle's 1X2 lines simply don't move that much. Most line movement is 0-2 percentage points, well below the filter threshold.
Standalone (minEdge=0, all filters off): N=84,977, ROI=-5.9%, CLV=+5.6%. The filter passes nearly everything through — it's not selecting a meaningful subset.
The Nuance
We actually tested line movement more aggressively in the capture signals session earlier today, using a 0.5pp threshold (much lower). That version — line-movement-confirms — showed +1.72pp marginal ROI, the closest any capture signal came to passing. But it failed IS/OOS validation: +6.03pp improvement in the 6 development leagues, +0.02pp in OOS leagues. The signal worked on EPL/La Liga (where Pinnacle lines are efficient and movement is informative) but not on lower leagues (where movement is noise).
| Threshold | Bets removed | Marginal ROI | IS/OOS |
|---|---|---|---|
| 3pp (this test) | 127 | -0.0pp | N/A (shelved) |
| 0.5pp (capture session) | 2,918 | +1.72pp | Divergent (6pp gap) |
The 0.5pp version removes too many bets from soft leagues where movement is noise. The 3pp version removes too few bets from any league. There's no threshold that works universally.
What Didn't Work
The fundamental issue is architectural: our model uses closing Pinnacle odds to compute edge. The CLV calculation is modelProb - closingImpliedProb. Line movement from opening to closing is already embedded in the closing price. Filtering on information the model has already consumed can't add value — it's like filtering on a variable that's already in the regression.
If we bet at opening prices (2-3 days before kickoff, Ted's "Goldilocks window"), line movement would be a genuine signal — we'd want to avoid bets where sharps were about to move the line against us. But our backtest evaluates at closing, and our paper trading targets closing-line-equivalent prices. The information is baked in.
What This Means
Shelved without full 10-gate run. Gate 4 (marginal ROI > 0) would fail immediately with -0.0pp.
The broader lesson: capture signals don't work in a closing-line model. We tested 8 capture variants (vig-aware, line movement, Pinnacle-vs-average gap, AH line shifts) and all failed. The model's CLV is computed against closing odds — any execution-quality information visible in the odds is already reflected in the CLV number. The CLV→ROI gap isn't about odds quality; it's about model calibration.
What's Next
No retest planned. The concept is sound for opening-price betting but irrelevant for closing-price evaluation. If we ever move to a system that targets opening prices (pre-sharp-money), this signal category becomes relevant again.
npx tsx scripts/test-signal.ts --signal=line-movement-filter