77a58e4113
PART A — HARNESS ARMED ON OUR OWN INFRA. snapshotScheduler now runs the
nightly backtest at HARNESS_HOUR_UTC (default 14), appends to
harness_results, and pages via opsWatch.harnessStaleAlarm — a validator
that stops running looks exactly like one that keeps passing. No external
dependency: the join is plain SQL through the service client and the
harness is a pure function. POST /api/internal/harness/run induces the
same code path on demand, because a scheduled mechanism is verified by
inducing it, never by waiting for a slot.
PART B PHASE 0 — GATE PASSED for what is capturable:
- C4 diagnosed: closing_line is ONE overwritable field with no timestamp
and no provenance. captureClosing writes the current line and, when a
prop fails to match, silently leaves the earlier value (= the lock) in
place — so "captured a real close" is indistinguishable from "never
updated". It is 92% equal, not 100%: 56 rows DID record movement, so
the defect is provenance, not the value.
- Feeds: normalized props already carry BOTH raw side prices per book,
with game_time, and the intraday refresh polls every ~20 min during
slate hours — so the last observable pre-lock line is available.
- SHARP close: pinnacle is in ALLOWED_BOOKS -> a no-vig reference is
capturable ("beat the market").
- ODAWA: NOT capturable. 'odawa' exists only as a UI preference option in
onboarding/settings; it is in no adapter, no ALLOWED_BOOKS, no feed. An
un-capturable source is a finding, not a gap to paper over.
- JOIN: must drop `line` from the natural key, because a close that MOVED
off the graded line is the entire point of CLV. Verified safe — all 164
current identity groups have exactly ONE line per
(sport, player_key, stat, side, game_date). Zero ambiguity.
PART B PHASE 1 — CAPTURE ONLY, built test-first. The refusal was proven
before the capture logic existed: unbound game_time, doubleheader
ambiguity, a missed pre-lock window, or a one-sided price all record
missed_reason with NO price. A stale or mid-day line substituted for a
close would manufacture a CLV proof from a number that was never the
close.
migration 029 closing_captures: append-only, never overwritten (that is
the provenance C4 lacked), BOTH raw side prices so the existing de-vig
engine can compute a fair closing probability later, sharp vs book line
types kept distinct. Wired into the intraday refresh with a capture-rate
alarm — a missed close is unrecoverable.
NO CLV metric built, as ordered. This starts the clock.
Suite 284/3417 green, build exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SmNjJAwEnqHPtXbvSZR8kA
121 lines
5.0 KiB
JavaScript
121 lines
5.0 KiB
JavaScript
/**
|
|
* Session 64 — CLOSING-LINE CAPTURE (capture only; no CLV metric).
|
|
*
|
|
* Written BEFORE the capture logic. The first property proven is the refusal:
|
|
* a prop we cannot legitimately price at the close must record `missed_close`
|
|
* and NO price. Substituting a stale or mid-day line would manufacture a CLV
|
|
* proof out of a number that was never the close — the exact failure this
|
|
* whole substrate exists to prevent.
|
|
*/
|
|
|
|
const cap = require('../../src/services/closingCapture');
|
|
|
|
const prop = (o = {}) => ({
|
|
player: 'José Ramírez', stat_type: 'hits', line: 1.5,
|
|
over_odds: -120, under_odds: 100, book: 'draftkings',
|
|
game_time: '2026-07-21T23:10:00Z',
|
|
home_team: 'CLE', away_team: 'MIN',
|
|
...o,
|
|
});
|
|
|
|
const NOW = new Date('2026-07-21T22:55:00Z'); // 15 min before lock
|
|
|
|
describe('THE REFUSAL — never fabricate a close', () => {
|
|
test('a prop with NO bound game_time records missed_close, no price', () => {
|
|
const rows = cap.buildCaptureRows('mlb', [prop({ game_time: null })], { now: NOW });
|
|
// BOTH sides are recorded as missed — a refusal is per-side, exactly like a
|
|
// capture, so the join finds an explicit answer for either side.
|
|
expect(rows).toHaveLength(2);
|
|
expect(rows[0].missed_reason).toBe('unbound_game_time');
|
|
expect(rows[0].over_odds).toBeNull();
|
|
expect(rows[0].under_odds).toBeNull();
|
|
expect(rows[0].line).toBeNull();
|
|
});
|
|
|
|
test('a DOUBLEHEADER prop is ambiguous — recorded, never attributed', () => {
|
|
const rows = cap.buildCaptureRows('mlb', [prop({ game_ambiguous: true })], { now: NOW });
|
|
expect(rows[0].missed_reason).toBe('doubleheader_ambiguous');
|
|
expect(rows[0].over_odds).toBeNull();
|
|
});
|
|
|
|
test('a game already LOCKED (past start) records missed_close, not a stale line', () => {
|
|
const late = new Date('2026-07-21T23:30:00Z'); // 20 min AFTER first pitch
|
|
const rows = cap.buildCaptureRows('mlb', [prop()], { now: late });
|
|
expect(rows[0].missed_reason).toBe('missed_window');
|
|
expect(rows[0].over_odds).toBeNull();
|
|
});
|
|
|
|
test('a prop far from lock is NOT captured at all (not yet the close)', () => {
|
|
const early = new Date('2026-07-21T12:00:00Z'); // 11h before
|
|
const rows = cap.buildCaptureRows('mlb', [prop()], { now: early });
|
|
expect(rows).toHaveLength(0);
|
|
});
|
|
|
|
test('no capture row ever carries a price without a real close', () => {
|
|
const rows = cap.buildCaptureRows('mlb', [
|
|
prop({ game_time: null }), prop({ game_ambiguous: true }),
|
|
], { now: NOW });
|
|
for (const r of rows) {
|
|
expect(r.missed_reason).toBeTruthy();
|
|
expect(r.over_odds).toBeNull();
|
|
expect(r.under_odds).toBeNull();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('CAPTURE — inside the pre-lock window', () => {
|
|
test('captures BOTH side prices raw (de-vig is computed downstream, not here)', () => {
|
|
const [r] = cap.buildCaptureRows('mlb', [prop()], { now: NOW });
|
|
expect(r.missed_reason).toBeNull();
|
|
expect(r.over_odds).toBe(-120);
|
|
expect(r.under_odds).toBe(100);
|
|
expect(r.line).toBe(1.5);
|
|
});
|
|
|
|
test('a one-sided price is NOT a usable close — recorded as incomplete', () => {
|
|
const [r] = cap.buildCaptureRows('mlb', [prop({ under_odds: null })], { now: NOW });
|
|
expect(r.missed_reason).toBe('one_sided_price');
|
|
expect(r.over_odds).toBeNull();
|
|
});
|
|
|
|
test('rows carry the natural JOIN KEY minus line (a close moves off the graded line)', () => {
|
|
const [r] = cap.buildCaptureRows('mlb', [prop()], { now: NOW });
|
|
expect(r.sport).toBe('mlb');
|
|
expect(r.player_key).toBe('jose ramirez');
|
|
expect(r.stat).toBe('hits');
|
|
expect(r.game_date).toBe('2026-07-21');
|
|
expect(r).toHaveProperty('side');
|
|
});
|
|
|
|
test('both sides of a prop are captured as separate joinable rows', () => {
|
|
const rows = cap.buildCaptureRows('mlb', [prop()], { now: NOW });
|
|
expect(rows.map((r) => r.side).sort()).toEqual(['over', 'under']);
|
|
});
|
|
|
|
test('every row is timestamped and immutable-by-construction (append-only)', () => {
|
|
const [r] = cap.buildCaptureRows('mlb', [prop()], { now: NOW });
|
|
expect(r.captured_at).toBe(NOW.toISOString());
|
|
expect(r.book).toBe('draftkings');
|
|
});
|
|
|
|
test('sharp vs book close are distinguished (different CLV questions)', () => {
|
|
const rows = cap.buildCaptureRows('mlb', [prop({ book: 'pinnacle' })], { now: NOW });
|
|
expect(rows[0].line_type).toBe('sharp');
|
|
const rows2 = cap.buildCaptureRows('mlb', [prop({ book: 'draftkings' })], { now: NOW });
|
|
expect(rows2[0].line_type).toBe('book');
|
|
});
|
|
});
|
|
|
|
describe('capture-rate alarm (silent-failure discipline)', () => {
|
|
test('alarms when most in-window props failed to capture', () => {
|
|
const r = cap.captureRateAlarm({ eligible: 100, captured: 40, missed: 60 });
|
|
expect(r.alarm).toBe(true);
|
|
});
|
|
test('quiet on a healthy pass', () => {
|
|
expect(cap.captureRateAlarm({ eligible: 100, captured: 95, missed: 5 }).alarm).toBe(false);
|
|
});
|
|
test('nothing eligible never alarms', () => {
|
|
expect(cap.captureRateAlarm({ eligible: 0, captured: 0, missed: 0 }).alarm).toBe(false);
|
|
});
|
|
});
|