Files
vyndr/tests/unit/projectionInstrument.test.js
T
builtbykev c5580f333e Layer 3 Step 1: wire the measurement instrument
Step 0 found we have been flying without one. p_win lives only in
model_snapshots, which has 1,000 rows and ZERO settled outcomes; the closing
line lives only in closing_captures, which carries no link to a result; and
ledger_entries, the row that actually settles, carries no probability at all.
So "is the projection calibrated" and "does it beat the market" have never been
answerable — the entire measurable universe was 35 rows recovered by a lossy
in-memory join.

PHASE 0 — closing coverage verified BEFORE reuse, because an instrument built
on a partial close measures a biased subset. closing_captures holds 70,254 rows
of which 13,364 are usable, and the 56,890 refusals are candidates we never
graded plus one-sided prices — not refusals of our props. Coverage on graded
props since capture started is 83/83, 100%. Safe to reuse, with the honest
caveat that capture only began 2026-07-20.

THE FOUR-TUPLE NOW LANDS ON ONE ROW. ledger_entries gains p_win, fair_prob_lock,
archetype_vector and projection_locked_at at LOCK time, and closing_prob plus
closing_captured_at from the append-only capture store. The join is the whole
point: calibration is p_win against outcome, market-comparison is p_win against
the close, and both become plain SQL on one record instead of a join that
silently drops 90% of the rows.

p_win and the archetype vector are IMMUTABLE — written once at lock via the
existing ignoreDuplicates upsert, never re-derived at settle. A re-derivation
would measure a projection we never made.

The archetype is stored as the VECTOR, not the label. "Did archetype-awareness
help?" can only be answered against the axes that were live at grade time, and
a single text column cannot express a blend. A grade with no archetype stores
null rather than an empty object.

HONEST-ABSENT BOTH WAYS. A past game with no usable capture is marked
market_unavailable_reason and never given an imputed line; calibration still
scores on those rows, only market-comparison is absent. And a game that has not
started yet is NOT declared closeless — a close can still arrive, and premature
absence is as dishonest as imputation in the other direction.

One bug caught before it shipped: the scheduler hook iterated a SPORTS
identifier that does not exist in that scope. Inside its try/catch it would have
thrown ReferenceError every tick and silently never run — the instrument would
have looked wired and captured nothing. Now iterates cadence.ALL_SPORTS.

The baseline accrues FORWARD. Historical p_win and closes are gone, discarded
before this existed. Calibration and market-comparison stay honest-absent until
volume accrues.

Tests 3614 passed / 294 suites, web build exit 0. Migration 033 applied.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCNgGSt5qvcLxaeQqa7Zpj
2026-07-20 22:58:30 -04:00

151 lines
7.1 KiB
JavaScript

