Fix the close-attach: de-vig raw prices, not a column that does not exist
Caught by inducing on real rows. The first attach ran and marked 642 rows market-unavailable while attaching ZERO closes — because it selected a `fair_prob` column from closing_captures, which has none. That table stores over_odds and under_odds deliberately (Session 64) so the de-vig can run later against the same engine the grade-time fair price uses; asking it for a probability returns nothing and makes every row look closeless. The de-vig now runs here, via devigTwoWay, which is what makes lock and close comparable at all. A one-sided capture yields no fair probability and is correctly not a close. Repair checked rather than assumed: the 642 markings turn out to be CORRECT — every one is a game from before closing capture existed on 2026-07-20, so those rows genuinely have no close and the absence is true. Zero capture-era rows were wrongly marked. The bug would have mis-marked every future row, which is what the fix prevents. Two tests added: the de-vig path with real prices, and a source assertion that the query never again asks closing_captures for a column it does not have. Tests 3616 passed / 294 suites. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCNgGSt5qvcLxaeQqa7Zpj
This commit is contained in:
@@ -315,17 +315,27 @@ async function attachClosingProb(sport, opts = {}) {
|
||||
if (error) return { updated: 0, error: error.message };
|
||||
if (!rows || !rows.length) return { updated: 0, absent: 0 };
|
||||
|
||||
// closing_captures deliberately stores BOTH RAW SIDE PRICES rather than a
|
||||
// probability (Session 64), so the de-vig runs here — the same devigTwoWay
|
||||
// the grade-time fair price uses, which is what makes lock and close
|
||||
// comparable at all. Asking this table for a `fair_prob` column is a bug: it
|
||||
// has none, and every row then looks closeless.
|
||||
const { devigTwoWay } = require('../utils/devig');
|
||||
const { data: caps } = await sb.from('closing_captures')
|
||||
.select('player_key, stat, side, game_date, fair_prob, captured_at, missed_reason')
|
||||
.select('player_key, stat, side, game_date, over_odds, under_odds, captured_at, missed_reason')
|
||||
.eq('sport', sp).limit(50000);
|
||||
|
||||
// Latest usable capture per identity = the TRUE close.
|
||||
const best = new Map();
|
||||
for (const c of caps || []) {
|
||||
if (c.missed_reason || c.fair_prob == null) continue;
|
||||
if (c.missed_reason) continue;
|
||||
const dv = devigTwoWay(c.over_odds, c.under_odds);
|
||||
const leg = dv && dv[String(c.side).toLowerCase() === 'under' ? 'under' : 'over'];
|
||||
const fp = leg && Number.isFinite(leg.fair_prob) ? leg.fair_prob : null;
|
||||
if (fp == null) continue; // one-sided or unusable → not a close
|
||||
const k = `${c.player_key}|${c.stat}|${c.side}|${c.game_date}`;
|
||||
const prev = best.get(k);
|
||||
if (!prev || String(c.captured_at) > String(prev.captured_at)) best.set(k, c);
|
||||
if (!prev || String(c.captured_at) > String(prev.captured_at)) best.set(k, { ...c, fair_prob: fp });
|
||||
}
|
||||
|
||||
let updated = 0; let absent = 0;
|
||||
|
||||
Reference in New Issue
Block a user