dCLV close leg: read priced captures only (the unfixed twin)

computeDirectionalForRow selected the latest closing_captures row with no
missed_reason filter, while its sibling attachClosingProb has had one since the
CLV instrument repair. closing_captures records a refusal for every prop x book
x side on every cycle after first pitch, so a refusal always carries a later
captured_at than the last real price - latest-first returned a refusal on
26,448 of 26,448 identity groups, and computeDirectionalClv refuses on a
missedReason. That is why dclv_state has been 'unknown' on 100% of rows since
Session 64.

Measured on 600 real settled rows: 100% unknown becomes flat 45.8%, negative
23.0%, positive 22.8%, unknown 8.3%. ClvBadge will render MOVED TOWARD US 114
and MOVED AWAY 117 per 600 - near-symmetric, which is the honest shape.

Existing rows do not recompute (first-computation-wins). The re-stamp is
described in BUILD-STATE, not run.

The grade is untouched. No rows deleted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kev
2026-08-12 00:14:12 -04:00
parent 7c8ef8b12e
commit 2271f46ab2
4 changed files with 222 additions and 0 deletions
+28
View File
@@ -3,6 +3,34 @@
## Last Updated ## Last Updated
2026-08-12 2026-08-12
## Fix B1 (2026-08-12) — dCLV close-leg filter ✅
4,780 tests / 372 suites, web build exit 0. **Grade untouched (0 grade-related
diff lines); no rows deleted or archived.**
- Added `.is('missed_reason', null)` + both-odds-not-null to the close leg
(`ledgerService:625`), matching `attachClosingProb:455`. Filter applied BEFORE
the limit, so it is "latest PRICED" not "latest, hopefully priced".
- **dCLV: 100% `unknown` → flat 45.8% / negative 23.0% / positive 22.8% /
unknown 8.3%** on 600 real settled rows. ClvBadge (analyst/desk) will render
**MOVED TOWARD US 114 · MOVED AWAY 117** per 600 rows — ~38% of rows gain a
badge that showed nothing before.
- 8 regression tests, incl. one proving the fixture actually exercises the old
bug (an unfiltered read returns `unknown` on the same data).
- **`missed_window` is now genuinely unread** — the precondition the RECLAIM
order correctly stopped on.
### RE-STAMP DECISION (described, NOT run)
14,083 settled rows carry a persisted `dclv_state='unknown'`. `settleLedger` is
first-computation-wins, so they will NOT recompute. To correct them:
`UPDATE ledger_entries SET dclv_computed_at = NULL WHERE dclv_state = 'unknown'`
then re-run the settle pass — it would recompute from the (unchanged, immutable)
`closing_captures` and `model_snapshots` history. Arguments both ways:
FOR — those rows are wrong, and the history to fix them exists.
AGAINST — first-computation-wins exists so a badge a user saw never flips;
these rows currently show NO badge, so a re-stamp only ADDS badges (it cannot
flip a shown one), which weakens the objection considerably.
Recommend running it, as its own order, AFTER the fix has been live a full slate
and the new-row distribution is confirmed against the simulation.
## DEPLOY (2026-08-12 03:10 UTC) — A1–A7 LIVE, grade unchanged ✅ ## DEPLOY (2026-08-12 03:10 UTC) — A1–A7 LIVE, grade unchanged ✅
Commit **f61ec6b** pushed to gitea, auto-deployed by Coolify, first slot wrote Commit **f61ec6b** pushed to gitea, auto-deployed by Coolify, first slot wrote
2026-08-12T03:10:52Z. Pre-deploy gate: 4,772 tests / 371 suites, web build exit 2026-08-12T03:10:52Z. Pre-deploy gate: 4,772 tests / 371 suites, web build exit
+31
View File
@@ -2085,6 +2085,37 @@ phased plan in the Session-57 conversation / BUILD-STATE Next section).
on the cron and had last drained through 08-08. Accrual counts must be read off on the cron and had last drained through 08-08. Accrual counts must be read off
SETTLED rows, never graded ones. SETTLED rows, never graded ones.
## B1 — the dCLV close leg (non-obvious)
- **`computeDirectionalForRow` was the UNFIXED TWIN of the A1 CLV bug.** Its
sibling `attachClosingProb` (`ledgerService:455`) got `.is('missed_reason',
null)` during the CLV instrument repair; the close leg at `:625` never did, and
**nothing tested it** — which is exactly why it survived a fix made a few lines
above it. Both reads now filter, and a test asserts EVERY
`from('closing_captures')` read carries the filter.
- **The mechanism is a clock artifact, not a data gap.** `buildCaptureRows`
records a refusal for every prop × book × **both sides** on every ~20-minute
cycle AFTER first pitch, so a `missed_window` row ALWAYS has a later
`captured_at` than the last real price. `latest-first` without a filter
returned a refusal on **26,448 of 26,448** identity groups; `computeDirectionalClv`
refuses on a `missedReason`; hence `dclv_state = 'unknown'` on 100% of rows
since Session 64. **Filter BEFORE the limit** — "latest of the priced rows",
never "the latest row, hopefully priced".
- **MEASURED before/after on 600 real settled rows:** 100% `unknown` →
**flat 275 (45.8%) · negative 138 (23.0%) · positive 137 (22.8%) · unknown 50
(8.3%)**. Badge render: **MOVED TOWARD US 114 · MOVED AWAY 117 · no badge 369**.
The near-symmetry (114 vs 117) is the honest shape for a market that mostly
does not move — a lopsided result would have been the warning sign.
- **`positive` ≠ badge.** 137 positive but 114 rendered: `clvBadge` suppresses a
bare label when `locked_odds`/`closing_odds` can't form the receipt. Flat and
unknown render nothing by design.
- **EXISTING ROWS DO NOT RECOMPUTE.** `settleLedger:792` is
first-computation-wins (`row.dclv_computed_at ? null : compute…`) so a badge a
user saw can never flip. The 14,083 rows already stamped `unknown` stay
`unknown`; only rows settled from here carry real values. A re-stamp is a
separate, deliberate decision — described in BUILD-STATE, not run.
- **This makes `missed_window` genuinely unread**, which is the precondition the
reclaim order stopped on. The reclaim is still downstream and still needs R2.
## Active Skills ## Active Skills
- vyndr-voice (all user-facing output) - vyndr-voice (all user-facing output)
- prop-analysis (grading methodology) - prop-analysis (grading methodology)
+19
View File
@@ -605,10 +605,25 @@ async function computeDirectionalForRow(sb, sport, row, deps = {}) {
.eq('sport', sport).eq('player_key', row.player_key).eq('stat', row.stat) .eq('sport', sport).eq('player_key', row.player_key).eq('stat', row.stat)
.eq('side', row.side).eq('game_date', row.game_date) .eq('side', row.side).eq('game_date', row.game_date)
.order('captured_at', { ascending: true }).limit(1), .order('captured_at', { ascending: true }).limit(1),
// ── FIX B1 — READ ONLY PRICED CAPTURES ──────────────────────────
// This select had no `missed_reason` filter while its sibling
// `attachClosingProb` (above) has had one since the CLV instrument repair.
// `closing_captures` records a refusal for every prop x book x side on
// every ~20-minute cycle AFTER first pitch, so those rows always carry a
// LATER `captured_at` than the last real price — and `latest-first`
// therefore returned a refusal on 100% of identity groups (measured:
// 26,448 of 26,448 over five days). `computeDirectionalClv` refuses on a
// `missedReason`, which is why `dclv_state` has been 'unknown' on every
// row since Session 64.
//
// The filter belongs BEFORE the limit: latest of the PRICED rows, not
// "the latest row, hopefully priced".
sb.from('closing_captures') sb.from('closing_captures')
.select('over_odds, under_odds, missed_reason, captured_at') .select('over_odds, under_odds, missed_reason, captured_at')
.eq('sport', sport).eq('player_key', row.player_key).eq('stat', row.stat) .eq('sport', sport).eq('player_key', row.player_key).eq('stat', row.stat)
.eq('side', row.side).eq('game_date', row.game_date) .eq('side', row.side).eq('game_date', row.game_date)
.is('missed_reason', null)
.not('over_odds', 'is', null).not('under_odds', 'is', null)
.order('captured_at', { ascending: false }).limit(1), .order('captured_at', { ascending: false }).limit(1),
]); ]);
const lock = snaps && snaps[0]; const lock = snaps && snaps[0];
@@ -1085,5 +1100,9 @@ module.exports = {
rowsFromSnapshot, computeClv, clvResultOf, indexProps, gameIdFor, rowsFromSnapshot, computeClv, clvResultOf, indexProps, gameIdFor,
dateET, sideOf, oddsForSide, isConfigured, CONFLICT, dateET, sideOf, oddsForSide, isConfigured, CONFLICT,
teamOpponentFor, teamsMatch, clvBucketIndex, CLV_BUCKETS, teamOpponentFor, teamsMatch, clvBucketIndex, CLV_BUCKETS,
// B1 — exported so the close leg is directly testable. It was untestable,
// and that is why the missing missed_reason filter survived while its
// sibling was fixed a few lines above it.
computeDirectionalForRow,
}, },
}; };
+144
View File
@@ -0,0 +1,144 @@
'use strict';
/**
* B1 — the dCLV close leg reads PRICED captures only.
*
* THE TEST THE TWIN NEVER HAD. `attachClosingProb` got a `missed_reason` filter
* during the CLV instrument repair and a comment explaining why;
* `computeDirectionalForRow` did not, and nothing tested it, so the defect
* survived while its sibling was fixed a foot away.
*
* The mechanism: `closing_captures` records a refusal for every prop x book x
* side on every ~20-minute cycle AFTER first pitch, so a refusal ALWAYS carries
* a later `captured_at` than the last real price. `latest-first` without a
* filter therefore returns a refusal every time — measured 26,448 of 26,448
* identity groups — and `computeDirectionalClv` refuses on a `missedReason`.
*/
const ledgerService = require('../../src/services/ledgerService');
/**
* A fake client that records the filters applied to `closing_captures` and
* serves rows honouring them, so an unapplied filter fails the test.
*/
function fakeSb({ snaps, closes }) {
const seen = { closing: null };
return {
seen,
from(table) {
const st = { table, is: {}, notNull: [], order: null, asc: true };
const q = {
select() { return q; },
eq() { return q; },
is(col, val) { st.is[col] = val; return q; },
not(col, op, val) { if (op === 'is' && val === null) st.notNull.push(col); return q; },
order(col, o) { st.order = col; st.asc = !o || o.ascending !== false; return q; },
limit(n) {
if (table === 'model_snapshots') {
const rows = [...snaps].sort((a, b) => (st.asc ? 1 : -1) * String(a.captured_at).localeCompare(String(b.captured_at)));
return Promise.resolve({ data: rows.slice(0, n) });
}
seen.closing = { ...st };
let rows = [...closes];
// Honour the filters the production select applies.
if ('missed_reason' in st.is && st.is.missed_reason === null) rows = rows.filter((r) => r.missed_reason == null);
for (const c of st.notNull) rows = rows.filter((r) => r[c] != null);
rows.sort((a, b) => (st.asc ? 1 : -1) * String(a.captured_at).localeCompare(String(b.captured_at)));
return Promise.resolve({ data: rows.slice(0, n) });
},
};
return q;
},
};
}
const ROW = { player_key: 'luis arraez', stat: 'total_bases', side: 'over', game_date: '2026-08-10' };
// The lock: both side prices, so it is de-viggable.
const SNAPS = [{ fair_prob: 0.52, over_odds: -110, under_odds: -110, captured_at: '2026-08-10T14:00:00Z' }];
// The production shape: a real price, then refusals stamped after first pitch.
const CLOSES = [
{ over_odds: -140, under_odds: 120, missed_reason: null, captured_at: '2026-08-10T23:41:20Z' },
{ over_odds: null, under_odds: null, missed_reason: 'missed_window', captured_at: '2026-08-11T01:21:09Z' },
{ over_odds: null, under_odds: null, missed_reason: 'missed_window', captured_at: '2026-08-11T03:21:09Z' },
];
const compute = ledgerService.__internals
? ledgerService.__internals.computeDirectionalForRow
: null;
describe('the close leg selects the latest PRICED capture', () => {
it('applies the missed_reason filter (the fix)', async () => {
const sb = fakeSb({ snaps: SNAPS, closes: CLOSES });
await ledgerService.__internals.computeDirectionalForRow(sb, 'mlb', ROW);
expect(sb.seen.closing.is).toHaveProperty('missed_reason', null);
});
it('requires BOTH side prices, matching its sibling', async () => {
const sb = fakeSb({ snaps: SNAPS, closes: CLOSES });
await ledgerService.__internals.computeDirectionalForRow(sb, 'mlb', ROW);
expect(sb.seen.closing.notNull).toEqual(expect.arrayContaining(['over_odds', 'under_odds']));
});
it('still takes the LATEST of the priced rows, not the earliest', async () => {
const twoPriced = [
{ over_odds: -200, under_odds: 170, missed_reason: null, captured_at: '2026-08-10T18:00:00Z' },
...CLOSES,
];
const sb = fakeSb({ snaps: SNAPS, closes: twoPriced });
const out = await ledgerService.__internals.computeDirectionalForRow(sb, 'mlb', ROW);
// -140/120 (23:41) not -200/170 (18:00): the later price wins.
expect(sb.seen.closing.asc).toBe(false);
expect(out.state).not.toBe('unknown');
});
it('RETURNS A REAL STATE where the unfiltered read returned unknown', async () => {
const sb = fakeSb({ snaps: SNAPS, closes: CLOSES });
const out = await ledgerService.__internals.computeDirectionalForRow(sb, 'mlb', ROW);
expect(['positive', 'negative', 'flat']).toContain(out.state);
expect(out.clv).not.toBeNull();
expect(out.fair_close).not.toBeNull();
});
it('THE REGRESSION: an unfiltered read would have returned unknown on this data', async () => {
// Proves the fixture actually exercises the bug — a test that passes on data
// where the old code also worked proves nothing.
const dclv = require('../../src/services/directionalClv');
const latestUnfiltered = [...CLOSES].sort((a, b) => String(b.captured_at).localeCompare(String(a.captured_at)))[0];
const old = dclv.computeDirectionalClv({
side: ROW.side, lockFairProb: 0.52,
closeOverOdds: latestUnfiltered.over_odds, closeUnderOdds: latestUnfiltered.under_odds,
missedReason: latestUnfiltered.missed_reason,
});
expect(old.state).toBe('unknown');
expect(old.reason).toBe('missed_window');
});
it('a genuinely closeless identity is STILL unknown — absence is preserved', async () => {
const onlyRefusals = CLOSES.filter((c) => c.missed_reason);
const sb = fakeSb({ snaps: SNAPS, closes: onlyRefusals });
const out = await ledgerService.__internals.computeDirectionalForRow(sb, 'mlb', ROW);
expect(out.state).toBe('unknown');
expect(out.reason).toBe('no_usable_close');
});
it('no retained lock => null, never a fabricated comparison', async () => {
const sb = fakeSb({ snaps: [], closes: CLOSES });
expect(await ledgerService.__internals.computeDirectionalForRow(sb, 'mlb', ROW)).toBeNull();
});
});
describe('the two selects agree (the twin is no longer a twin)', () => {
const src = require('fs').readFileSync(
require('path').join(__dirname, '..', '..', 'src/services/ledgerService.js'), 'utf8');
it('BOTH closing_captures reads filter missed_reason', () => {
const reads = src.split("from('closing_captures')").slice(1);
expect(reads.length).toBeGreaterThanOrEqual(2);
for (const r of reads) {
const head = r.slice(0, 500);
expect(head).toMatch(/\.is\('missed_reason', null\)/);
}
});
});