/* ============================================================
Session 70 — THE MEASUREMENT INSTRUMENT.
Calibration needs p_win beside the outcome. Market-comparison needs p_win
beside the close. Both need them ON THE SAME ROW. Before this, p_win lived
only in a table that never settles and the close lived only in a table with
no probability — so neither question was answerable.
============================================================ */
const ledger = require('../../src/services/ledgerService');
const GRADE = {
player: 'Josh Bell', stat_type: 'hits', direction: 'over', grade: 'B',
gradedAt: { line: 0.5, odds: -210, timestamp: '2026-07-21T03:00:00.000Z' },
p_win: 0.757, fair_prob: 0.633, edge_pct: 12, confidence: 63,
archetype_axes: { blend: [{ label: 'BOMBER', axis: 'POWER', tier: 'elite' }] },
};
const PROP = { player: 'Josh Bell', stat_type: 'hits', game_time: '2026-07-21T23:05:00Z', book: 'betmgm', over_odds: -210, under_odds: 170 };
/** rowsFromSnapshot is internal; exercise it through the public writer. */
async function rowFor(grade, prop = PROP) {
let captured = null;
const sb = { from: () => ({ upsert: async (rows) => { captured = rows; return { error: null }; } }) };
await ledger.recordPipelineGrades('mlb', [grade], [prop], { sb, now: () => '2026-07-21T03:00:00.000Z' });
return captured && captured[0];
}
describe('lock-time capture — the projection we ACTUALLY made', () => {
it('writes p_win, the market at lock, and the archetype VECTOR onto the row', async () => {
const r = await rowFor(GRADE);
expect(r.p_win).toBe(0.757);
expect(r.fair_prob_lock).toBe(0.633);
expect(r.archetype_vector).toEqual(GRADE.archetype_axes);
expect(r.projection_locked_at).toBe('2026-07-21T03:00:00.000Z');
});
it('lands them on the SAME record as the outcome — the join is the point', async () => {
const r = await rowFor(GRADE);
// One row carries identity + p_win + the settle target. Calibration and
// market-comparison are then plain SQL, not a lossy in-memory join.
for (const f of ['player_key', 'stat', 'line', 'side', 'game_date', 'p_win']) {
expect(r[f]).toBeDefined();
}
});
it('stores the VECTOR, not a label — a label cannot attribute anything', async () => {
const withBlend = await rowFor({ ...GRADE, archetype_axes: undefined, archetype_blend: [{ archetype: 'BOMBER', weight: 1 }], archetype: 'BOMBER' });
expect(withBlend.archetype_vector.blend).toHaveLength(1);
});
it('a grade with no archetype stores NULL, not an empty object', async () => {
const r = await rowFor({ ...GRADE, archetype_axes: undefined, archetype_blend: undefined, archetype: undefined });
expect(r.archetype_vector).toBeNull();
});
it('a grade with no p_win stores NULL, never 0', async () => {
const r = await rowFor({ ...GRADE, p_win: undefined, fair_prob: undefined });
expect(r.p_win).toBeNull();
expect(r.fair_prob_lock).toBeNull();
});
it('is IMMUTABLE — the writer never overwrites an existing lock', async () => {
// recordPipelineGrades upserts with ignoreDuplicates, so a re-run cannot
// rewrite p_win. Re-deriving at settle would measure a projection we never
// made.
let opts = null;
const sb = { from: () => ({ upsert: async (_r, o) => { opts = o; return { error: null }; } }) };
await ledger.recordPipelineGrades('mlb', [GRADE], [PROP], { sb });
expect(opts.ignoreDuplicates).toBe(true);
});
});
describe('attachClosingProb — the market half', () => {
const makeSb = ({ rows, caps }) => {
const updates = [];
return {
updates,
from: (t) => ({
select: () => ({
is: function () { return this; }, eq: function () { return this; },
limit: async () => ({ data: t === 'ledger_entries' ? rows : caps, error: null }),
}),
update: (patch) => ({ eq: async (_c, id) => { updates.push({ id, patch }); return { error: null }; } }),
}),
};
};
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 () => {
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' },
],
});
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
});
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' }],
});
const out = await ledger.attachClosingProb('mlb', { sb, beforeDate: '2026-07-21' });
expect(out.updated).toBe(0);
expect(sb.updates[0].patch.market_unavailable_reason).toBe('no_usable_close');
});
it('NEVER imputes a closing line — absent is marked, not filled', async () => {
const sb = makeSb({ rows: [ROW], caps: [] });
await ledger.attachClosingProb('mlb', { sb, beforeDate: '2026-07-21' });
expect(sb.updates[0].patch.closing_prob).toBeUndefined();
expect(sb.updates[0].patch.market_unavailable_reason).toBe('no_usable_close');
});
it('does NOT declare a future game closeless — a close can still arrive', async () => {
// Premature absence is as dishonest as imputation, in the other direction.
const sb = makeSb({ rows: [{ ...ROW, game_date: '2026-07-25' }], caps: [] });
const out = await ledger.attachClosingProb('mlb', { sb, beforeDate: '2026-07-21' });
expect(sb.updates).toHaveLength(0);
expect(out.updated).toBe(0);
});
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'));
expect(fn).toMatch(/\.is\('closing_prob', null\)/);
});
});
describe('what stays measurable when the market is absent', () => {
it('calibration needs only p_win + outcome; market-comparison needs the close', () => {
// Encoded as a contract check: the three fields are independent columns, so
// a row missing the close still scores for calibration.
const src = require('fs').readFileSync(require.resolve('../../src/services/ledgerService'), 'utf8');
expect(src).toMatch(/market_unavailable_reason/);
expect(src).toMatch(/p_win: numOrNull\(g\.p_win\)/);
});
});
describe('scheduler wiring', () => {
const src = require('fs').readFileSync(require.resolve('../../src/snapshotScheduler'), 'utf8');
it('runs the close attach on every configured sport', () => {
expect(src).toContain('attachClosingProb');
expect(src).toMatch(/for \(const sp of cadence\.ALL_SPORTS\)/);
});
});