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:
@@ -3,6 +3,34 @@
|
||||
## Last Updated
|
||||
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 ✅
|
||||
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
|
||||
|
||||
@@ -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
|
||||
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
|
||||
- vyndr-voice (all user-facing output)
|
||||
- prop-analysis (grading methodology)
|
||||
|
||||
@@ -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('side', row.side).eq('game_date', row.game_date)
|
||||
.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')
|
||||
.select('over_odds, under_odds, missed_reason, captured_at')
|
||||
.eq('sport', sport).eq('player_key', row.player_key).eq('stat', row.stat)
|
||||
.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),
|
||||
]);
|
||||
const lock = snaps && snaps[0];
|
||||
@@ -1085,5 +1100,9 @@ module.exports = {
|
||||
rowsFromSnapshot, computeClv, clvResultOf, indexProps, gameIdFor,
|
||||
dateET, sideOf, oddsForSide, isConfigured, CONFLICT,
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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\)/);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user