Audit /recommendations — pertinence des signaux financiers
Date : 2026-05-15 · Auditeur : quant senior · Périmètre : recommendations/page.tsx, recommendation-engine.ts, RecoCard.tsx, RecoHeroCard.tsx, RecoFilteredContent.tsx, winning-strategy.ts (STRATEGY_PROOF n=173). Données live vérifiées via Prisma au moment de l'audit.
TL;DR (5 puces)
- 466 BUY / 236 SELL récupérés au 15/05 ; après dominance + filtres, la page reste alignée sur les chiffres affichés (le compteur
sellCountdu hero ≠ 207 user-side : drift ≤15 %, à vérifier). - Plancher de score trop bas :
signalScore > 0laisse passer 215 signaux BUY avec score < 30 (46 % du flux brut). Le pré-filtre n'élimine pas le bruit ; seul un filtre client (score_min) le masque visuellement. - Score → action illisible : aucun seuil "Strong Buy / Buy / Watch / Pass" sur la page ; un utilisateur voyant 57 ne sait pas si c'est exploitable. Le tier interne (
high≥75 / mid≥55 / low) existe en code mais n'apparaît jamais en clair. - Bug critique de polarité (RecoHeroCard:312) : le hero affiche toujours "Achat / Buy", même quand
item.action === "SELL". Le top SELL est donc présenté comme un achat dans la zone A. - STRATEGY_PROOF refresh 14/05 :
winRate 77 %,sharpe 1.87,n=173. Sans IC95 (±6.3 pts à n=173), ces nombres sont over-précis. La page methodo ne mentionne ni l'IC ni les deux années perdantes (2022 −7.9 %, 2024 −3.7 %).
Matrice de pertinence (10 dimensions)
| # | Dimension | État actuel | Verdict | Fix |
|---|---|---|---|---|
| 1 | Score → action | Score brut 0-100, gauge couleur, aucune zone "Strong Buy / Buy / Watch / Pass" visible | ❌ | Étiquette explicite à côté du gauge : ≥75 STRONG BUY · 60-74 BUY · 45-59 WATCH · <45 PASS. Aligné sur tier code (RecoCard.tsx:116). |
| 2 | Recency & decay | Recency calculée (15 pts, half-life 45j) MAIS jamais affichée séparément. Date pubDate montrée brute. Pas de "Frais / Stale / Périmé" |
⚠️ | Badge recency : Fresh ≤7j / Récent ≤14j / Mûr ≤30j / Périmé >30j. Déjà calculé en displayStalenessPenalty(). |
| 3 | Pipeline pertinence | signalScore: { gt: 0 } admet le score 1, le pre-filter ne coupe qu'à er < 2 && signal < 50. 215/466 BUY < 30. |
❌ | Hausse plancher à signalScore >= 25 côté SQL (engine:556) — élimine 0 signal méritoire, retire le ballast UX. |
| 4 | Cluster weight | Badge "cluster" affiché, +5 pts conviction. MAIS le tri primaire reste recoScore global → cluster pur peut être détrôné par un signal solo à score AMF élevé. Pas de ré-ordonnancement explicite par cluster. |
⚠️ | Boost de tri secondaire (isCluster ? +3 pts) ou affichage dédié "Top cluster signals". Conforme Alldredge-Blank 2019 (+0.9 %/mois). |
| 5 | Sell usefulness | Hero SELL équipoise visuelle 50/50 avec hero BUY ; copy "Top Vente · cessions sur profils baissiers" overstate. Lakonishok-Lee 2001 montre prédictivité SELL ≈ 1/3 vs BUY. Aucun warning explicite. | ❌ | Bandeau warning sur card SELL : "Les ventes d'initiés sont moins prédictives que les achats (Lakonishok 2001)". Réduire la taille visuelle hero-sell à 70 % du hero-buy. |
| 6 | IC sur win rate | Affiche 77 % ou 67 % sans intervalle de confiance. À n=173 (proof) ou n=20-50 (bucket), IC95 ≈ ±6 à ±14 pts. |
❌ | Format 67 % ±5 ou tooltip "n=N · IC95 ±X pts". Wilson score interval ; le shrinkage existe déjà côté point estimate mais l'incertitude n'est jamais exposée. |
| 7 | Tooltip "Pourquoi" | Pas de tooltip "Pourquoi" à proprement parler — EvidenceBullets dans RecoHeroCard génère 4 puces depuis les données. RecoCard standard n'a aucun rationale visible. |
⚠️ | Valider rationales (cf. samples ci-dessous) — 3/5 contiennent une affirmation cluster discutable. Étendre les bullets aux cards non-hero. |
| 8 | Position sizing | Aucune suggestion de taille de position. WINNING_STRATEGY.maxPositionPct=0.08 est defined côté code mais jamais affichée. |
❌ | Ajouter ligne "Pondération Sigma : ~3 % du portefeuille (cap 8 %)" sur HeroCard et fiches conviction ≥75. |
| 9 | Hold window | Hero affiche T+45 / T+60 / T+90 selon score (RecoHeroCard:215) — bonne pratique. MAIS cards standard montrent uniquement "Retour estimé T+90". |
⚠️ | Propager la même règle conviction → holdDays sur RecoCard standard. La méthodologie indique T+45-60 optimal pour ≥75. |
| 10 | Exclusions transparency | ExcludedSignalsPanel affiche raisons réelles (Direction: Sell, Score < N, Out of sector, No cluster). MAIS s'affiche uniquement quand un filtre est actif (!isDefaultState) — sur le default feed (curated 12), les ~660 signaux écartés sont invisibles. |
⚠️ | Toujours afficher un panneau "Sigma a vu X signaux · retenu N · exclu M (raisons)" en bas, même en default. |
Bugs spécifiques (file:line)
src/components/RecoHeroCard.tsx:312— Tag d'action hard-codéAchat / Buyindépendant deitem.action. Si le top signal est une VENTE (cas possible sur tab?dir=sell), il est affiché comme achat. La couleur (--signal-pos-bd) confirme : hero card n'a pas de branche SELL.src/lib/recommendation-engine.ts:556—signalScore: { not: null, gt: 0 }: plancher à 1. Donnée live : 215 BUY avec score 1-29 entrent dans le candidate pool. Le post-filter ne sauve que les caser < 2 && signal < 50, lasses les autres remonter au tri.src/lib/recommendation-engine.ts:583—if (er != null && er < 2 && (decl.signalScore ?? 0) < 50) continue;: la condition estAND(er < 2 ET signal < 50). Un signal er=NULL (cold bucket) passe systématiquement. Devrait êtreer ?? -Infinity < 2 OR signalScore < 25.src/lib/recommendation-engine.ts:638— SELL tri partotalAmountpuispubDate, pas parrecoScore. Un gros nominal sans signal est promu top sell. Incohérent avec BUY (tri parrecoScore).src/components/RecoCard.tsx:307— Tooltip "T+90 · N trades" sans IC. À sampleSize=8 (cold bucket), affiche "67 % T+90" qui est statistiquement non-significatif.src/lib/recommendation-engine.ts:311-315—convictionPts:decl.isCluster +5puispctMcap >0.5 +3puisamount >500k +2capped à 10. Cumul possible 10 (sans cap mid). MaispctOfMarketCapest en pourcentage par déclaration — un cluster de 5 décl chacune 0.6 % gagne le bonus 5 fois si vu individuellement (mais merge a lieu après). À auditer.src/components/RecoHeroCard.tsx:215-221—holdDayscalculé surscoreRoundmaisdeadline = buyDate + 7jest constant. Le "Buy before {deadline}" suggère une urgence faussement uniforme — un signal de 3j vs 28j ont la même fenêtre.src/lib/recommendation-engine.ts:561—take: limit * 8(BUY) et* 6(SELL). Pour limit=60 → 480 / 360 décls retournées avant filtres. Charge DB excessive pour la valeur ajoutée.src/components/RecoFilteredContent.tsx:217—sellCount = allItems.filter(r => r.action === "SELL").lengthmaisallItems=[...rawBuys, ...rawSells]post-dominance. Le compteur affiché sur le toggle bar peut différer du SELL réel post-cross-trade filter.src/lib/recommendation-engine.ts:583— Pré-filtre BUY rejetteer < 2 && signal < 50, mais le seuil 2 % est figé : pour un bucket SELL où le ER négatif est "bon", ce filtre n'a aucun équivalent → SELL accepte n'importe quel ER.
5 rationales vs ground truth DB
Top 5 BUY après tri par signalScore desc au 15/05 :
| # | Rationale potentiel (extrait code) | Ground truth DB | Verdict |
|---|---|---|---|
| 1 | KLEA HOLDING · Raphael Smila PDG · Cluster actif · 0.03 % mcap · 15 272 € | isCluster: true MAIS 3 décl 02-04/03 toutes du même Smila (différents IDs). "Cluster" au sens AMF = ≥2 insiders distincts. Ici 1 seul. |
❌ Faux cluster · drapeau incorrect côté DB (isCluster dérivé par signal computation, à revoir) |
| 2 | LDC · Denis Lambert · Président CS · Cluster · 0.25 % mcap · 5.85 M€ | OK. Lambert est board ; clusterRequired + excludeBoardRole du WINNING_STRATEGY l'exclurait, mais l'engine getBuyRecommendations n'applique pas ces filtres. |
⚠️ Rationale OK mais incohérent avec winning strategy promue ailleurs |
| 3 | HERMES · Matthieu Dumas · Membre Conseil Surveillance · 82 550 € · 0.000037 % mcap | Board-only insider, pctOfMarketCap quasi-nul (3.7e-5 %). N'apporte aucun signal exploitable. |
❌ Signal trash · devrait être exclu (board + < 0.001 % mcap) |
| 4 | KLEA · Smila · 62 336 € · 0.11 % mcap · doublon | Même remarque que #1 — déduplication par merge mais 3 cards Smila restent dans le candidate pool, biaisant le tri | ❌ |
| 5 | KLEA · Smila · 59 134 € · doublon | idem | ❌ |
3/5 rationales suspectes. Le "cluster" KLEA est en réalité un single-insider DCA (achats répétés du même PDG) : pertinent comme signal, mais mal étiqueté "cluster".
10 améliorations recommandées (par impact)
- (Impact +++) Filtrer côté SQL
signalScore >= 25→ coupe 215 signaux bruit dès le pool, allège DB et UX (engine:556). - (Impact +++) Fix bug polarité hero SELL : conditionner couleur + texte du tag sur
item.action(RecoHeroCard:312). - (Impact ++) Étiquette "STRONG BUY / BUY / WATCH" à côté du gauge — résout dim. 1 sans nouveaux concepts.
- (Impact ++) Afficher IC95 sur win rate :
67 % ±5 (n=124). Wilson formula trivial à implémenter. - (Impact ++) Warning SELL "moins prédictif" sur hero + cards SELL (référence Lakonishok-Lee 2001 dans tooltip).
- (Impact ++) Distinguer "cluster vrai" (≥2 insiders) vs "DCA solo" (même insider, plusieurs ops). KLEA n'est pas un cluster.
- (Impact +) Position sizing : ligne "~3 % portefeuille" sur cards conviction ≥75, exposer
maxPositionPctdu STRATEGY. - (Impact +) Trier SELL par
recoScoreau lieu detotalAmount(engine:638), cohérence avec BUY. - (Impact +) Propager
holdDaysdynamique aux RecoCard standard (pas seulement hero). - (Impact +) Toujours afficher un mini-bandeau "X vus · N retenus · M exclus" en default state — transparency même sans filtre.
"Don't ship" — claims overstated
- ❌ "Recommandations actionnables" (H1) — terme à risque AMF / MAR. Préférer "Signaux d'initiés" ou "Lectures actionnables" ; "recommandation" en droit financier français a un sens précis (RTO/conseil régulé).
- ❌ "Top Achat / Top Vente" sur le hero — frame déclaratif d'action. Acceptable si disclaimer renforcé en haut de page (pas en bas).
- ❌ "Acheter avant le {date} · fenêtre T+45" (HeroCard:355) — injonction directe à l'achat. Reformulation : "Fenêtre d'opportunité : achat antérieur au {date} dans le backtest" ou "Si pertinent pour votre stratégie : action avant {date}".
- ⚠️ "Win rate 77 %" sans IC ni mention des 2 années perdantes (2022, 2024 dans STRATEGY_PROOF) — sélection biaisée. Toujours annoncer
77 % ±6 · bat le CAC 2/4 annéesou présenter year-by-year. - ⚠️ "Cluster actif — N dirigeants ont acheté" (HeroCard:112) — vrai uniquement si N >1 distinct. Sur KLEA : 1 insider, étiquette fallacieuse.
- ⚠️ "Live · mis à jour quotidiennement" (masthead) — vrai (sync AMF quotidien) mais bucketStats
revalidate: 1800(30 min) et reco caches 5 min — il y a un décalage potentiel jusqu'à 30 min entre nouveau backtest et display. À documenter. - ⚠️ MethodologyCard "30 pts Signal AMF v3" — la note de bas
patternBonus neutralized(signes inversés en FR) n'est pas mentionnée. Tant que la re-tune est pending, ajouter un asterisque "composante pattern temporairement neutralisée — re-calibration en cours". - ⚠️ Hero badge "Top conviction" —
recoScorepeut atteindre 75 par cumul (signal 18 + winRate 22 + ret 18 + recency 12 + conviction 5) sans aucune composante "haute conviction" véritable. La gauge "gold tier" devrait exigersignalPts >= 22 AND convictionPts >= 7pour mériter "Top conviction".
Résumé 100 mots
La page /recommendations est structurellement solide (suspense streaming, dominance filter, freemium gate cohérents) mais souffre de quatre faiblesses de pertinence : un plancher de score SQL trop bas (215/466 BUY < 30), un bug de polarité hero qui affiche tout signal en "Achat", l'absence d'étiquettes Strong Buy / Watch / Pass alors que le tier existe en code, et une équipoise visuelle BUY ≈ SELL qui contredit Lakonishok-Lee 2001. À court terme : fix polarité hero, hausser plancher SQL à 25, ajouter IC95 sur win rate, distinguer cluster vrai vs DCA solo. À moyen terme : repositionner SELL comme "signal secondaire" et tempérer la copy "actionable / acheter avant".