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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user