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:
@@ -86,23 +86,28 @@ describe('attachClosingProb — the market half', () => {
|
||||
};
|
||||
const ROW = { id: 'r1', player_key: 'josh bell', stat: 'hits', side: 'over', game_date: '2026-07-20' };
|
||||
|
||||
it('writes the LATEST usable capture as the true close', async () => {
|
||||
it('DE-VIGS the raw both-side close prices (the table stores no probability)', async () => {
|
||||
// closing_captures deliberately stores over_odds/under_odds, NOT a
|
||||
// probability. Selecting a `fair_prob` column from it returns nothing and
|
||||
// makes every row look closeless — that shipped once and marked rows
|
||||
// market-unavailable without ever reading a capture.
|
||||
const sb = makeSb({
|
||||
rows: [ROW],
|
||||
caps: [
|
||||
{ player_key: 'josh bell', stat: 'hits', side: 'over', game_date: '2026-07-20', fair_prob: 0.60, captured_at: '2026-07-20T20:00:00Z' },
|
||||
{ player_key: 'josh bell', stat: 'hits', side: 'over', game_date: '2026-07-20', fair_prob: 0.64, captured_at: '2026-07-20T22:50:00Z' },
|
||||
{ player_key: 'josh bell', stat: 'hits', side: 'over', game_date: '2026-07-20', over_odds: -150, under_odds: 120, captured_at: '2026-07-20T20:00:00Z' },
|
||||
{ player_key: 'josh bell', stat: 'hits', side: 'over', game_date: '2026-07-20', over_odds: -210, under_odds: 170, captured_at: '2026-07-20T22:50:00Z' },
|
||||
],
|
||||
});
|
||||
const out = await ledger.attachClosingProb('mlb', { sb, beforeDate: '2026-07-21' });
|
||||
expect(out.updated).toBe(1);
|
||||
expect(sb.updates[0].patch.closing_prob).toBe(0.64); // the later one
|
||||
// The LATER capture is the true close, de-vigged from -210/+170.
|
||||
expect(sb.updates[0].patch.closing_prob).toBe(0.647);
|
||||
});
|
||||
|
||||
it('IGNORES refused captures — a missed_reason is not a close', async () => {
|
||||
const sb = makeSb({
|
||||
rows: [ROW],
|
||||
caps: [{ player_key: 'josh bell', stat: 'hits', side: 'over', game_date: '2026-07-20', fair_prob: 0.7, captured_at: 'x', missed_reason: 'one_sided_price' }],
|
||||
caps: [{ player_key: 'josh bell', stat: 'hits', side: 'over', game_date: '2026-07-20', over_odds: -210, under_odds: 170, captured_at: 'x', missed_reason: 'one_sided_price' }],
|
||||
});
|
||||
const out = await ledger.attachClosingProb('mlb', { sb, beforeDate: '2026-07-21' });
|
||||
expect(out.updated).toBe(0);
|
||||
@@ -124,6 +129,22 @@ describe('attachClosingProb — the market half', () => {
|
||||
expect(out.updated).toBe(0);
|
||||
});
|
||||
|
||||
it('a one-sided capture is NOT a close (de-vig needs both sides)', async () => {
|
||||
const sb = makeSb({
|
||||
rows: [ROW],
|
||||
caps: [{ player_key: 'josh bell', stat: 'hits', side: 'over', game_date: '2026-07-20', over_odds: -210, under_odds: null, captured_at: 'x' }],
|
||||
});
|
||||
await ledger.attachClosingProb('mlb', { sb, beforeDate: '2026-07-21' });
|
||||
expect(sb.updates[0].patch.market_unavailable_reason).toBe('no_usable_close');
|
||||
});
|
||||
|
||||
it('never asks closing_captures for a column it does not have', () => {
|
||||
const src = require('fs').readFileSync(require.resolve('../../src/services/ledgerService'), 'utf8');
|
||||
const fn = src.slice(src.indexOf('async function attachClosingProb'));
|
||||
expect(fn).toMatch(/from\('closing_captures'\)[\s\S]{0,200}over_odds, under_odds/);
|
||||
expect(fn).not.toMatch(/closing_captures'\)[\s\S]{0,200}fair_prob,/);
|
||||
});
|
||||
|
||||
it('only considers rows that have no close yet (write-once)', () => {
|
||||
const src = require('fs').readFileSync(require.resolve('../../src/services/ledgerService'), 'utf8');
|
||||
const fn = src.slice(src.indexOf('async function attachClosingProb'));
|
||||
|
||||
Reference in New Issue
Block a user