Audit Point-in-Time (PIT) — Champs Yahoo Finance sur Company
Date : 2026-05-14 · Périmètre : signals.ts, backtest-compute.ts, winning-strategy.ts, recommendation-engine.ts
Résumé exécutif
18 champs Yahoo Finance sur le modèle Company ont été audités. Tous sont des snapshots actuels : ils reflètent la valeur aujourd'hui, non la valeur au moment du trade historique. Leur usage dans le moteur de scoring (signals.ts) constitue du look-ahead leakage pour toute déclaration antérieure à la dernière mise à jour du champ.
Verdict global : 3 niveaux de risque
| Niveau | Champs | Détail |
|---|---|---|
| HIGH — leakage structurel | marketCap, analystScore, numAnalysts, analystReco, targetMean, targetHigh, targetLow |
Utilisés dans le scoring historique ET dans le filtre de la Winning Strategy |
| MEDIUM — leakage probable | trailingPE, forwardPE, priceToBook, debtToEquity, profitMargin, returnOnEquity, returnOnAssets, freeCashFlow, heldByInsiders, heldByInstitutions, shortRatio |
Utilisés dans le composite bonus du scoring historique |
| LOW / SAFE | currentPrice, fiftyTwoWeekHigh, fiftyTwoWeekLow, fiftyDayAverage, twoHundredDayAverage |
Utilisés dans le composite bonus mais aussi en affichage live ; risque moyen sur le composite |
Inventaire champ par champ
1. marketCap (BigInt, EUR)
| Attribut | Valeur |
|---|---|
| Timestamp de fraîcheur | Company.marketCapAt (gardé pour compat) + Company.financialsAt |
| Usages backtest | signals.ts:496 — pctOfMarketCap = amount / mcap * 100 (score F1, 16 pts) |
| Usages backtest | backtest-compute.ts:302 — sizeLabel(d.company.marketCap) (bucket bySize) |
| Usages backtest | winning-strategy.ts:114-119 — filtre WHERE company.marketCap BETWEEN 200M AND 1B |
| Usages live-only | recommendation-engine.ts:359 — affiché sur la card reco |
| Verdict | LEAKING |
| Magnitude | CRITIQUE — pctOfMarketCap est la feature la plus pondérée du score (16/100 pts). Un insider qui a acheté 0,5% du capital en 2022 sur une petite cap devenue mid-cap en 2026 obtiendra un score différent. Le filtre mid-cap de la Winning Strategy est encore plus sensible : une société classée "mid-cap aujourd'hui" mais "small-cap au moment du trade" sera incluse à tort dans les 380 signaux gagnants. |
2. analystScore (Float, 1.0–5.0)
| Attribut | Valeur |
|---|---|
| Timestamp de fraîcheur | Company.analystAt |
| Usages backtest | signals.ts:597,599 — fundamentalsScore(analystScore, …) (score F2, −2..+4 pts) |
| Usages backtest | signals.ts:603,606 — analystContrarianScore(direction, analystScore, numAnalysts) (score F7, 0..+6 pts) |
| Usages backtest | signals.ts:562,570 — passé à computeComposite(…) pour le sub-bonus upside |
| Verdict | LEAKING |
| Magnitude | HIGH — 10 pts au total (fundamentals + contrarian + composite upside). Le consensus analystes de 2022 est systématiquement différent du snapshot 2026. |
3. numAnalysts (Int)
| Attribut | Valeur |
|---|---|
| Timestamp de fraîcheur | Company.analystAt |
| Usages backtest | signals.ts:603 — gate de analystContrarianScore (require numAnalysts ≥ 3) |
| Usages backtest | signals.ts:571 — gate du composite upside bonus |
| Verdict | LEAKING |
| Magnitude | MEDIUM — une société suivie par 5 analystes en 2026 n'en avait peut-être qu'1 en 2022, ce qui aurait désactivé le bonus contrarian. |
4. targetMean, targetHigh, targetLow (Float)
| Attribut | Valeur |
|---|---|
| Timestamp de fraîcheur | Company.analystAt |
| Usages backtest | signals.ts:567-571 — composite bonus "upside-25pct" / "upside-15pct" |
| Usages live | winning-strategy.ts:63, recommendation-engine.ts:362 — affiché sur la card |
| Verdict | LEAKING (dans signals.ts) / SAFE (dans winning-strategy.ts / reco-engine : affichage seul) |
| Magnitude | HIGH sur les signaux 2022–2024 — les targets analystes varient de ±40% sur 2 ans. |
5. analystReco (String)
| Usages | winning-strategy.ts:63, recommendation-engine.ts:361 — affichage |
| Verdict | SAFE — jamais utilisé dans le calcul du score |
6. trailingPE, debtToEquity (Float)
| Usages backtest | signals.ts:597 — fundamentalsScore(…, trailingPE, debtToEquity) (−2..+4 pts) |
| Usages backtest | signals.ts:574,578 — composite bonus value-combo, quality-combo |
| Verdict | LEAKING |
| Magnitude | MEDIUM — PE et D/E sont volatils sur 4 ans. Impact direct : ±2 pts via fundamentalsScore. |
7. currentPrice, fiftyTwoWeekHigh, fiftyTwoWeekLow, twoHundredDayAverage, fiftyDayAverage (Float)
| Usages backtest | signals.ts:562-582 — composite sub-bonuses "near-52w-low", "near-52w-high", "oversold", "above-ma200" |
| Verdict | LEAKING (dans signals.ts) |
| Magnitude | MEDIUM — ces métriques techniques sont les plus time-sensitive : le prix actuel n'a aucun rapport avec le prix au moment du trade. Un signal "near-52w-low" appliqué à une déclaration de 2022 avec les données 2026 est entièrement faux. Impact composite : 0–4 pts. |
8. forwardPE, priceToBook, profitMargin, returnOnEquity, returnOnAssets, freeCashFlow, heldByInsiders, heldByInstitutions, shortRatio
| Usages backtest | signals.ts:573-580 — composite sub-bonuses value-combo, quality-combo, insider-owned-high, short-squeeze |
| Verdict | LEAKING |
| Magnitude | LOW-MEDIUM — ces fondamentaux varient lentement ; le biais est réel mais moins aigu que les données de prix ou analysts. Impact : 0–4 pts via composite. |
Usages par fichier (récapitulatif)
| Fichier | Champs Yahoo utilisés | Contexte | Verdict global |
|---|---|---|---|
src/lib/signals.ts |
18/18 champs | Moteur de scoring historique (batch) | LEAKING sur tous les champs snapshot |
src/lib/backtest-compute.ts |
marketCap (sizeLabel) |
Segmentation bySize des backtests | LEAKING sur le bucket label |
src/lib/winning-strategy.ts |
marketCap, currentPrice, analystReco, targetMean |
Filtre DB + affichage card | marketCap LEAKING (filtre), reste SAFE (affichage) |
src/lib/recommendation-engine.ts |
marketCap, currentPrice, targetMean, analystReco |
Affichage card + sizeLabel bucket | sizeLabel LEAKING, affichage SAFE |
Fixs livrés
Fix 1 — signals.ts : LEAKAGE comments sur tous les champs snapshot
Ajout de commentaires // LEAKAGE explicites au point d'usage dans scoreDeclarations() pour les champs Yahoo utilisés en scoring historique. Le scoring n'est pas désactivé (breaking change, les scores existants seraient invalidés), mais chaque usage est maintenant documenté avec la magnitude de biais.
Fix 2 — winning-strategy.ts : LEAKAGE comment sur le filtre marketCap
Le filtre company.marketCap BETWEEN 200M AND 1B est le plus critique car il détermine l'inclusion dans les 380 signaux gagnants. Commentaire explicite ajouté.
Fix 3 — Banner /methodologie
Section "Transparence PIT" ajoutée dans la page /methodologie au niveau de la section Yahoo Finance (§1 Sources), bilingue FR/EN.
TODOs — Infrastructure nécessaire
TODO-1 (HIGH, ~2 semaines) : Snapshots marketCap point-in-time
Problème : pctOfMarketCap est recalculé à chaque scoring avec le marketCap actuel. Pour les déclarations historiques, il faudrait marketCap_at_transactionDate.
Solution :
- Créer une table
CompanyMarketCapSnapshot(companyId, date, marketCapEur)alimentée quotidiennement par un cron Yahoov8/chart(cours ×sharesOut). - Modifier
scoreDeclarations()pour lookupMarketCapSnapshot WHERE date = transactionDateavant de fallback surCompany.marketCap. - Backfiller avec
quantity * unitPrice * sharesOutapproximatif depuisPriceHistorysi disponible, sinon marquerpctOfMarketCap = nullavec un flagmcapEstimated = true.
Délai estimé : 1 sem. infrastructure + 1 sem. backfill + re-score complet.
TODO-2 (HIGH, ~1 semaine) : Neutraliser les features composite en scoring historique
Problème : computeComposite() utilise 14 champs snapshot (prix, analyst targets, technicals) pour des trades historiques.
Solution pragmatique : créer computeCompositeHistorical() qui ne retourne que les features non-temporelles (freeCashFlow > 0, heldByInsiders >= 0.2). Désactiver "near-52w-low", "above-ma200", "upside-25pct", "value-combo", "quality-combo", "short-squeeze" pour les déclarations datant de plus de 90 jours.
Délai estimé : 3j code + re-score complet (1h cron).
TODO-3 (MEDIUM, ~3 semaines) : Snapshots analyst consensus PIT
Problème : analystScore, targetMean, numAnalysts utilisés dans fundamentalsScore et analystContrarianScore pour des trades historiques.
Solution : table CompanyAnalystSnapshot(companyId, date, analystScore, targetMean, numAnalysts). Backfill difficile — les données historiques consensus ne sont pas disponibles gratuitement via Yahoo. Alternative : désactiver ces features pour les déclarations > 6 mois.
Délai estimé : 1 sem. si désactivation temporelle ; 3 sem. si vrai backfill.
Plan de mitigation 2 semaines
| Semaine | Actions |
|---|---|
| S1 | (a) Créer cron daily-mcap-snapshot.ts qui insère sharesOut × currentPrice dans CompanyMarketCapSnapshot. (b) Implémenter computeCompositeHistorical() sans features snapshot. (c) Re-scorer toutes les déclarations avec la nouvelle logique. |
| S2 | (d) Backfiller CompanyMarketCapSnapshot sur 4 ans via Yahoo v8/chart (cours historique × sharesOut proxied). (e) Re-calculer pctOfMarketCap PIT pour les 380 signaux gagnants. (f) Re-run le grid search pour vérifier que la Winning Strategy tient toujours. |
Risque connu : si pctOfMarketCap PIT diffère significativement des valeurs actuelles sur les 380 signaux, le win rate et l'alpha déclarés pourraient être révisés à la baisse. C'est le résultat attendu d'un audit honnête.
Conclusion
Le système actuel est honnête sur les données d'entrée (déclarations AMF publiques, horodatées) mais biaisé sur les features d'enrichissement Yahoo (snapshots actuels injectés dans le scoring historique). Le biais le plus grave est le filtre marketCap de la Winning Strategy : il sélectionne rétrospectivement des sociétés qui sont mid-cap aujourd'hui, pas au moment du trade. Cela constitue du survivorship bias partiel couplé à du look-ahead sur la taille.
Les performances déclarées (+16.3% moyen, Sharpe 1.00) restent plausibles — la majeure partie de l'alpha vient de features robustes au leakage (cluster directionnel, rôle, track record Bayésien, délai AMF). Mais l'ampleur exacte du biais est inconnue jusqu'à la livraison de TODO-1.