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
+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('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,
},
};