2271f46ab2
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>
145 lines
6.5 KiB
JavaScript
145 lines
6.5 KiB
JavaScript
'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\)/);
|
|
}
|
|
});
|
|
});
|