From 04aa32a61a925033dda157518e3c9eb045847b47 Mon Sep 17 00:00:00 2001 From: jsboige Date: Sun, 4 Oct 2026 04:57:45 +0200 Subject: [PATCH] feat(qc,#18901): Corrective-AI ch. 08-02 -- primaire Breedon-Ranaldo + meta-etiquetage - main.py : algorithme parametre (fenetres 03-09/11-15 ET, swap_sides, meta GBM subsample=0.7, garde history vide aux jours fermes) - research.ipynb : quantbook execute sur QC Cloud (690 fenetres, walk-forward x8, DM Ledoit-Wolf, rapport de biais) -- verdict regle C : NO BEATS sur les deux conventions - 20 backtests moteur : magnitude non reproduite (Sharpe -0.881 vs 0.88 annonce), le meta reduit la perte et divise le DD mais edge < 2 sigma - BOOK_MAPPING : ligne 08-02 STUB -> COVERED Co-Authored-By: Claude Sonnet 5.5 --- .../QuantConnect/BOOK_MAPPING.md | 2 +- .../projects/Corrective-AI/README.en.md | 85 ++-- .../projects/Corrective-AI/README.md | 90 ++-- .../projects/Corrective-AI/main.py | 222 ++++++++ .../projects/Corrective-AI/research.ipynb | 480 ++++++++++++++++++ 5 files changed, 765 insertions(+), 114 deletions(-) create mode 100644 MyIA.AI.Notebooks/QuantConnect/projects/Corrective-AI/main.py create mode 100644 MyIA.AI.Notebooks/QuantConnect/projects/Corrective-AI/research.ipynb diff --git a/MyIA.AI.Notebooks/QuantConnect/BOOK_MAPPING.md b/MyIA.AI.Notebooks/QuantConnect/BOOK_MAPPING.md index 6cfff9f010..72114cbc3a 100644 --- a/MyIA.AI.Notebooks/QuantConnect/BOOK_MAPPING.md +++ b/MyIA.AI.Notebooks/QuantConnect/BOOK_MAPPING.md @@ -134,7 +134,7 @@ Les projets [RL-DQN-Trading](projects/RL-DQN-Trading/) et [Reinforcement-Learnin | # | Exemple du livre | Projet(s) du dépôt | Statut | Statut QC | Remarque | |---|------------------|--------------------|--------|-----------|----------| | 01 | Conditional Portfolio Optimization Applied | [Portfolio-Optimization-ML](projects/Portfolio-Optimization-ML/), [QC-Py-21-Portfolio-Optimization-ML](Python/QC-Py-21-Portfolio-Optimization-ML.ipynb) | PARTIAL | Recherche-phase | optimisation de portefeuille sans le service PredictNow.ai du livre (API payante) | -| 02 | Application of Corrective Artificial Intelligence Applied | [Corrective-AI](projects/Corrective-AI/) | STUB | Stub | portage suivi par [#18901](https://github.com/jsboige/CoursIA/issues/18901) | +| 02 | Application of Corrective Artificial Intelligence Applied | [Corrective-AI](projects/Corrective-AI/) | COVERED | Backteste | portage [#18901](https://github.com/jsboige/CoursIA/issues/18901) : primaire Breedon-Ranaldo + méta-étiquetage sans predictnow.ai (payant) | --- diff --git a/MyIA.AI.Notebooks/QuantConnect/projects/Corrective-AI/README.en.md b/MyIA.AI.Notebooks/QuantConnect/projects/Corrective-AI/README.en.md index ba7aa7d7cb..d5a18b34a2 100644 --- a/MyIA.AI.Notebooks/QuantConnect/projects/Corrective-AI/README.en.md +++ b/MyIA.AI.Notebooks/QuantConnect/projects/Corrective-AI/README.en.md @@ -1,69 +1,46 @@ -# Corrective AI / Meta-Labeling (Ch08-02) +# Corrective AI — EUR/USD intraday seasonality + meta-labeling (Ch. 08-02) -**Type:** Stub (planned exercise, code files not yet created) +**Hands-On AI Trading**, chapter 08-02 — faithful port of the "Application of Corrective Artificial Intelligence Applied" episode. Tracked by [#18901](https://github.com/jsboige/CoursIA/issues/18901). French README is the primary documentation. -**Hands-On AI Trading**, Chapter 8-02 — Meta-Labeling with Corrective AI +## Mechanism (the book) -## Objective +1. **Primary strategy** — EUR/USD intraday seasonality of Breedon and Ranaldo (2012): **sell during European business hours** (03:00–09:00 ET), **buy during US business hours** (11:00–15:00 ET), flat otherwise. Out of sample (Oct. 2021 – Jan. 2023, 1-minute EBS bars) the book measures a **0.88 Sharpe** for the primary alone. +2. **Corrective AI** — the book corrects the primary trade by trade with a gradient boosting model of more than one hundred predictors through the **paid** predictnow.ai API: announced Sharpe 1.29 (+0.41) over the same period. -Implement a meta-labeling approach where a primary model generates trading signals and a secondary corrective model filters out false positives, improving precision while maintaining recall. +## This port -## Approach +The predictnow.ai API is paid and proprietary: this port implements the corrective **principle** without a paid dependency, via **meta-labeling** (López de Prado, *Advances in Financial Machine Learning*, 2018): -### Primary Model: SMA Crossover -- Fast SMA (20-day) vs Slow SMA (60-day) -- Signal: bullish when fast > slow * 1.02, bearish when fast < slow * 0.98 -- Universe: SPY, TLT, GLD (multi-asset diversification) +- the trade unit is one window (European or American) of the primary; +- at each entry, a walk-forward-trained classifier (gradient boosted trees, `sklearn`) predicts the probability that the window will be a winner; +- the window is taken only above the threshold (`sizing: filter`) — or sized by probability (`sizing: scale`); +- declared gap vs the book: **~16 ex-ante predictors** (minute returns/volatilities, window side, weekday, rolling primary win-rates, streak) against **>100** at predictnow.ai. -### Corrective Filter -The secondary model evaluates whether to trust the primary signal based on: -- **Trend strength**: Gap between fast and slow SMA (normalized) -- **Volatility regime**: Price relative to EMA (50-day volatility proxy) -- **Rules**: - - Strong trend (>3% gap) -> trust signal - - Moderate trend + low volatility -> trust signal - - Weak trend + high volatility -> reject signal (stay flat) +## Parameters (backtests without recompilation) -### Meta-Labeling Concept -- Primary model labels: +1 (long), -1 (short), 0 (flat) -- Secondary model labels: 1 (trust primary), 0 (reject primary) -- Net position = primary_direction * corrective_approval -- Expected: higher precision (fewer false signals), similar or lower recall +| Parameter | Default | Role | +|---|---|---| +| `start` / `end` | 2021-10-01 / 2023-01-31 | window (`YYYY-MM-DD`) | +| `use_meta` | `true` | `false` = primary-only leg | +| `threshold` | `0.5` | probability threshold (`filter`) | +| `seed` | `0` | classifier seed (multi-seed, rule §C) | +| `retrain_days` | `21` | walk-forward retraining cadence | +| `min_train` | `120` | minimum past windows before the first meta trade | +| `sizing` | `filter` | `filter` or `scale` | +| `swap_sides` | `false` | `true` = Breedon-Ranaldo paper convention (buy EU / sell US), the inverse convention tested empirically | -## QC Project +Declared choices: labels on gross entry→exit returns (minute mids; engine fees on actual fills only); no entry during the walk-forward warm-up (`min_train`) nor during the 5-day data warm-up; windows with no usable minute history (e.g. January 1st, market closed) are skipped. -| Field | Value | -|-------|-------| -| Project ID | 30800636 | -| Organization | Trading Firm QC-Course | -| Period | 2018-01-01 to 2024-12-31 | -| Starting Capital | $1,000,000 | +## Results -## Backtest Results (v1) +See `research.ipynb` (book window: primary vs meta, multi-seed ≥ 4, Diebold-Mariano test, reported bias — `BEATS` / `NO BEATS` / `INCONCLUSIVE` verdict per rule §C) and the 2018-2026 long-window robustness table. Metrics come from the QC backtests cited in the delivery PR body. -| Metric | Value | -|--------|-------| -| Sharpe Ratio | -0.151 | -| CAGR | 2.22% | -| Max Drawdown | 11.3% | -| Net Profit | +16.64% | -| Win Rate | 70% | -| Total Orders | 1242 | -| Total Fees | $2,673.78 | +## History -### Analysis -- 70% win rate confirms the corrective filter improves signal quality -- Negative Sharpe indicates risk-adjusted returns are suboptimal -- Average loss (-0.18%) exceeds average win (0.12%) — position sizing and exit timing need improvement -- The multi-asset approach (SPY/TLT/GLD) provides diversification but the SMA crossover lag hurts in sideways markets +The exploratory v1 of this folder (SMA crossover on SPY/TLT/GLD + rule-based corrective filter, backtest Sharpe −0.151 on the same QC project 30800636, April 2026) was a planned-exercise stub that did **not** match the actual chapter content; it is superseded by this port. QC Cloud project `30800636` (Corrective-AI-Ch08) is reused. -## Files +## References -- `main.py` — QC algorithm with primary signal + corrective filter -- `research.ipynb` — SMA crossover computation, corrective filter design, signal reduction analysis - -## Reference - -- Hands-On AI Trading, Chapter 8-02 -- De Prado, M. (2018), "Advances in Financial Machine Learning" — Meta-labeling concept -- Corrective AI: secondary model trained on primary model's prediction errors +- Breedon, T. & Ranaldo, A. (2012), "Intraday Patterns in FX Returns and Order Flow" — EUR/USD intraday seasonality. +- López de Prado, M. (2018), *Advances in Financial Machine Learning* — meta-labeling, ch. 3. +- *Hands-On AI Trading* (Jared Broad), ch. 08-02. diff --git a/MyIA.AI.Notebooks/QuantConnect/projects/Corrective-AI/README.md b/MyIA.AI.Notebooks/QuantConnect/projects/Corrective-AI/README.md index 12cc2f0442..d24a093ec5 100644 --- a/MyIA.AI.Notebooks/QuantConnect/projects/Corrective-AI/README.md +++ b/MyIA.AI.Notebooks/QuantConnect/projects/Corrective-AI/README.md @@ -1,74 +1,46 @@ -# Corrective AI / Meta-Labeling (Ch08-02) +# Corrective AI — saisonnalité intrajournalière EUR/USD + méta-étiquetage (Ch. 08-02) -**Type :** Stub (exercice planifié, fichiers de code non encore créés) +**Hands-On AI Trading**, chapitre 08-02 — port fidèle de l'épisode « Application of Corrective Artificial Intelligence Applied ». Suivi par [#18901](https://github.com/jsboige/CoursIA/issues/18901). -**Hands-On AI Trading**, Chapitre 8-02 — Meta-Labeling avec Corrective AI +## Mécanisme (le livre) -## Objectif +1. **Stratégie primaire** — saisonnalité intrajournalière EUR/USD de Breedon et Ranaldo (2012) : **vente pendant les heures ouvrées européennes** (03:00–09:00 ET), **achat pendant les heures ouvrées américaines** (11:00–15:00 ET), plat sinon. Le livre mesure, hors échantillon (oct. 2021 – janv. 2023, barres 1 minute EBS), un **Sharpe 0,88** pour la primaire seule. +2. **Corrective AI** — le livre corrige la primaire trade par trade avec un gradient boosting de plus de cent prédicteurs via l'API **payante** predictnow.ai : Sharpe annoncé 1,29 (+0,41) sur la même période. -Implémenter une approche de meta-labeling où un modèle primaire génère des signaux de trading et un modèle correctif secondaire filtre les faux positifs, améliorant la précision tout en préservant le rappel. +## Ce port -## Approche +L'API predictnow.ai est payante et propriétaire : ce port implémente le **principe** correctif sans dépendance payante, par **méta-étiquetage** (López de Prado, *Advances in Financial Machine Learning*, 2018) : -### Modèle primaire : croisement de SMA +- l'unité de trade = une fenêtre (européenne ou américaine) de la primaire ; +- à chaque entrée, un classifieur (gradient boosting d'arbres, `sklearn`) entraîné **walk-forward** sur les fenêtres passées prédit la probabilité que la fenêtre soit gagnante ; +- la fenêtre n'est prise qu'au-delà du seuil (`sizing: filter`) — ou dimensionnée par la probabilité (`sizing: scale`) ; +- écart déclaré au livre : **~16 prédicteurs ex-ante** (rendements/volatilités minute, côté de la fenêtre, jour de semaine, win-rates glissants de la primaire, série) contre **>100** chez predictnow.ai. -- SMA rapide (20 jours) vs SMA lente (60 jours) -- Signal : haussier quand rapide > lente * 1.02, baissier quand rapide < lente * 0.98 -- Univers : SPY, TLT, GLD (diversification multi-actifs) +## Paramètres (backtests sans recompilation) -### Filtre correctif +| Paramètre | Défaut | Rôle | +|---|---|---| +| `start` / `end` | 2021-10-01 / 2023-01-31 | fenêtre (format `YYYY-MM-DD`) | +| `use_meta` | `true` | `false` = jambe primaire seule | +| `threshold` | `0.5` | seuil de probabilité (`filter`) | +| `seed` | `0` | graine du classifieur (multi-seed règle §C) | +| `retrain_days` | `21` | cadence de ré-entraînement walk-forward | +| `min_train` | `120` | fenêtres passées minimales avant le premier trade méta | +| `sizing` | `filter` | `filter` ou `scale` | +| `swap_sides` | `false` | `true` = convention du papier Breedon-Ranaldo (achat UE / vente US), convention inverse testée empiriquement | -Le modèle secondaire évalue s'il faut faire confiance au signal primaire sur la base de : +Choix déclarés : labels sur rendements bruts entrée→sortie (mid-minute, frais moteur à l'exécution réelle uniquement) ; pas d'entrée pendant le warm-up walk-forward (`min_train`) ni pendant les 5 jours de warm-up de données ; fenêtre sans historique minute exploitable (ex. 1er janvier, marché fermé) sautée. -- **Force de la tendance** : écart entre SMA rapide et lente (normalisé) -- **Régime de volatilité** : prix relatif à l'EMA (proxy de volatilité 50 jours) -- **Règles** : - - Tendance forte (écart >3 %) -> confiance dans le signal - - Tendance modérée + faible volatilité -> confiance dans le signal - - Tendance faible + forte volatilité -> rejet du signal (rester à plat) +## Résultats -### Concept de meta-labeling +Voir `research.ipynb` (fenêtre du livre : primaire vs méta, multi-seed ≥ 4, test de Diebold-Mariano, biais rapporté — verdict `BEATS` / `NO BEATS` / `INCONCLUSIVE` selon la règle §C) et le tableau de robustesse fenêtre longue 2018-2026. Les métriques sont celles des backtests QC cités dans le corps de la PR de livraison. -- Étiquettes du modèle primaire : +1 (long), -1 (short), 0 (plat) -- Étiquettes du modèle secondaire : 1 (faire confiance au primaire), 0 (rejeter le primaire) -- Position nette = direction_primaire * approbation_corrective -- Attendu : précision plus élevée (moins de faux signaux), rappel similaire ou inférieur +## Historique -## Projet QC +La v1 exploratoire de ce dossier (croisement SMA sur SPY/TLT/GLD + filtre correctif à règles, backtest Sharpe −0,151 sur le même projet QC 30800636, avril 2026) était un stub d'exercice planifié qui ne correspondait **pas** au contenu réel du chapitre ; elle est remplacée par ce port. Le projet QC Cloud `30800636` (Corrective-AI-Ch08) est réutilisé. -| Champ | Valeur | -|-------|--------| -| Project ID | 30800636 | -| Organisation | Trading Firm QC-Course | -| Période | 2018-01-01 à 2024-12-31 | -| Capital de départ | $1 000 000 | +## Références -## Résultats du backtest (v1) - -| Métrique | Valeur | -|----------|--------| -| Sharpe Ratio | -0.151 | -| CAGR | 2.22 % | -| Max Drawdown | 11.3 % | -| Net Profit | +16.64 % | -| Win Rate | 70 % | -| Total Orders | 1242 | -| Total Fees | $2 673.78 | - -### Analyse - -- Un win rate de 70 % confirme que le filtre correctif améliore la qualité des signaux -- Un Sharpe négatif indique des rendements ajustés au risque sous-optimaux -- La perte moyenne (-0.18 %) dépasse le gain moyen (0.12 %) — le sizing de position et le timing de sortie doivent être améliorés -- L'approche multi-actifs (SPY/TLT/GLD) apporte de la diversification mais le retard du croisement SMA pénalise en marché latéral - -## Fichiers - -- `main.py` — algorithme QC avec signal primaire + filtre correctif -- `research.ipynb` — calcul du croisement SMA, conception du filtre correctif, analyse de la réduction des signaux - -## Référence - -- Hands-On AI Trading, Chapitre 8-02 -- De Prado, M. (2018), « Advances in Financial Machine Learning » — concept de meta-labeling -- Corrective AI : modèle secondaire entraîné sur les erreurs de prédiction du modèle primaire +- Breedon, T. & Ranaldo, A. (2012), « Intraday Patterns in FX Returns and Order Flow » — saisonnalité intrajournalière EUR/USD. +- López de Prado, M. (2018), *Advances in Financial Machine Learning* — méta-étiquetage, ch. 3. +- *Hands-On AI Trading* (Jared Broad), ch. 08-02. diff --git a/MyIA.AI.Notebooks/QuantConnect/projects/Corrective-AI/main.py b/MyIA.AI.Notebooks/QuantConnect/projects/Corrective-AI/main.py new file mode 100644 index 0000000000..89c859049a --- /dev/null +++ b/MyIA.AI.Notebooks/QuantConnect/projects/Corrective-AI/main.py @@ -0,0 +1,222 @@ +# region imports +from AlgorithmImports import * +import numpy as np +# endregion + +# qc-research #18901 : Corrective AI (Hands-On AI Trading, ch. 08-02). +# Strategie primaire : saisonnalite intrajournaliere EUR/USD de Breedon et +# Ranaldo (2012) -- vente pendant les heures ouvrees europeennes (03:00-09:00 +# ET), achat pendant les heures ouvrees americaines (11:00-15:00 ET), plat +# sinon. Le livre (OOS oct. 2021 - janv. 2023, barres 1 minute EBS) mesure +# Sharpe 0,88 pour la primaire seule. +# Corrective AI : le livre corrige la primaire trade par trade via un gradient +# boosting de +100 predicteurs (API payante predictnow.ai, Sharpe 1,29). Ce +# port implemente le PRINCIPE sans dependance payante, comme l'exige l'issue : +# meta-etiquetage (Lopez de Prado, 2018) -- un classifieur walk-forward +# entraine sur les fenetres passees predit la probabilite que le trade de la +# fenetre soit gagnant, et filtre (ou dimensionne) l'entree en consequence. +# Ecart declare au livre : ~16 predicteurs calculables ex-ante vs >100 chez +# predictnow.ai (proprietaire) ; le verdict BEATS/NO BEATS se mesure sur ce +# port, pas sur la promesse du livre. + + +class CorrectiveAIAlgorithm(QCAlgorithm): + + def initialize(self) -> None: + # Periodes par defaut : fenetre du livre (oct. 2021 - janv. 2023). + # Overridables par parametre (format YYYY-MM-DD) : fenetre longue + # 2018-2026 et multi-seed sans recompilation. + start = self._param_date("start", date(2021, 10, 1)) + end = self._param_date("end", date(2023, 1, 31)) + self.set_start_date(start.year, start.month, start.day) + self.set_end_date(end.year, end.month, end.day) + self.set_cash(100_000) + + self._eurusd = self.add_forex("EURUSD", Resolution.MINUTE, Market.OANDA) + self.set_benchmark(self._eurusd.symbol) + self.set_warm_up(timedelta(days=5)) + + self._use_meta = self.get_parameter("use_meta", "true") == "true" + self._threshold = float(self.get_parameter("threshold", "0.5")) + self._seed = int(self.get_parameter("seed", "0")) + self._retrain_days = int(self.get_parameter("retrain_days", "21")) + self._min_train = int(self.get_parameter("min_train", "120")) + self._sizing = self.get_parameter("sizing", "filter") # filter | scale + + # Fenetres : (heure d'entree ET, heure de sortie ET, cote). + # Convention par defaut = transcription de l'issue #18901 (vente UE, + # achat US). swap_sides=true teste la convention du papier Breedon- + # Ranaldo (achat UE, vente US) : mesuree en mirror du livre sur nos + # donnees (Sharpe -0,881 vs +0,88 annonces), a trancher empiriquement. + swap = self.get_parameter("swap_sides", "false") == "true" + sign = -1.0 if swap else 1.0 + self._windows = [(3, 9, -1.0 * sign), (11, 15, 1.0 * sign)] + + # Buffers walk-forward du meta-modele. + self._x_buf: list[list[float]] = [] + self._y_buf: list[int] = [] + self._entry_features: list[list[float]] = [] + self._entry_price: list[float] = [] + self._model = None + self._last_fit = datetime.min + + # Comptes pour le rapport de fin d'execution. + self._n_windows = 0 + self._n_taken = 0 + self._n_skipped = 0 + self._n_refits = 0 + + for entry_h, exit_h, _ in self._windows: + self.schedule.on( + self.date_rules.every_day(self._eurusd.symbol), + self.time_rules.at(entry_h, 0), + self._make_entry(entry_h)) + self.schedule.on( + self.date_rules.every_day(self._eurusd.symbol), + self.time_rules.at(exit_h, 0), + self._make_exit(entry_h)) + + # -- Parametrage ------------------------------------------------------ + + def _param_date(self, name: str, default: date) -> date: + raw = self.get_parameter(name) + return ( + datetime.strptime(raw, "%Y-%m-%d").date() + if raw else default) + + # -- Ordonnancement --------------------------------------------------- + + def _make_entry(self, entry_hour: int): + side = next(s for h, _, s in self._windows if h == entry_hour) + + def enter() -> None: + # Le calendrier every_day tire aussi le dimanche (forex ferme) : + # garde semaine uniquement, comme le livre (jours ouvrables). + if self.is_warming_up or self.time.weekday() > 4: + return + features = self._compute_features(side) + if features is None: + # Fenetre sans historique minute exploitable (ex. 1er janvier, + # marche ferme, warmup sans donnees pre-start) : sautee. + return + take, weight = self._meta_decision(features) + self._n_windows += 1 + if not take: + self._n_skipped += 1 + else: + self._n_taken += 1 + self.set_holdings(self._eurusd.symbol, side * weight) + # La fenetre est journalisee meme si sautee : le meta-modele + # apprend des fenetres passees, prises ou non (le label mesure + # ce que la primaire AURAIT fait, pas ce que le filtre a fait). + self._entry_features.append(features) + self._entry_price.append(float( + self.securities[self._eurusd.symbol].price)) + return enter + + def _make_exit(self, entry_hour: int): + def exit_window() -> None: + if self.is_warming_up or self.time.weekday() > 4: + return + if self._entry_price: + entry = self._entry_price.pop(0) + features = self._entry_features.pop(0) + exit_price = float( + self.securities[self._eurusd.symbol].price) + if entry > 0 and exit_price > 0: + side = next( + s for h, _, s in self._windows if h == entry_hour) + pnl = (exit_price / entry - 1.0) * side + self._x_buf.append(features) + self._y_buf.append(1 if pnl > 0 else 0) + if self.portfolio[self._eurusd.symbol].invested: + self.liquidate(self._eurusd.symbol) + return exit_window + + # -- Meta-etiquetage --------------------------------------------------- + + def _meta_decision(self, features: list[float]) -> tuple[bool, float]: + """Verdict du filtre correctif : (prendre la fenetre, poids).""" + if not self._use_meta: + return True, 1.0 + self._maybe_refit() + if self._model is None: + # Warm-up walk-forward : pas d'entree tant que le meta-modele + # n'a pas min_train fenetres passees (declare dans le README). + return False, 0.0 + p = self._model.predict_proba([features])[0][1] + if self._sizing == "scale": + return p >= 0.5, min(1.0, max(0.05, 2.0 * p - 1.0)) + return p >= self._threshold, 1.0 + + def _maybe_refit(self) -> None: + if (self.time - self._last_fit).days < self._retrain_days: + return + if len(self._y_buf) < self._min_train: + return + from sklearn.ensemble import GradientBoostingClassifier + # sub-sample 0.7 (stochastic GBM, Friedman 2002) : la graine pilote + # le sous-echantillonnage -- sinon le classifieur est deterministe et + # le multi-seed de la regle C n'a aucune variance a mesurer. + self._model = GradientBoostingClassifier( + n_estimators=80, max_depth=3, subsample=0.7, + random_state=self._seed) + self._model.fit(self._x_buf, self._y_buf) + self._last_fit = self.time + self._n_refits += 1 + + # -- Predicteurs -------------------------------------------------------- + + def _compute_features(self, side: float) -> list[float]: + """~16 predicteurs ex-ante : la divergence vs les >100 du livre est + declaree en tete de fichier (predictnow.ai = payant, proprietaire).""" + hist = self.history(self._eurusd.symbol, 240, Resolution.MINUTE) + if hist is None or hist.empty or "close" not in hist.columns: + return None + values = hist["close"].dropna().values + if len(values) == 0: + return None + if len(values) < 240: + # Filet defensif (le warm-up de 5 jours couvre deja 240 barres) : + # borner a gauche au premier close evade l'index negatif bouclant. + values = np.concatenate( + [np.full(240 - len(values), values[0]), values]) + logs = np.log(values) + rets = np.diff(logs) + + def ret(w: int) -> float: + return float(logs[-1] - logs[-1 - w]) + + def vol(w: int) -> float: + return float(np.std(rets[-w:])) + + labels = self._y_buf + win10 = np.mean(labels[-10:]) if labels else 0.0 + win20 = np.mean(labels[-20:]) if labels else 0.0 + pnl10 = [ + (1 if lab else -1) for lab in labels[-10:]] + streak = 0 + if labels: + last = labels[-1] + for lab in reversed(labels): + if lab != last: + break + streak += 1 + + return [ + ret(1), ret(5), ret(15), ret(30), ret(60), ret(120), + vol(15), vol(30), vol(60), + side, float(self.time.weekday()), ret(240 - 1), + float(win10), float(win20), float(np.mean(pnl10)) if pnl10 else 0.0, + float(streak), + ] + + # -- Rapport ------------------------------------------------------------- + + def on_end_of_algorithm(self) -> None: + self.log( + "corrective-ai: fenetres=" + str(self._n_windows) + + " prises=" + str(self._n_taken) + + " sautees=" + str(self._n_skipped) + + " refits=" + str(self._n_refits) + + " echantillon_train=" + str(len(self._y_buf))) diff --git a/MyIA.AI.Notebooks/QuantConnect/projects/Corrective-AI/research.ipynb b/MyIA.AI.Notebooks/QuantConnect/projects/Corrective-AI/research.ipynb new file mode 100644 index 0000000000..602a529ed1 --- /dev/null +++ b/MyIA.AI.Notebooks/QuantConnect/projects/Corrective-AI/research.ipynb @@ -0,0 +1,480 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Corrective AI — saisonnalité EUR/USD + méta-étiquetage (recherche)\n", + "\n", + "**Hands-On AI Trading**, ch. 08-02 — port [#18901](https://github.com/jsboige/CoursIA/issues/18901).\n", + "Ce quantbook est le volet recherche du projet [`main.py`](main.py) : il dérive\n", + "la série par fenêtre de la stratégie primaire, réplique le méta-étiquetage\n", + "walk-forward multi-seed sur les mêmes données minute, et rend le verdict de la\n", + "règle §C (edge ≥ 2σ cross-seed **et** Diebold-Mariano < 0,05).\n", + "\n", + "**Mécanisme du livre** :\n", + "1. **Primaire** — saisonnalité intrajournalière EUR/USD (Breedon & Ranaldo 2012) :\n", + " vente 03:00–09:00 ET (heures ouvrées européennes), achat 11:00–15:00 ET\n", + " (heures ouvrées américaines). Le livre mesure Sharpe 0,88 en OOS\n", + " (oct. 2021 – janv. 2023, barres 1 minute EBS).\n", + "2. **Corrective AI** — le livre filtre chaque trade via un gradient boosting\n", + " de >100 prédicteurs (API payante predictnow.ai, Sharpe annoncé 1,29). Ici le\n", + " **principe** est porté sans dépendance payante : méta-étiquetage (López de\n", + " Prado 2018) avec ~16 prédicteurs ex-ante, GBM stochastique\n", + " (`subsample=0.7` — la graine pilote le sous-échantillonnage, sinon le\n", + " classifieur est déterministe et le multi-seed n'a aucune variance à mesurer).\n", + "\n", + "**Choix déclarés** : labels sur rendements bruts fenêtre (mid minute, frais\n", + "moteur exclus du label, appliqués aux fills réels dans les backtests) ;\n", + "fenêtres du lundi au vendredi ; équivalent du warm-up 5 jours = fenêtres à\n", + "prédicteurs incomplets écartées." + ], + "id": "cell-00" + }, + { + "cell_type": "code", + "metadata": {}, + "execution_count": 1, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": "config: seeds [0, 1, 7, 42] | fenetres [(3, 9, -1.0), (11, 15, 1.0)] | retrain 21 j | min_train 120\n" + } + ], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "from datetime import datetime\n", + "\n", + "SEEDS = [0, 1, 7, 42]\n", + "WINDOWS = [(3, 9, -1.0), (11, 15, 1.0)] # (entree ET, sortie ET, cote)\n", + "RETRAIN_DAYS, MIN_TRAIN, THRESHOLD = 21, 120, 0.5\n", + "ANNUAL_WINDOWS = 2 * 252 # ~2 fenetres/jour ouvre\n", + "START, END = datetime(2021, 10, 1), datetime(2023, 1, 31)\n", + "print(\"config: seeds\", SEEDS, \"| fenetres\", WINDOWS,\n", + " \"| retrain\", RETRAIN_DAYS, \"j | min_train\", MIN_TRAIN)" + ], + "id": "cell-01" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Données\n", + "\n", + "EUR/USD minute (OANDA) sur la fenêtre du livre. Le flux du quantbook est le\n", + "mid des barres — le livre utilise des barres EBS : l'ordre de grandeur du\n", + "Sharpe primaire est comparable, pas la décimale." + ], + "id": "cell-02" + }, + { + "cell_type": "code", + "metadata": {}, + "execution_count": 2, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": "barres minute: 494,241 | 2021-10-01 00:01:00 -> 2023-01-31 00:00:00\n" + }, + { + "output_type": "execute_result", + "metadata": {}, + "execution_count": 2, + "data": { + "text/plain": "time\n2023-01-30 23:58:00 1.084535\n2023-01-30 23:59:00 1.084585\n2023-01-31 00:00:00 1.084620\nName: close, dtype: float64" + } + } + ], + "source": [ + "# Sur le Lab QC, qb est injecte par l'environnement ; sur un kernel\n", + "# python3 brut (execution WS), il faut le construire explicitement.\n", + "if \"qb\" not in dir():\n", + " from QuantConnect.Research import QuantBook\n", + " qb = QuantBook()\n", + "eurusd = qb.add_forex(\"EURUSD\", Resolution.MINUTE, Market.OANDA)\n", + "df = qb.history(eurusd.symbol, START, END, Resolution.MINUTE)\n", + "if isinstance(df.index, pd.MultiIndex):\n", + " df = df.loc[eurusd.symbol]\n", + "closes = df[\"close\"].dropna()\n", + "print(f\"barres minute: {len(closes):,} | \"\n", + " f\"{closes.index[0]} -> {closes.index[-1]}\")\n", + "closes.tail(3)" + ], + "id": "cell-03" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Stratégie primaire — fenêtres et rendements\n", + "\n", + "Une fenêtre = un trade : entrée au premier bar de l'heure d'ouverture, sortie\n", + "au dernier bar avant l'heure de fermeture (miroir des événements planifiés de\n", + "`main.py`). `ret_primary` est le rendement signé côté (vente = −Δprix)." + ], + "id": "cell-04" + }, + { + "cell_type": "code", + "metadata": {}, + "execution_count": 3, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": "fenetres: 690 | win-rate: 0.520 | rendement moyen/fenetre: 0.51 bp\nSharpe ann. convention transcription (vente UE): 0.364\nSharpe ann. convention papier (achat UE): -0.364\nreference du livre (OOS, EBS): Sharpe 0.88 | backtest moteur correspondant cite dans la PR\n" + }, + { + "output_type": "execute_result", + "metadata": {}, + "execution_count": 3, + "data": { + "text/plain": " date hour side ret_primary\n0 2021-10-01 3 -1.0 -0.001424\n1 2021-10-01 11 1.0 0.000112\n2 2021-10-04 3 -1.0 -0.002339\n3 2021-10-04 11 1.0 0.000013\n4 2021-10-05 3 -1.0 -0.000194\n5 2021-10-05 11 1.0 0.000423" + } + } + ], + "source": [ + "rows = []\n", + "for d, day in closes.groupby(closes.index.date):\n", + " if d.weekday() > 4:\n", + " continue\n", + " for h_in, h_out, side in WINDOWS:\n", + " w = day.between_time(f\"{h_in:02d}:00:00\", f\"{h_out - 1:02d}:59:00\")\n", + " if len(w) < 60: # fenetre incomplete (jour ferie / gap de flux)\n", + " continue\n", + " rows.append({\n", + " \"date\": d, \"hour\": h_in, \"side\": side,\n", + " \"t_entry\": w.index[0],\n", + " \"ret_primary\": (w.iloc[-1] / w.iloc[0] - 1.0) * side,\n", + " })\n", + "win = pd.DataFrame(rows)\n", + "n = len(win)\n", + "mean_r, std_r = win[\"ret_primary\"].mean(), win[\"ret_primary\"].std(ddof=1)\n", + "sharpe_primary = mean_r / std_r * np.sqrt(ANNUAL_WINDOWS)\n", + "wr = (win[\"ret_primary\"] > 0).mean()\n", + "# Convention inverse (papier Breedon-Ranaldo : achat UE / vente US) :\n", + "# meme fenetres, cotes opposes -> serie = negation exacte.\n", + "win[\"ret_swap\"] = -win[\"ret_primary\"]\n", + "sharpe_swap = -sharpe_primary\n", + "print(f\"fenetres: {n} | win-rate: {wr:.3f} | \"\n", + " f\"rendement moyen/fenetre: {mean_r * 1e4:.2f} bp\")\n", + "print(f\"Sharpe ann. convention transcription (vente UE): {sharpe_primary:.3f}\")\n", + "print(f\"Sharpe ann. convention papier (achat UE): {sharpe_swap:.3f}\")\n", + "print(f\"reference du livre (OOS, EBS): Sharpe 0.88 | backtest moteur \"\n", + " f\"correspondant cite dans la PR\")\n", + "win[[\"date\", \"hour\", \"side\", \"ret_primary\"]].head(6)" + ], + "id": "cell-05" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Prédicteurs ex-ante (miroir de `main.py._compute_features`)\n", + "\n", + "10 prédicteurs de marché dérivés des 240 dernières minutes (log-rendements\n", + "1/5/15/30/60/120 min, volatilités réalisées 15/30/60 min, drift 240 min) +\n", + "6 prédicteurs séquentiels (côté, jour de semaine, win-rates glissants 10/20 de\n", + "la primaire, PnL moyen 10, série). Les labels passés ne sont lus qu'après\n", + "clôture de leur fenêtre — aucune fuite." + ], + "id": "cell-06" + }, + { + "cell_type": "code", + "metadata": {}, + "execution_count": 4, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": "predicteurs de marche: 10 | barres couvertes: 494,002\n" + }, + { + "output_type": "execute_result", + "metadata": {}, + "execution_count": 4, + "data": { + "text/plain": " ret1 ret5 ret15 ret30 ret60 \\\ntime \n2023-01-30 23:58:00 -0.000023 0.000065 0.000286 0.000304 0.000055 \n2023-01-30 23:59:00 0.000046 0.000138 0.000286 0.000401 0.000124 \n2023-01-31 00:00:00 0.000032 0.000129 0.000272 0.000512 0.000143 \n\n ret120 vol15 vol30 vol60 ret240 \ntime \n2023-01-30 23:58:00 -0.000631 0.000041 0.000068 0.000067 -0.000811 \n2023-01-30 23:59:00 -0.000553 0.000041 0.000067 0.000067 -0.000742 \n2023-01-31 00:00:00 -0.000521 0.000040 0.000065 0.000067 -0.000691 " + } + } + ], + "source": [ + "logs = pd.Series(np.log(closes.values), index=closes.index)\n", + "lr = logs.diff()\n", + "feat = pd.DataFrame(index=closes.index)\n", + "for w in (1, 5, 15, 30, 60, 120):\n", + " feat[f\"ret{w}\"] = logs.diff(w)\n", + "for w in (15, 30, 60):\n", + " feat[f\"vol{w}\"] = lr.rolling(w).std()\n", + "feat[\"ret240\"] = logs.diff(239)\n", + "feat = feat.dropna() # equiv. warm-up 5 jours : fenetres sans historique 240 min\n", + "print(f\"predicteurs de marche: {feat.shape[1]} | barres couvertes: {len(feat):,}\")\n", + "feat.tail(3)" + ], + "id": "cell-07" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Méta-étiquetage walk-forward multi-seed\n", + "\n", + "Boucle strictement chronologique identique à `main.py` : à chaque entrée, le\n", + "modèle (entraîné sur les fenêtres **déjà clôturées**, ré-entraîné tous les\n", + "21 jours, premier trade après 120 fenêtres) prédit P(fenêtre gagnante) ;\n", + "fenêtre prise si p ≥ 0,5. Rendement méta = rendement primaire si pris, 0 sinon." + ], + "id": "cell-08" + }, + { + "cell_type": "code", + "metadata": {}, + "execution_count": 5, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": "=== convention transcription (primaire 0.364) ===\n seed fenetres_evaluees prises sharpe_meta rendement_bp sharpe_primaire\n 0 689 320 0.674919 0.693756 0.363858\n 1 689 310 -0.005232 -0.005160 0.363858\n 7 689 306 0.049500 0.048963 0.363858\n 42 689 307 -0.023997 -0.024214 0.363858\n=== convention papier (primaire -0.364) ===\n seed fenetres_evaluees prises sharpe_meta rendement_bp sharpe_primaire\n 0 689 260 -0.356503 -0.311843 -0.363858\n 1 689 261 0.173919 0.165720 -0.363858\n 7 689 276 0.067397 0.064425 -0.363858\n 42 689 279 -0.408206 -0.381735 -0.363858\n" + } + ], + "source": [ + "from sklearn.ensemble import GradientBoostingClassifier\n", + "\n", + "def run_meta(ret_col):\n", + " \"\"\"Walk-forward miroir de main.py sur la serie de fenetres donnee.\"\"\"\n", + " per_seed, meta_rets = [], {}\n", + " for seed in SEEDS:\n", + " X_buf, y_buf = [], []\n", + " model, last_fit = None, None\n", + " rets_meta, taken = [], 0\n", + " for _, row in win.iterrows():\n", + " if row[\"t_entry\"] not in feat.index:\n", + " continue\n", + " f_market = feat.loc[row[\"t_entry\"]].to_numpy()\n", + " y_arr = np.array(y_buf)\n", + " win10 = y_arr[-10:].mean() if len(y_arr) else 0.0\n", + " win20 = y_arr[-20:].mean() if len(y_arr) else 0.0\n", + " pnl10 = (2 * win10 - 1) if len(y_arr) else 0.0\n", + " streak, last = 0, None\n", + " for lab in reversed(y_buf):\n", + " if last is None:\n", + " last = lab\n", + " if lab != last:\n", + " break\n", + " streak += 1\n", + " feats = np.concatenate([f_market, [\n", + " row[\"side\"], float(row[\"date\"].weekday()),\n", + " win10, win20, pnl10, float(streak)]])\n", + " due = (last_fit is None) or (\n", + " (row[\"date\"] - last_fit).days >= RETRAIN_DAYS)\n", + " if due and len(y_buf) >= MIN_TRAIN:\n", + " model = GradientBoostingClassifier(\n", + " n_estimators=80, max_depth=3, subsample=0.7,\n", + " random_state=seed)\n", + " model.fit(X_buf, y_buf)\n", + " last_fit = row[\"date\"]\n", + " take = False\n", + " if model is not None:\n", + " take = model.predict_proba([feats])[0][1] >= THRESHOLD\n", + " if take:\n", + " taken += 1\n", + " rets_meta.append(row[ret_col] if take else 0.0)\n", + " # la fenetre se clot ce jour : son label n'alimente que les suivantes\n", + " X_buf.append(feats)\n", + " y_buf.append(1 if row[ret_col] > 0 else 0)\n", + " m = np.array(rets_meta)\n", + " meta_rets[seed] = m\n", + " per_seed.append({\n", + " \"seed\": seed,\n", + " \"fenetres_evaluees\": len(m),\n", + " \"prises\": taken,\n", + " \"sharpe_meta\": m.mean() / m.std(ddof=1) * np.sqrt(ANNUAL_WINDOWS),\n", + " \"rendement_bp\": m.mean() * 1e4,\n", + " })\n", + " return pd.DataFrame(per_seed), meta_rets\n", + "\n", + "runs = {}\n", + "for conv, col, base in [(\"transcription\", \"ret_primary\", sharpe_primary),\n", + " (\"papier\", \"ret_swap\", sharpe_swap)]:\n", + " df_seed, meta_rets = run_meta(col)\n", + " df_seed[\"sharpe_primaire\"] = base\n", + " runs[conv] = {\"df\": df_seed, \"meta\": meta_rets, \"base\": base,\n", + " \"base_series\": win[col].to_numpy()[-len(meta_rets[SEEDS[0]]):]}\n", + " print(f\"=== convention {conv} (primaire {base:.3f}) ===\")\n", + " print(df_seed.to_string(index=False))" + ], + "id": "cell-09" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Validation règle §C — edge cross-seed + Diebold-Mariano\n", + "\n", + "- **edge σ** : (Sharpe méta moyen − Sharpe primaire) / écart-type des Sharpes\n", + " méta inter-seeds — la dispersion mesure la stochasticité du filtre.\n", + "- **DM (Ledoit-Wolf 2008)** : test apparié de différence de Sharpe avec SE HAC\n", + " Newey-West (référence dépôt : `m11c_sharpe_test`), appliqué à la série\n", + " seed-ensemble (moyenne inter-seeds des rendements par fenêtre) vs la\n", + " primaire. Unilatéral : H₀ S_méta ≤ S_primaire.\n", + "- **Verdict** : `BEATS` exige edge ≥ 2σ **et** p < 0,05 ; `NO BEATS` si l'un\n", + " échoue nettement ; `INCONCLUSIVE` sinon. Jamais « promising »." + ], + "id": "cell-10" + }, + { + "cell_type": "code", + "metadata": {}, + "execution_count": 6, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": "=== convention transcription ===\nSharpe primaire: 0.364 | meta par seed: 0.675, -0.005, 0.049, -0.024\nmoyenne meta: 0.174 +/- 0.336 | edge = -0.57 sigma | DM t=-0.334 p_unilat=0.6310\nVERDICT regle C: NO BEATS\n=== convention papier ===\nSharpe primaire: -0.364 | meta par seed: -0.357, 0.174, 0.067, -0.408\nmoyenne meta: -0.131 +/- 0.294 | edge = 0.79 sigma | DM t=0.375 p_unilat=0.3539\nVERDICT regle C: NO BEATS\n" + } + ], + "source": [ + "from scipy import stats as sps\n", + "\n", + "def _hac_cov(z, q):\n", + " t, k = z.shape\n", + " z = z - z.mean(axis=0, keepdims=True)\n", + " omega = (z.T @ z) / t\n", + " for h in range(1, q + 1):\n", + " w = 1.0 - h / (q + 1.0)\n", + " gh = (z[h:].T @ z[:-h]) / t\n", + " omega += w * (gh + gh.T)\n", + " return omega\n", + "\n", + "def ledoit_wolf_sharpe_diff_se(r_a, r_b):\n", + " r_a = np.asarray(r_a, float).ravel()\n", + " r_b = np.asarray(r_b, float).ravel()\n", + " t = len(r_a)\n", + " if t < 30 or len(r_a) != len(r_b):\n", + " return (np.nan,) * 4\n", + " mu_a, mu_b = r_a.mean(), r_b.mean()\n", + " s2a, s2b = r_a.var(), r_b.var()\n", + " sa, sb = np.sqrt(s2a), np.sqrt(s2b)\n", + " if sa < 1e-12 or sb < 1e-12:\n", + " return (np.nan,) * 4\n", + " ua, ub = r_a - mu_a, r_b - mu_b\n", + " z = np.column_stack([ua, ua * ua - s2a, ub, ub * ub - s2b])\n", + " q = max(1, int(np.floor(t ** 0.25)))\n", + " omega = _hac_cov(z, q)\n", + " grad = np.array([1 / sa, -mu_a / (2 * sa ** 3), -1 / sb, mu_b / (2 * sb ** 3)])\n", + " se = float(np.sqrt(grad @ omega @ grad / t))\n", + " return mu_a / sa, mu_b / sb, mu_a / sa - mu_b / sb, se\n", + "\n", + "verdicts = {}\n", + "for conv in runs:\n", + " r = runs[conv]\n", + " meta_sharpes = r[\"df\"][\"sharpe_meta\"].to_numpy()\n", + " mean_meta, std_meta = meta_sharpes.mean(), meta_sharpes.std(ddof=1)\n", + " edge_sigma = ((mean_meta - r[\"base\"]) / std_meta\n", + " if std_meta > 1e-12 else np.inf)\n", + " ens = np.mean([r[\"meta\"][s_] for s_ in SEEDS], axis=0)\n", + " primary_matched = win[\"ret_primary\" if conv == \"transcription\"\n", + " else \"ret_swap\"].to_numpy()[-len(ens):]\n", + " sa_, sb_, sdiff, se = ledoit_wolf_sharpe_diff_se(ens, primary_matched)\n", + " t_stat = sdiff / se if se and se == se else np.nan\n", + " p_one = 1 - sps.norm.cdf(t_stat) if t_stat == t_stat else np.nan\n", + " verdicts[conv] = (\"BEATS\" if (edge_sigma >= 2.0 and p_one < 0.05) else\n", + " (\"NO BEATS\" if (mean_meta <= r[\"base\"] or p_one >= 0.05)\n", + " else \"INCONCLUSIVE\"))\n", + " print(f\"=== convention {conv} ===\")\n", + " print(f\"Sharpe primaire: {r['base']:.3f} | meta par seed: \"\n", + " + \", \".join(f\"{v:.3f}\" for v in meta_sharpes))\n", + " print(f\"moyenne meta: {mean_meta:.3f} +/- {std_meta:.3f} | \"\n", + " f\"edge = {edge_sigma:.2f} sigma | DM t={t_stat:.3f} \"\n", + " f\"p_unilat={p_one:.4f}\")\n", + " print(f\"VERDICT regle C: {verdicts[conv]}\")" + ], + "id": "cell-11" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6. Rapport de biais (règle §C-7)\n", + "\n", + "Un edge porté par le biais (rendement moyen signé) plutôt que par la précision\n", + "(win-rate / ratio gain-perte) se déclare comme tel." + ], + "id": "cell-12" + }, + { + "cell_type": "code", + "metadata": {}, + "execution_count": 7, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": "=== biais convention transcription ===\n jambe rendement_moyen_bp win_rate ratio_gain_perte ecart_type_bp\n transcription / primaire 0.528863 0.521045 0.962403 31.327894\n transcription / meta-s0 (prises) 1.493744 0.534375 0.980623 33.872119\n transcription / meta-s1 (prises) -0.011468 0.516129 0.936638 33.033489\n transcription / meta-s7 (prises) 0.110248 0.526144 0.908623 33.352305\ntranscription / meta-s42 (prises) -0.054343 0.514658 0.939031 33.966191\n=== biais convention papier ===\n jambe rendement_moyen_bp win_rate ratio_gain_perte ecart_type_bp\n papier / primaire -0.528863 0.478955 1.039066 31.327894\n papier / meta-s0 (prises) -0.826384 0.511538 0.890158 31.999422\n papier / meta-s1 (prises) 0.437476 0.505747 1.011242 34.796148\n papier / meta-s7 (prises) 0.160828 0.525362 0.915382 33.943386\npapier / meta-s42 (prises) -0.942708 0.491039 0.960432 33.019054\n\nlecture: si le Sharpe meta monte sans que le win-rate des fenetres prises ne monte, l'edge est un artefact de selection, pas de precision.\n" + } + ], + "source": [ + "def leg_stats(r):\n", + " r = np.asarray(r, float)\n", + " wins, losses = r[r > 0], r[r < 0]\n", + " return {\n", + " \"rendement_moyen_bp\": r.mean() * 1e4,\n", + " \"win_rate\": (r > 0).mean() if len(r) else np.nan,\n", + " \"ratio_gain_perte\": (wins.mean() / abs(losses.mean())\n", + " if len(wins) and len(losses) else np.nan),\n", + " \"ecart_type_bp\": r.std(ddof=1) * 1e4,\n", + " }\n", + "\n", + "for conv in runs:\n", + " r = runs[conv]\n", + " rows_bias = [{\"jambe\": f\"{conv} / primaire\",\n", + " **leg_stats(r[\"base_series\"])}]\n", + " for seed in SEEDS:\n", + " m = r[\"meta\"][seed]\n", + " taken_mask = m != 0.0\n", + " rows_bias.append({\n", + " \"jambe\": f\"{conv} / meta-s{seed} (prises)\",\n", + " **leg_stats(m[taken_mask]),\n", + " })\n", + " print(f\"=== biais convention {conv} ===\")\n", + " print(pd.DataFrame(rows_bias).to_string(index=False))\n", + "print(\"\\nlecture: si le Sharpe meta monte sans que le win-rate des fenetres \"\n", + " \"prises ne monte, l'edge est un artefact de selection, pas de precision.\")" + ], + "id": "cell-13" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 7. Limites et robustesse\n", + "\n", + "- **Fenêtre longue 2018-2026** : la robustesse hors fenêtre du livre est mesurée\n", + " par les backtests moteur (frais réels, fills, equity curve) — matrice\n", + " `long-*` rapportée dans le corps de la PR de livraison.\n", + "- **Écarts déclarés au livre** : flux mid OANDA vs barres EBS ; ~16 prédicteurs\n", + " vs >100 (predictnow.ai, payant) ; fenêtre minute ≠ barres du livre exactes.\n", + "- Le Sharpe « par fenêtre » de ce notebook n'est pas le Sharpe equity-curve\n", + " des backtests : bases différentes, mêmes ordres de grandeur attendus." + ], + "id": "cell-14" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (QC Cloud research)", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file