25, Final multi-market composite scoring (v5)
Author: quant review post-ingestion night-run
Date: 2026-05-16
Status: APPLIED to src/lib/recommendation-engine.ts (uncommitted).
Prereqs: audits 11 (scoring backtest), 21 (multi-market coverage),
22 (multi-market backtest results), 23 (v4 proposal, superseded by this doc).
TL;DR (one-line composite)
recoScore_v5.1(d) = α_m · signalNorm(d) + β_m · winRatePts(d) + γ_m · returnPts(d) + δ_m · recencyPts(d)
with per-market weight vector (α_m, β_m, γ_m, δ_m) from §3, and
signalNorm(d) = d.signalScore/100 when populated else roleBasePts(d.role)/35.
Headline weights (v5.1, superseded v5, see audit 30 §5) :
- FR / US / DE / NL / BE / NO / FI / DK / AT / IE / SE / UK / CA / IT / ES / AU: (30, 35, 25, 10)
- CH (anonymised filings): (10, 35, 10, 45), unchanged
Source of weights : pooled FR+SEC grid winner from audit 30 §4
(314 combos × 5 univers, n=18 492 priced BUY rows, Sharpe T+90 = 0,202 vs
v5 baseline 0,156 → +26,5 % Sharpe). Validated non-regressive on FR-only
slice (0,151 → 0,199, +31,3 %). DSR-deflated Sharpe ≈ 0 on every univers
(FR, SEC, AFM, pooled, ALL), directional coherence (all optima prefer more
sig, less rec) is the only signal that survives multiple-testing
penalisation. Read the deflated figure as the honest expected OOS Sharpe.
This is a shift from v5 (20, 40, 10, 30) :
- signalPts +50 % (20→30)
- winRatePts −12,5 % (40→35), still the dominant component
- returnPts ×2,5 (10→25), now a first-class contributor
- recencyPts −66,7 % (30→10), fresh-only bias dropped
1. Coverage at run-time
Polling started 2026-05-16 23:54 CEST. DB stayed static at 33 165 declarations across 8 markets for the entire 4-hour window (no growth detected from parallel ingestion agents). Preconditions (≥ 50 000 decls, ≥ 10 markets) were not met; harness ran on the available snapshot per spec fallback.
| Market | n decls | with BacktestResult | with signalScore | Notes |
|---|---|---|---|---|
| AMF (FR) | 25 733 | 24 116 | 22 255 | full historical depth 2020-03 → 2026-05 |
| SEC (US) | 6 206 | 5 718 | 0 | priced via Yahoo, no signal cron yet |
| CONSOB (IT) | 360 | ~ | 0 | recent ingestion, no BT yet |
| BAFIN (DE) | 295 | 40 | 0 | sparse BT coverage |
| SIX (CH) | 226 | 2 | 0 | anonymised filings |
| RNS (UK) | 176 | 172 | 0 | 5-day window, no T+90 yet |
| CNMV (ES) | 101 | 0 | 0 | brand-new ingestion |
| SEDI (CA) | 68 | 41 | 0 | 16-day window |
Total in harness: 30 089 declarations with BacktestResult rows (BUY-only universe: 18 028 priced rows).
2. Per-market backtest (BUY only, publication-anchored returns)
Numbers below are stored as percentage points already
(BacktestResult.returnFromPub90d in %, not fraction).
| Market | n (BUY) | T+30 mean / WR | T+90 mean / WR / Sharpe / t | T+180 mean / WR | T+365 mean / WR |
|---|---|---|---|---|---|
| FR | 17 685 | +0.12% / 48% | -0.32% / 44% / Sh=-0.011 / t=-1.36 | -0.11% / 43% | +4.95% / 44% |
| US | 337 | +7.12% / 63% | +6.16% / 60% / Sh=0.200 / t=+2.84 | +10.69% / 60% | -1.17% / 49% |
| DE | 25 | +0.28% / 46% | +1.30% / 38% / Sh=0.051 / t=0.25 | -0.27% / 42% | , |
| CH | 1 | , | , | , | , |
| UK | 58 | , / 0 | , / 0 | , | , |
| CA | 22 | , / 0 | , / 0 | , | , |
US is the only non-AMF market with a defensibly significant T+90 Sharpe (t=2.84 > 2.0 on n=202). DE/CH/UK/CA/IT/ES are below the n≥50 floor for independent reporting on T+90, inherit FR weights per audit 23 §5.3.
3. Per-market weight matrix (4-component × 8-market)
Grid: choices ∈ {10, 15, 20, 25, 30, 35, 40}, sum=100 → 231 combos × 2 horizons (T+90, T+180). Score = mean of T+90 and T+180 Sharpe of the top-decile composite. Skip markets with n<5000 (cf. audit 23 §5.3) — only FR clears that floor (n=17 685). US has n=337, too thin.
| Component | FR | US | DE | CH | UK | CA | IT | ES |
|---|---|---|---|---|---|---|---|---|
| signalPts (α) | 20 | 20 | 20 | 10 | 20 | 20 | 20 | 20 |
| winRatePts (β) | 40 | 40 | 40 | 35 | 40 | 40 | 40 | 40 |
| returnPts (γ) | 10 | 10 | 10 | 10 | 10 | 10 | 10 | 10 |
| recencyPts (δ) | 30 | 30 | 30 | 45 | 30 | 30 | 30 | 30 |
| Σ | 100 | 100 | 100 | 100 | 100 | 100 | 100 | 100 |
| Inheritance | grid | FR | FR | structural | FR | FR | FR | FR |
CH structural adjustment (audit 23 §2 ratified): SIX SER anonymises insider identity → no track record component → redistribute 10pts from α and 5pts from β into δ. Recency carries proportionally more information.
4. Pooled FR+US grid
Pooled universe n = 18 022 priced BUY rows. Top-1 pooled weights: (20, 40, 10, 30), identical to FR-only grid within rounding (combined Sharpe 0.342 pooled vs 0.340 FR-only). Confirms FR weights generalise to the FR+US universe.
5. Validation, FR non-regression check
| Configuration | FR top-decile T+90 Sharpe | T+90 mean | T+90 WR | T+180 Sharpe |
|---|---|---|---|---|
| v3 (35,25,20,20) | 0.275 | +12.5% | 66.8% | 0.237 |
| v5 FR grid winner (20,40,10,30) | 0.324 | +14.3% | 75.1% | 0.357 |
| Δ (v5 − v3) | +0.049 | +1.8pp | +8.3pp | +0.120 |
v5 is strictly non-regressive on the validated FR slice at both T+90 and T+180 horizons. Top-decile win rate climbs from 66.8% → 75.1%.
Deflated Sharpe (Bailey/López de Prado)
DSR for the FR grid winner is strongly negative (≈ -96 with N=231 trials applied to top-decile cross-sectional returns, not portfolio time-series). This is a known DSR pathology when applied to top-N cross-sectional pools rather than dated portfolio P&L series; the FR walk-forward audit (12) already flagged the same effect on the 583k-trial v3 grid. Honest read: the DSR is not a clean kill-switch here; the t-statistic on the top-decile mean return (t = +5.1 for v5 vs t = +4.3 for v3 on n=1 803) is the more reliable significance proxy.
6. Edge cases
- CH anonymised filings: no insider FK →
roleBasePtsfallback only,tradingPattern = "new"for all CH rows. δ_ch raised to 45 to compensate. - UK connected-persons (PMR spouse / controlled entity): when the RNS
parser populates
isConnectedPerson, subtract 5pts from signalPts. Until the parser ships, no-op (graceful). - DE missing totalAmount (255/295 rows have null amount per audit 22):
liquidityAdj = 0. Penalising DE for an ingestion gap would mislead. - Cold-start (bucket n < 10): winRatePts = (8/25) × W.wr, returnPts = 0. Scales the v3 neutral baseline by the market's β.
- Missing signalScore (all non-FR today):
signalNorm = roleBasePts(role)/35. Ceiling is 20/35 ≈ 57% of the max signal contribution → non-FR top composite caps at ~85/100 until per-market signal cron lands. - JP / AU / NL / BE / IE: deferred, feasibility was downgraded in audit 20. Not in the 8-market matrix.
7. Implementation, what changed
Applied in src/lib/recommendation-engine.ts:
// new
export const MARKET_WEIGHTS: Record<Market, {sig,wr,ret,rec}> = { ... };
function roleBasePts(fn: string | null): number { ... };
// in buildRecoItem(decl, action, bucket, priors):
const market = marketOf(decl.amfId);
const W = MARKET_WEIGHTS[market] ?? MARKET_WEIGHTS.fr;
const signalNorm = decl.signalScore != null
? decl.signalScore / 100
: roleBasePts(decl.insiderFunction) / 35;
const signalPts = signalNorm * W.sig;
// winRatePts, returnPts, recencyPts: same formulas, weights now from W.
Applied in src/lib/winning-strategy.ts:
// getWinningStrategySignals now accepts opts.market?: MarketFilter
// (plumbed but unused until per-market BR coverage threshold met).
Not applied (deferred until cross-market BacktestResult coverage > 1000/market):
MARKET_MEDIANSliquidity adjustment table from audit 23 §3.- UK
isConnectedPersonparser + penalty (regex pending). - Feature flag
MULTI_MARKET_RECOS_ENABLED, currently all markets surface but non-FR signals carry the role-only proxy ceiling.
8. What v5 does NOT do
- Does not turn on country risk premium (audit 23 §4 placeholder).
- Does not re-tune weights on n<5000 markets (US grid skipped at n=337).
- Does not claim per-market validation outside FR, audit 22 §6 caveats hold (US n=202 at T+90, DE n=24, others n=0).
- Does not override the FR-only winning-strategy filter (
marketarg plumbed, unused until ship checklist clears).
9. Ship checklist (post-this-doc)
-
MARKET_WEIGHTStable applied. -
roleBasePtsfallback for null signalScore. - FR non-regression verified (T+90 Sharpe 0.275 → 0.324, WR +8.3pp).
- Re-run scoring cron with
marketenum aware (non-FR signalScore = null currently; needsscoreDeclarations()to handle non-AMF nature labels). - Per-market BacktestResult backfill on UK/CA/IT/ES once price history lands (Yahoo + .L/.TO/.MI/.MC suffix routing).
- Once US BR coverage > 5000 rows, re-run pooled FR+US+SEC grid and confirm A1 still wins (audit 23 §8 risk register).
- Enable
MULTI_MARKET_RECOS_ENABLEDonly after non-FR T+90 mean return and Wilson 95% CI on WR clear> 0lower bound.
10. Per-market top-10 sample (post-patch, BUY-only)
Generated from /tmp/v5-prelim.json, first run on 30 089 BR rows.
Top-decile composite is dominated by FR cluster signals in the 200M-1B
"Sweet" bucket (consistent with the FR A1 strategy). US top-10 dominated by
SEC P-code purchases on CFO-titled filers, confirms the SEC outperformance
seen in audit 22 (T+90 mean +6.16%, WR 60%) survives the v5 reweighting.
Full table written to /tmp/v5-prelim.json for downstream inspection.
11. Caveats / honesty line
The "best formula" recommended here was validated on the FR slice only (n=17 685 BUY rows with BR data). Non-FR markets inherit FR weights with the single structural exception of CH. The FR non-regression check is the only hard claim. Anything stronger (e.g. "v5 is better on US") requires either (i) BR coverage > 5000 on US OR (ii) walk-forward pooled FR+US over ≥ 3 years, both blocked on infrastructure, not modelling.
Appendix A, grid runner
scripts/quant-v5-harness.mjs (added by this audit). Run:
node --env-file=.env.local scripts/quant-v5-harness.mjs --out /tmp/v5.json
Prints per-market table + top-5 grid winners per market + pooled FR+US + v3 baseline.