Arm harness on our scheduler + start closing-line capture (capture only)

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
This commit is contained in:
Kev
2026-07-20 12:05:16 -04:00
parent e809a0eb3c
commit 77a58e4113
6 changed files with 499 additions and 0 deletions
+43
View File
@@ -345,6 +345,49 @@ router.get('/backup/offbox', async (req, res) => {
}
});
/**
* POST /api/internal/harness/run (Session 64) — induce the nightly harness run
* on demand. Scheduled mechanisms are verified by INDUCING their real code
* path, never by waiting for a slot to discover whether they work.
*/
router.post('/harness/run', async (req, res) => {
try {
const runner = require('../services/harnessRunner');
const out = await runner.runAndRecord();
return res.json({ ok: out.ok !== false, ...out, last_run_at: await runner.lastRunAt() });
} catch (err) {
return res.status(500).json({ ok: false, error: err.message });
}
});
/**
* POST /api/internal/closing/capture/:sport (Session 64) — induce one closing
* capture pass against the CURRENT live odds, so the mechanism is proven now
* rather than discovered tomorrow.
*/
router.post('/closing/capture/:sport', async (req, res) => {
try {
const sp = String(req.params.sport || '').toLowerCase();
const closing = require('../services/closingCapture');
const odds = await require('../services/oddsService').getOdds(sp);
const props = (odds && Array.isArray(odds.props)) ? odds.props : [];
await require('../services/gameBinder').attachGameTimes(sp, props, {});
const windowMinutes = Number(req.query.window || 0) || undefined;
const rows = closing.buildCaptureRows(sp, props, { windowMinutes });
const priced = rows.filter((r) => !r.missed_reason).length;
const persisted = req.query.dry === '1' ? { skipped: true } : await closing.persist(rows);
const reasons = {};
for (const r of rows) if (r.missed_reason) reasons[r.missed_reason] = (reasons[r.missed_reason] || 0) + 1;
return res.json({
ok: true, sport: sp, props: props.length,
rows: rows.length, priced, missed: rows.length - priced,
missed_reasons: reasons, persisted,
});
} catch (err) {
return res.status(500).json({ ok: false, error: err.message });
}
});
/**
* POST /api/internal/ledger/settle (Session 58, Phase 1) — settle the
* persistent ledger (outcome + actual_value + CLV) across every sport.