82 · V13.3s wired into the live picker (2026-05-19)
TL;DR
Until this change the live recommendation engine (src/lib/recommendation-engine.ts)
ranked candidates by Declaration.signalScore only (V12, 10-factor composite). The
V13.3s formula (V13.1g_stacked + V13.2 earnings overlay + V13.3 sector momentum
multiplier) had shipped inside computeV13Score() (src/lib/signals.ts) and
backtested to Sharpe 1.15 / DSR 0.53 / CAGR 43.2 % / MaxDD -14.1 % on the same OOS
harness as V13.1g, but it was not reaching users.
This audit captures the wiring change: persist a new Declaration.signalScoreV13
column, bulk-rescore the whole DB with computeV13Score(), and switch the engine
sort key to COALESCE(signalScoreV13, signalScore) DESC so the V13 alpha lands in
production. Legacy signalScore is preserved as a rollback safety net.
Change set
| Layer | File | Change |
|---|---|---|
| Schema | prisma/schema.prisma |
New Declaration.signalScoreV13 Float? + @@index |
| Migration | prisma/migrations/20260519600000_declaration_signal_score_v13/migration.sql |
Committed, not auto-applied. prisma migrate deploy runs it on CI/prod gate |
| Backfill script | scripts/_rescore-all-v13.ts |
Batches of 2000 rows, idempotent, --apply gate, bulk UPDATE ... FROM (VALUES ...) for speed |
| Engine BUY ranking | src/lib/recommendation-engine.ts (getBuyRecommendations) |
ROW_NUMBER() OVER (... ORDER BY COALESCE(signalScoreV13, signalScore) DESC, pubDate DESC) |
| Engine SELL ranking | src/lib/recommendation-engine.ts (getSellRecommendations) |
Converted to raw SQL, same coalesce |
| Engine personal | src/lib/recommendation-engine.ts (getPersonalRecommendations) |
orderBy: [{ signalScoreV13: { sort: 'desc', nulls: 'last' } }, { signalScore: 'desc' }] |
| Type | RecoItem.signalScoreV13?: number | null |
Persisted on payload so the UI can label "V13" badge later |
| Strategy proof | src/lib/winning-strategy.ts |
oosResults + monthlyPortfolio updated to V13.3s values |
signalScore (V12) is intentionally left untouched. If V13 needs to be rolled back,
drop the column or NULL it out and the engine falls through to V12 with no other
code change.
Why persist, not compute on the fly
The V13.3 multiplier needs sectorReturnPre180d (already persisted), the V13.2
overlay needs earningsProximityScore (already persisted), and V13.1g_stacked
needs clusterParticipantCount (±30d, same company, distinct insiders) and
insiderRecentAlpha90d (mean priors r90d for the same insider, strictly before
pubDate). Computing the last two per request would require a window scan and a
priors join inside the hot path of /recommendations. Persisting the composite
into one indexed column keeps the engine's candidate-fetch query index-bound
(Declaration_signalScoreV13_idx + the partition-key composite).
OOS stats moved into STRATEGY_PROOF.oosResults
From V13.1g (audit doc 78) to V13.3s (audit doc 80, wired here):
| Metric | V13.1g (prev live) | V13.3s (new live) | Δ |
|---|---|---|---|
| Sharpe (annualised) | 0.70 | 1.15 | +0.45 |
| Deflated Sharpe (N=11) | 0.33 | 0.53 | +0.20 |
| CAGR | 25.4 % | 43.2 % | +17.8 pp |
| Max DD | -29.7 % | -14.1 % | +15.6 pp |
| Hit rate (months) | 55.7 % | 57.1 % | +1.4 pp |
| Bootstrap CI95 Sharpe | [-1.18, 3.64] | [-0.62, 3.88] | width narrows |
CI95 still straddles zero — the lift is meaningful but the sample (14 monthly
buckets, n=140 picks) is too thin to be statistically robust against selection
bias. Surfaced via the existing sharpeDisclosure() helper.
Cache invalidation
Searched src/: no revalidateTag() callers for reco-general / reco-sells /
picks-cache / home-data. The caching layer uses unstable_cache with TTL
windows (e.g. sectors, top-movers, clusters/recent). The reco page itself
runs server-rendered against Prisma with no TTL cache wrapper, so the V13 cutover
takes effect on the next request after the rescore completes — no manual purge
needed.
Rollout
- Land schema + script + engine changes (this PR).
- CI green + Vercel deploy.
- On prod:
npx prisma migrate deploy(idempotent — applies the V13 column). - On prod box:
tsx scripts/_rescore-all-v13.ts(dry, sanity-check sample). - On prod box:
tsx scripts/_rescore-all-v13.ts --apply(writessignalScoreV13). - Watch /recommendations: top-10 BUY cards re-rank as V13 column populates.
Rollback path: drop the column or UPDATE "Declaration" SET "signalScoreV13" = NULL.
Engine COALESCE falls back to V12 silently.
Test sample · top BUY picks before vs after
To be captured post---apply on prod. The audit will be amended with the diff
table (top 10 V12-ranked vs top 10 V13-ranked) once the backfill completes.
Expected pattern from the bake-off: 2 of 14 OOS months saw cohort reranking
under V13.3s, and both flipped months were net wins. Day-1 live picks will
likely look very similar to V12, with sector-tagged BUYs (17.5 % coverage)
either lifted (mean-reversion band) or fade-dampened (euphoric sector band).