diff --git a/MyIA.AI.Notebooks/QuantConnect/Python/QC-Py-41-PaperTrading-IBKR.ipynb b/MyIA.AI.Notebooks/QuantConnect/Python/QC-Py-41-PaperTrading-IBKR.ipynb
index 293236b04f..050838da5f 100644
--- a/MyIA.AI.Notebooks/QuantConnect/Python/QC-Py-41-PaperTrading-IBKR.ipynb
+++ b/MyIA.AI.Notebooks/QuantConnect/Python/QC-Py-41-PaperTrading-IBKR.ipynb
@@ -42,7 +42,7 @@
"tags": []
},
"source": [
- "[<< Sommaire QC](../README.md) | [Précédent : QC-Py-40-PaperTrading-Binance <<](./QC-Py-40-PaperTrading-Binance.ipynb)\n",
+ "[<< Sommaire QC](../README.md) | [Précédent : QC-Py-40-PaperTrading-Binance <<](./QC-Py-40-PaperTrading-Binance.ipynb) | [Suivant : QC-Py-42-Alpha-Mining-Evolution >>](./QC-Py-42-Alpha-Mining-Evolution.ipynb)\n",
"\n",
"# QC-Py-41 : Paper Trading IBKR - SP500 Momentum\n",
"\n",
@@ -1397,4 +1397,4 @@
},
"nbformat": 4,
"nbformat_minor": 5
-}
\ No newline at end of file
+}
diff --git a/MyIA.AI.Notebooks/QuantConnect/Python/QC-Py-42-Alpha-Mining-Evolution.ipynb b/MyIA.AI.Notebooks/QuantConnect/Python/QC-Py-42-Alpha-Mining-Evolution.ipynb
new file mode 100644
index 0000000000..4856f0b0ba
--- /dev/null
+++ b/MyIA.AI.Notebooks/QuantConnect/Python/QC-Py-42-Alpha-Mining-Evolution.ipynb
@@ -0,0 +1,1737 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "6e14756c",
+ "metadata": {},
+ "source": [
+ "[<< Sommaire QC](../README.md) | [Précédent : QC-Py-41-PaperTrading-IBKR <<](./QC-Py-41-PaperTrading-IBKR.ipynb)\n",
+ "\n",
+ "# QC-Py-42 - Alpha mining par evolution\n",
+ "\n",
+ "**Boucle from-scratch d'evolution d'expressions alpha**, et pont entre la serie `Search/`\n",
+ "(algorithmes evolutionnaires) et la serie `QuantConnect` (validation financiere).\n",
+ "\n",
+ "## Pourquoi ce carnet\n",
+ "\n",
+ "Le depot mine ses alphas **a la main**. La litterature recente automatise cette etape par\n",
+ "**evolution d'expressions** : R&D-Agent(Q), Microsoft Research Asia, arXiv:2505.15155, archive au\n",
+ "gisement bibliographique `Trading/`. Le depot enseigne deja les algorithmes evolutionnaires\n",
+ "(`deap`, `Search-05-GeneticAlgorithms.ipynb`) mais ne les a **jamais** appliques a la finance.\n",
+ "\n",
+ "Mesure de l'angle mort au 2026-10-09, grep prose (`*.py` / `*.md` / `*.yml` / `*.cs`, sorties\n",
+ "`.ipynb` exclues) : `RD-Agent` **0**, `AlphaGen` **0**, `alpha mining` **0**.\n",
+ "\n",
+ "## Ce que ce carnet fait\n",
+ "\n",
+ "1. Construit un panier crypto depuis les donnees **locales** du depot (10 actifs) : aucune\n",
+ " dependance reseau, l'execution est reproductible. La plage **2018-2026** est celle des\n",
+ " fichiers ; la fenetre reellement exploitee, apres intersection des dates communes, est\n",
+ " **mesuree en §1** et vaut environ cinq ans - c'est elle qui alimente tous les chiffres.\n",
+ "2. Evolue des **expressions alpha** (arbres d'operateurs sur des features de base) avec `deap`.\n",
+ "3. Confronte le meilleur individu a une **baseline de facteurs fixes** (momentum, reversal,\n",
+ " volatilite, volume) - le temoin negatif sans lequel « l'evolution gagne » ne veut rien dire.\n",
+ "4. Mesure hors-echantillon en **walk-forward a 5 blocs**, sur **4 seeds** (0 / 1 / 7 / 42), avec un\n",
+ " test de **Diebold-Mariano** sur une perte de precision (`mse`) **et un critere directionnel**\n",
+ " explicite : une difference significative mais **defavorable** n'est pas une victoire.\n",
+ "5. Rend un **verdict honnete** : `BEATS` / `NO BEATS` / `INCONCLUSIVE`.\n",
+ "\n",
+ "> La jambe « mini-boucle LLM » (proposer / executer / apprendre, facon R&D-Agent reduite) est\n",
+ "> **hors de ce carnet** : elle demande un acces modele et un budget d'evaluation distincts.\n",
+ "> Elle est suivie par #20041.\n",
+ "\n",
+ "**Duree d'execution** : quelques minutes sur CPU (population et generations volontairement\n",
+ "bornees pour un carnet pedagogique)."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "342bf6c7",
+ "metadata": {},
+ "source": [
+ "## 1. Chargement du panier\n",
+ "\n",
+ "Les donnees viennent du depot (`QuantConnect/datasets/yfinance/crypto_panier/`). On ne fait\n",
+ "**aucun forward-fill** : un trou de cotation n'est pas un prix. L'intersection des dates communes\n",
+ "est donc prise telle quelle, ce qui raccourcit la periode au plus tardif des demarrages."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "2883d0e3",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-10-09T11:40:22.319835Z",
+ "iopub.status.busy": "2026-10-09T11:40:22.319565Z",
+ "iopub.status.idle": "2026-10-09T11:40:23.747832Z",
+ "shell.execute_reply": "2026-10-09T11:40:23.746349Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "panier : D:\\dev\\CoursIA-20041-alpha\\MyIA.AI.Notebooks\\QuantConnect\\datasets\\yfinance\\crypto_panier\n",
+ "csv vus : 10\n"
+ ]
+ }
+ ],
+ "source": [
+ "import os, glob, time, warnings\n",
+ "import numpy as np\n",
+ "import pandas as pd\n",
+ "import matplotlib\n",
+ "matplotlib.use(\"Agg\")\n",
+ "import matplotlib.pyplot as plt\n",
+ "\n",
+ "warnings.filterwarnings(\"ignore\")\n",
+ "pd.set_option(\"display.width\", 170)\n",
+ "\n",
+ "SEEDS = (0, 1, 7, 42) # plancher multi-seed de la doctrine §C\n",
+ "ASSETS = [\"BTC-USD\", \"ETH-USD\", \"ADA-USD\", \"AVAX-USD\", \"DOT-USD\",\n",
+ " \"LINK-USD\", \"LTC-USD\", \"MATIC-USD\", \"SOL-USD\", \"XRP-USD\"]\n",
+ "\n",
+ "\n",
+ "def find_panier_dir():\n",
+ " here = os.path.abspath(os.getcwd())\n",
+ " for _ in range(7):\n",
+ " cand = os.path.join(here, \"MyIA.AI.Notebooks\", \"QuantConnect\",\n",
+ " \"datasets\", \"yfinance\", \"crypto_panier\")\n",
+ " if os.path.isdir(cand):\n",
+ " return cand\n",
+ " here = os.path.dirname(here)\n",
+ " raise FileNotFoundError(\"panier introuvable depuis %s\" % os.getcwd())\n",
+ "\n",
+ "\n",
+ "PANIER = find_panier_dir()\n",
+ "print(\"panier :\", PANIER)\n",
+ "print(\"csv vus :\", len(glob.glob(os.path.join(PANIER, \"*.csv\"))))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "213abd10",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-10-09T11:40:23.751581Z",
+ "iopub.status.busy": "2026-10-09T11:40:23.751032Z",
+ "iopub.status.idle": "2026-10-09T11:40:23.897410Z",
+ "shell.execute_reply": "2026-10-09T11:40:23.896082Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "periode : 2020-09-22 -> 2025-03-24\n",
+ "forme : (1645, 10) (jours x actifs)\n",
+ "actifs : ['BTC-USD', 'ETH-USD', 'ADA-USD', 'AVAX-USD', 'DOT-USD', 'LINK-USD', 'LTC-USD', 'MATIC-USD', 'SOL-USD', 'XRP-USD']\n"
+ ]
+ },
+ {
+ "data": {
+ "text/html": [
+ "
\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " BTC-USD | \n",
+ " ETH-USD | \n",
+ " ADA-USD | \n",
+ " AVAX-USD | \n",
+ " DOT-USD | \n",
+ " LINK-USD | \n",
+ " LTC-USD | \n",
+ " MATIC-USD | \n",
+ " SOL-USD | \n",
+ " XRP-USD | \n",
+ "
\n",
+ " \n",
+ " | Date | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " | 2025-03-22 | \n",
+ " 83832.484375 | \n",
+ " 1980.037842 | \n",
+ " 0.700941 | \n",
+ " 19.420124 | \n",
+ " 4.444770 | \n",
+ " 14.226722 | \n",
+ " 91.257545 | \n",
+ " 0.209941 | \n",
+ " 128.484985 | \n",
+ " 2.369824 | \n",
+ "
\n",
+ " \n",
+ " | 2025-03-23 | \n",
+ " 86054.375000 | \n",
+ " 2005.299194 | \n",
+ " 0.709508 | \n",
+ " 19.811661 | \n",
+ " 4.495940 | \n",
+ " 14.435690 | \n",
+ " 91.415352 | \n",
+ " 0.212728 | \n",
+ " 132.599319 | \n",
+ " 2.435493 | \n",
+ "
\n",
+ " \n",
+ " | 2025-03-24 | \n",
+ " 87498.914062 | \n",
+ " 2077.479004 | \n",
+ " 0.730995 | \n",
+ " 21.460428 | \n",
+ " 4.616263 | \n",
+ " 15.089212 | \n",
+ " 93.674507 | \n",
+ " 0.216415 | \n",
+ " 140.869278 | \n",
+ " 2.448162 | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " BTC-USD ETH-USD ADA-USD AVAX-USD DOT-USD LINK-USD LTC-USD MATIC-USD SOL-USD XRP-USD\n",
+ "Date \n",
+ "2025-03-22 83832.484375 1980.037842 0.700941 19.420124 4.444770 14.226722 91.257545 0.209941 128.484985 2.369824\n",
+ "2025-03-23 86054.375000 2005.299194 0.709508 19.811661 4.495940 14.435690 91.415352 0.212728 132.599319 2.435493\n",
+ "2025-03-24 87498.914062 2077.479004 0.730995 21.460428 4.616263 15.089212 93.674507 0.216415 140.869278 2.448162"
+ ]
+ },
+ "execution_count": 2,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "frames = {}\n",
+ "for a in ASSETS:\n",
+ " p = os.path.join(PANIER, \"%s_2018-01-01_2026-05-01.csv\" % a)\n",
+ " if os.path.exists(p):\n",
+ " frames[a] = pd.read_csv(p, parse_dates=[\"Date\"]).set_index(\"Date\").sort_index()\n",
+ "\n",
+ "close = pd.DataFrame({a: f[\"Close\"] for a, f in frames.items()}).sort_index().dropna(how=\"any\")\n",
+ "volume = pd.DataFrame({a: f[\"Volume\"] for a, f in frames.items()}).sort_index().reindex(close.index)\n",
+ "\n",
+ "print(\"periode :\", close.index.min().date(), \"->\", close.index.max().date())\n",
+ "print(\"forme :\", close.shape, \"(jours x actifs)\")\n",
+ "print(\"actifs :\", list(close.columns))\n",
+ "close.tail(3)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "0808220d",
+ "metadata": {},
+ "source": [
+ "### Rendements et cible\n",
+ "\n",
+ "L'alpha cherche a predire le **rendement transversal du lendemain**. On construit donc :\n",
+ "\n",
+ "- `ret` : rendement quotidien simple ;\n",
+ "- `fwd` : cible = `ret` decale d'un jour (ce que l'on cherche a predire).\n",
+ "\n",
+ "Le decalage est la seule precaution anti-fuite : aucune feature ne doit voir le jour qu'elle predit."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "683ab9f5",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-10-09T11:40:23.900861Z",
+ "iopub.status.busy": "2026-10-09T11:40:23.900433Z",
+ "iopub.status.idle": "2026-10-09T11:40:23.947325Z",
+ "shell.execute_reply": "2026-10-09T11:40:23.945877Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "jours exploitables : 1643\n",
+ "rendement quotidien moyen (transversal) : 0.00250\n"
+ ]
+ },
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " BTC-USD | \n",
+ " ETH-USD | \n",
+ " ADA-USD | \n",
+ " AVAX-USD | \n",
+ " DOT-USD | \n",
+ " LINK-USD | \n",
+ " LTC-USD | \n",
+ " MATIC-USD | \n",
+ " SOL-USD | \n",
+ " XRP-USD | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " | mean | \n",
+ " 0.00179 | \n",
+ " 0.00194 | \n",
+ " 0.00271 | \n",
+ " 0.00283 | \n",
+ " 0.00146 | \n",
+ " 0.00179 | \n",
+ " 0.00154 | \n",
+ " 0.00353 | \n",
+ " 0.00433 | \n",
+ " 0.00310 | \n",
+ "
\n",
+ " \n",
+ " | std | \n",
+ " 0.03210 | \n",
+ " 0.04153 | \n",
+ " 0.05405 | \n",
+ " 0.06480 | \n",
+ " 0.05351 | \n",
+ " 0.05455 | \n",
+ " 0.04678 | \n",
+ " 0.06664 | \n",
+ " 0.06364 | \n",
+ " 0.05967 | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " BTC-USD ETH-USD ADA-USD AVAX-USD DOT-USD LINK-USD LTC-USD MATIC-USD SOL-USD XRP-USD\n",
+ "mean 0.00179 0.00194 0.00271 0.00283 0.00146 0.00179 0.00154 0.00353 0.00433 0.00310\n",
+ "std 0.03210 0.04153 0.05405 0.06480 0.05351 0.05455 0.04678 0.06664 0.06364 0.05967"
+ ]
+ },
+ "execution_count": 3,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "ret = close.pct_change().replace([np.inf, -np.inf], np.nan)\n",
+ "vol = volume.replace(0, np.nan)\n",
+ "\n",
+ "# Cible : rendement du lendemain. shift(-1) = anti-fuite.\n",
+ "fwd = ret.shift(-1)\n",
+ "\n",
+ "valid = ret.notna().all(axis=1) & fwd.notna().all(axis=1)\n",
+ "# close est filtre EN MEME TEMPS que ret/vol/fwd. Sans cela, les terminaux batis sur\n",
+ "# close (logclose) gardent les lignes que ret a retirees, et toute expression qui\n",
+ "# melange les deux familles est rejetee par le controle de forme d'evaluate().\n",
+ "close, ret, vol, fwd = close[valid], ret[valid], vol[valid], fwd[valid]\n",
+ "\n",
+ "print(\"jours exploitables :\", len(ret))\n",
+ "print(\"rendement quotidien moyen (transversal) : %.5f\" % ret.mean().mean())\n",
+ "ret.describe().loc[[\"mean\", \"std\"]].round(5)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "09038abb",
+ "metadata": {},
+ "source": [
+ "## 2. Operateurs, terminaux, et evaluation d'une expression\n",
+ "\n",
+ "Une **expression alpha** est un arbre dont :\n",
+ "\n",
+ "- les **feuilles** sont des features de base (prix, volume, rendement) ;\n",
+ "- les **noeuds** sont des operateurs (moyenne glissante, ecart-type glissant, delta, rang\n",
+ " transversal, negation, arithmetique protegee).\n",
+ "\n",
+ "`deap` fournit les operateurs evolutionnaires ; notre travail est de definir le **jeu de\n",
+ "primitives** et la **fonction de fitness**. C'est exactement le pont Search -> QuantConnect qui\n",
+ "n'existait pas.\n",
+ "\n",
+ "Les operateurs temporels prennent une **fenetre** en parametre ; on expose donc des variantes\n",
+ "nommees (`mean5`, `mean20`, ...) plutot qu'un argument libre, ce qui garde l'arbre typable et\n",
+ "l'espace de recherche interprettable."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "29bd90b1",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-10-09T11:40:23.950623Z",
+ "iopub.status.busy": "2026-10-09T11:40:23.950376Z",
+ "iopub.status.idle": "2026-10-09T11:40:25.967854Z",
+ "shell.execute_reply": "2026-10-09T11:40:25.966646Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "primitives : 15\n",
+ "terminaux : ['logclose', 'ret', 'vol']\n"
+ ]
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "ts_rank : ecart max vs oracle pandas = 0 (masques identiques : True)\n"
+ ]
+ }
+ ],
+ "source": [
+ "def zscore_rows(x):\n",
+ " mu = np.nanmean(x, axis=1, keepdims=True)\n",
+ " sd = np.nanstd(x, axis=1, keepdims=True)\n",
+ " return (x - mu) / np.where(sd == 0, np.nan, sd)\n",
+ "\n",
+ "\n",
+ "def rank_rows(x):\n",
+ " out = np.full_like(x, np.nan, dtype=float)\n",
+ " for i in range(x.shape[0]):\n",
+ " row = x[i]\n",
+ " ok = ~np.isnan(row)\n",
+ " if ok.sum() < 2:\n",
+ " continue\n",
+ " order = np.argsort(np.argsort(row[ok]))\n",
+ " out[i, ok] = order / (ok.sum() - 1.0)\n",
+ " return out\n",
+ "\n",
+ "\n",
+ "def ts_mean(x, w):\n",
+ " return pd.DataFrame(x).rolling(w, min_periods=max(2, w // 2)).mean().values\n",
+ "\n",
+ "\n",
+ "def ts_std(x, w):\n",
+ " return pd.DataFrame(x).rolling(w, min_periods=max(2, w // 2)).std().values\n",
+ "\n",
+ "\n",
+ "def ts_delta(x, w):\n",
+ " d = pd.DataFrame(x)\n",
+ " return (d - d.shift(w)).values\n",
+ "\n",
+ "\n",
+ "def ts_rank_ref(x, w):\n",
+ " # Reference pandas : lisible, mais 1,7 s par appel sur un panneau (1643 x 10)\n",
+ " # (rolling.apply passe par une boucle Python). Inutilisable dans une boucle\n",
+ " # evolutionnaire qui evalue des milliers d'expressions -- gardee ici comme\n",
+ " # oracle de la version vectorisee ci-dessous.\n",
+ " return pd.DataFrame(x).rolling(w, min_periods=max(2, w // 2)).apply(\n",
+ " lambda r: (r.iloc[-1] > r).mean(), raw=False).values\n",
+ "\n",
+ "\n",
+ "def ts_rank(x, w):\n",
+ " # Rang (0..1) de la derniere valeur dans sa fenetre glissante de w jours,\n",
+ " # en version vectorisee : 0,020 s au lieu de 1,7 s, meme resultat au bit.\n",
+ " # Deux details de la semantique pandas sont reproduits : (a) pendant\n",
+ " # l'amorcage, la fenetre est plus courte que w et min_periods vaut w // 2 ;\n",
+ " # (b) les NaN de la fenetre comptent dans le denominateur (pandas passe la\n",
+ " # fenetre brute, pas la fenetre nettoyee).\n",
+ " from numpy.lib.stride_tricks import sliding_window_view\n",
+ " T, N = x.shape\n",
+ " m = max(2, w // 2)\n",
+ " out = np.full((T, N), np.nan)\n",
+ " for i in range(m - 1, T):\n",
+ " lo = max(0, i - w + 1)\n",
+ " win = x[lo:i + 1, :]\n",
+ " ok = ~np.isnan(win)\n",
+ " cnt = ok.sum(axis=0)\n",
+ " gt = ((x[i, :] > win) & ok).sum(axis=0)\n",
+ " out[i, :] = np.where(cnt >= m, gt / np.float64(i - lo + 1), np.nan)\n",
+ " return out\n",
+ "\n",
+ "\n",
+ "def sdiv(a, b):\n",
+ " return np.where(np.abs(b) < 1e-12, np.nan, a / b)\n",
+ "\n",
+ "\n",
+ "def neg(x):\n",
+ " return -x\n",
+ "\n",
+ "\n",
+ "def slog(x):\n",
+ " return np.sign(x) * np.log1p(np.abs(x))\n",
+ "\n",
+ "\n",
+ "PRIMITIVES = {\n",
+ " \"add\": (np.add, 2), \"sub\": (np.subtract, 2), \"mul\": (np.multiply, 2),\n",
+ " \"div\": (sdiv, 2), \"neg\": (neg, 1), \"log\": (slog, 1),\n",
+ " \"rank\": (rank_rows, 1), \"zscore\": (zscore_rows, 1),\n",
+ " \"mean5\": (lambda x: ts_mean(x, 5), 1), \"mean20\": (lambda x: ts_mean(x, 20), 1),\n",
+ " \"std5\": (lambda x: ts_std(x, 5), 1), \"std20\": (lambda x: ts_std(x, 20), 1),\n",
+ " \"delta5\": (lambda x: ts_delta(x, 5), 1), \"delta20\": (lambda x: ts_delta(x, 20), 1),\n",
+ " \"tsrank20\": (lambda x: ts_rank(x, 20), 1),\n",
+ "}\n",
+ "\n",
+ "TERMINALS = {\n",
+ " \"logclose\": np.log(close.values),\n",
+ " \"ret\": ret.values,\n",
+ " \"vol\": np.log1p(vol.values),\n",
+ "}\n",
+ "\n",
+ "print(\"primitives :\", len(PRIMITIVES))\n",
+ "print(\"terminaux :\", list(TERMINALS))\n",
+ "\n",
+ "# ts_rank est vectorise pour tenir dans la boucle evolutionnaire. L'equivalence\n",
+ "# avec l'oracle pandas est MESUREE ici, sur les donnees reelles, et non supposee :\n",
+ "# la ligne imprimee doit afficher 0.0.\n",
+ "_r = ts_rank_ref(ret.values, 20)\n",
+ "_f = ts_rank(ret.values, 20)\n",
+ "_common = np.isfinite(_r) & np.isfinite(_f)\n",
+ "print(\"ts_rank : ecart max vs oracle pandas = %.3g (masques identiques : %s)\"\n",
+ " % (np.nanmax(np.abs(_r[_common] - _f[_common])),\n",
+ " np.array_equal(np.isfinite(_r), np.isfinite(_f))))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "0749f2cb",
+ "metadata": {},
+ "source": [
+ "### Interpretreur d'expression\n",
+ "\n",
+ "L'expression est un arbre en notation prefixee (`list`) : `['div', ['delta5', ['logclose']], ['std20', ['ret']]]`.\n",
+ "Un interpreteur recursif l'evalue sur le panneau complet. Evaluer l'arbre soi-meme (plutot que\n",
+ "`deap.gp.compile`) permet de **rejeter proprement** une expression qui produit `NaN` partout, au\n",
+ "lieu de propager une exception au milieu de la boucle evolutionnaire."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "9718d408",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-10-09T11:40:25.970797Z",
+ "iopub.status.busy": "2026-10-09T11:40:25.970548Z",
+ "iopub.status.idle": "2026-10-09T11:40:25.981852Z",
+ "shell.execute_reply": "2026-10-09T11:40:25.980583Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "expression : (div (delta5 logclose) (std20 ret))\n",
+ "forme : (1643, 10)\n",
+ "fini : True\n"
+ ]
+ }
+ ],
+ "source": [
+ "def evaluate(expr, terminals=TERMINALS):\n",
+ " # Evalue une expression prefixee -> ndarray (T, N). None si non evaluable.\n",
+ " if isinstance(expr, str):\n",
+ " return terminals[expr]\n",
+ " head = expr[0]\n",
+ " if head not in PRIMITIVES:\n",
+ " return None\n",
+ " fn, arity = PRIMITIVES[head]\n",
+ " args = []\n",
+ " for k in range(1, arity + 1):\n",
+ " v = evaluate(expr[k], terminals)\n",
+ " if v is None:\n",
+ " return None\n",
+ " args.append(v)\n",
+ " try:\n",
+ " out = fn(*args)\n",
+ " except Exception:\n",
+ " return None\n",
+ " out = np.asarray(out, dtype=float)\n",
+ " if out.shape != (len(close), close.shape[1]):\n",
+ " return None\n",
+ " return out\n",
+ "\n",
+ "\n",
+ "def expression_size(expr):\n",
+ " if isinstance(expr, str):\n",
+ " return 1\n",
+ " return 1 + sum(expression_size(e) for e in expr[1:])\n",
+ "\n",
+ "\n",
+ "def to_prefix(expr):\n",
+ " if isinstance(expr, str):\n",
+ " return expr\n",
+ " return \"(%s %s)\" % (expr[0], \" \".join(to_prefix(e) for e in expr[1:]))\n",
+ "\n",
+ "\n",
+ "# Convention d'ecriture : un TERMINAL est une chaine nue (\"logclose\"), jamais une\n",
+ "# liste a un element ([\"logclose\"]) -- une liste est un noeud d'operateur, et\n",
+ "# [\"logclose\"] serait alors lu comme un appel a une primitive inexistante.\n",
+ "demo = [\"div\", [\"delta5\", \"logclose\"], [\"std20\", \"ret\"]]\n",
+ "d = evaluate(demo)\n",
+ "print(\"expression :\", to_prefix(demo))\n",
+ "print(\"forme :\", None if d is None else d.shape)\n",
+ "print(\"fini :\", None if d is None else bool(np.isfinite(d).any()))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a551dbbf",
+ "metadata": {},
+ "source": [
+ "### Fonction de fitness\n",
+ "\n",
+ "Le fitness est l'**IC transversal moyen** (correlation de rang, jour par jour, entre signal et\n",
+ "rendement du lendemain) sur la **fenetre d'entrainement** uniquement. Un IC moyen eleve et stable\n",
+ "est ce qu'un alpha doit produire.\n",
+ "\n",
+ "Le fitness n'est **jamais** calcule sur la fenetre de test : c'est ce qui rend le verdict\n",
+ "hors-echantillon credible."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "765f07e0",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-10-09T11:40:25.985053Z",
+ "iopub.status.busy": "2026-10-09T11:40:25.984656Z",
+ "iopub.status.idle": "2026-10-09T11:40:25.993960Z",
+ "shell.execute_reply": "2026-10-09T11:40:25.992610Z"
+ }
+ },
+ "outputs": [],
+ "source": [
+ "def signal_to_rank(sig):\n",
+ " return pd.DataFrame(sig).rank(axis=1, pct=True).values\n",
+ "\n",
+ "\n",
+ "def daily_ic(sig, target):\n",
+ " s = signal_to_rank(sig)\n",
+ " t = pd.DataFrame(target).rank(axis=1, pct=True).values\n",
+ " both = np.isfinite(s) & np.isfinite(t)\n",
+ " n = both.sum(axis=1)\n",
+ " keep = n >= 5\n",
+ " if keep.sum() < 20:\n",
+ " return np.nan\n",
+ " s, t = s[keep], t[keep]\n",
+ " sm = s - s.mean(axis=1, keepdims=True)\n",
+ " tm = t - t.mean(axis=1, keepdims=True)\n",
+ " num = (sm * tm).sum(axis=1)\n",
+ " den = np.sqrt((sm ** 2).sum(axis=1) * (tm ** 2).sum(axis=1))\n",
+ " ic = np.where(den == 0, np.nan, num / den)\n",
+ " return float(np.nanmean(ic))\n",
+ "\n",
+ "\n",
+ "def long_short_daily(sig, target, q=0.3):\n",
+ " # Rendement quotidien d'un portefeuille long top-q / short bottom-q, equipondere.\n",
+ " s = signal_to_rank(sig)\n",
+ " k = max(1, int(round(s.shape[1] * q)))\n",
+ " out = np.full(s.shape[0], np.nan)\n",
+ " for i in range(s.shape[0]):\n",
+ " row, tgt = s[i], target[i]\n",
+ " ok = np.isfinite(row) & np.isfinite(tgt)\n",
+ " if ok.sum() < 2 * k:\n",
+ " continue\n",
+ " idx = np.argsort(row[ok])\n",
+ " labs = np.array(tgt[ok])[idx]\n",
+ " out[i] = labs[-k:].mean() - labs[:k].mean()\n",
+ " return out\n",
+ "\n",
+ "\n",
+ "def sharpe(daily):\n",
+ " d = daily[np.isfinite(daily)]\n",
+ " if len(d) < 20 or d.std() == 0:\n",
+ " return np.nan\n",
+ " return float(d.mean() / d.std() * np.sqrt(365))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "478b170c",
+ "metadata": {},
+ "source": [
+ "## 3. Baseline - les facteurs fixes\n",
+ "\n",
+ "Le **temoin negatif**. Quatre facteurs classiques, chacun un signal connu :\n",
+ "\n",
+ "| Facteur | Signal |\n",
+ "|---|---|\n",
+ "| momentum | rendement cumule sur 20 jours |\n",
+ "| reversal | negation du rendement de la veille |\n",
+ "| volatilite | ecart-type glissant 20 jours, negatif (low-vol) |\n",
+ "| volume | rang transversal du volume |\n",
+ "\n",
+ "Si l'evolution ne bat pas ces facteurs, le verdict est `NO BEATS` - et c'est un resultat utile.\n",
+ "\n",
+ "### Le decoupage, et ce que chaque etape lit\n",
+ "\n",
+ "La serie est coupee en **6 intervalles** contigus, qui forment **5 folds** : le fold *i*\n",
+ "entraine sur l'intervalle *i* et teste sur le suivant. Les 6 intervalles sont donc lus, sans\n",
+ "reste : c'est le decoupage **walk-forward a 5 blocs** que la doctrine §C demande (§C en exige\n",
+ "cinq ; un decoupage a 4 folds laissait le dernier intervalle hors de toute lecture).\n",
+ "\n",
+ "La **baseline** est caracterisee sur les 5 folds. Le **champion evolue**, lui, est entraine\n",
+ "**une seule fois** (sur l'intervalle d'entrainement du fold 0) puis re-evalue sur les blocs de\n",
+ "test des folds **1 a 4** - il ne peut pas etre re-entraine par fold, sinon chaque fold aurait\n",
+ "son propre champion et l'ecart inter-seeds ne mesurerait plus la dispersion d'une meme methode.\n",
+ "\n",
+ "La comparaison finale porte donc sur **la meme fenetre pour les deux** : la colonne\n",
+ "`Sharpe OOS (comparaison)` de la table ci-dessous restreint la baseline a ces quatre blocs, ce\n",
+ "qui la rend homometrique du champion. Comparer le champion (4 blocs) a une baseline moyennee\n",
+ "sur 5 blocs comparerait deux fenetres differentes - l'ecart ne voudrait rien dire.\n",
+ "\n",
+ "Enfin, le **facteur de reference** est choisi sur l'**IC d'entrainement** (colonne\n",
+ "`IC train moyen`), jamais sur le Sharpe hors-echantillon de la fenetre de comparaison :\n",
+ "selectionner la reference sur la fenetre meme ou l'on compare reviendrait a choisir le\n",
+ "meilleur adversaire *apres* avoir vu le resultat."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "89cd2fca",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-10-09T11:40:25.996902Z",
+ "iopub.status.busy": "2026-10-09T11:40:25.996528Z",
+ "iopub.status.idle": "2026-10-09T11:40:26.287005Z",
+ "shell.execute_reply": "2026-10-09T11:40:26.285991Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "baseline facteurs fixes (walk-forward 5 blocs ; comparaison sur 4 blocs)\n"
+ ]
+ },
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " IC train moyen | \n",
+ " Sharpe OOS (5 blocs) | \n",
+ " Sharpe OOS (comparaison) | \n",
+ "
\n",
+ " \n",
+ " | facteur | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " | lowvol | \n",
+ " 0.0586 | \n",
+ " -0.8184 | \n",
+ " -0.6359 | \n",
+ "
\n",
+ " \n",
+ " | volume | \n",
+ " 0.0509 | \n",
+ " 0.2739 | \n",
+ " 1.0793 | \n",
+ "
\n",
+ " \n",
+ " | reversal | \n",
+ " 0.0199 | \n",
+ " -0.8939 | \n",
+ " -0.8912 | \n",
+ "
\n",
+ " \n",
+ " | momentum | \n",
+ " 0.0102 | \n",
+ " 1.6890 | \n",
+ " 1.5247 | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " IC train moyen Sharpe OOS (5 blocs) Sharpe OOS (comparaison)\n",
+ "facteur \n",
+ "lowvol 0.0586 -0.8184 -0.6359\n",
+ "volume 0.0509 0.2739 1.0793\n",
+ "reversal 0.0199 -0.8939 -0.8912\n",
+ "momentum 0.0102 1.6890 1.5247"
+ ]
+ },
+ "execution_count": 7,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "FIXED = {\n",
+ " \"momentum\": evaluate([\"delta20\", \"logclose\"]),\n",
+ " \"reversal\": evaluate([\"neg\", \"ret\"]),\n",
+ " \"lowvol\": evaluate([\"neg\", [\"std20\", \"ret\"]]),\n",
+ " \"volume\": evaluate([\"rank\", \"vol\"]),\n",
+ "}\n",
+ "if any(v is None for v in FIXED.values()):\n",
+ " raise RuntimeError(\"facteur fixe non evaluable : %s\"\n",
+ " % [k for k, v in FIXED.items() if v is None])\n",
+ "\n",
+ "# Decoupage walk-forward a 5 blocs : 6 intervalles contigus, tous consommes\n",
+ "# (la doctrine §C exige 5 folds ; a 4 folds le dernier intervalle n'etait lu par personne).\n",
+ "n = len(ret)\n",
+ "edges = np.linspace(0, n, 7).astype(int)\n",
+ "FOLDS = [(edges[i], edges[i + 1], edges[i + 1], edges[i + 2]) for i in range(5)]\n",
+ "\n",
+ "# Le champion est entraine UNE fois sur FOLDS[0] puis re-evalue sur les blocs de test\n",
+ "# de FOLDS[1:] : ce sont ces memes blocs qui portent la comparaison baseline <-> champion,\n",
+ "# d'ou EVAL_FOLDS, utilise aussi bien dans la table ci-dessous qu'a la section 5.\n",
+ "EVAL_FOLDS = FOLDS[1:]\n",
+ "\n",
+ "rows = []\n",
+ "for name, sig in FIXED.items():\n",
+ " if sig is None:\n",
+ " continue\n",
+ " ics, shps, shps_eval = [], [], []\n",
+ " for tr0, tr1, te0, te1 in FOLDS:\n",
+ " ics.append(daily_ic(sig[tr0:tr1], fwd.values[tr0:tr1]))\n",
+ " shps.append(sharpe(long_short_daily(sig[te0:te1], fwd.values[te0:te1])))\n",
+ " for tr0, tr1, te0, te1 in EVAL_FOLDS:\n",
+ " shps_eval.append(sharpe(long_short_daily(sig[te0:te1], fwd.values[te0:te1])))\n",
+ " rows.append({\"facteur\": name,\n",
+ " \"IC train moyen\": np.nanmean(ics),\n",
+ " \"Sharpe OOS (5 blocs)\": np.nanmean(shps),\n",
+ " \"Sharpe OOS (comparaison)\": np.nanmean(shps_eval)})\n",
+ "\n",
+ "baseline = pd.DataFrame(rows).set_index(\"facteur\").round(4)\n",
+ "print(\"baseline facteurs fixes (walk-forward %d blocs ; comparaison sur %d blocs)\"\n",
+ " % (len(FOLDS), len(EVAL_FOLDS)))\n",
+ "# Reference choisie sur l'IC d'ENTRAINEMENT : la ligne de comparaison est celle du facteur\n",
+ "# retenu, visible ici pour que le lecteur puisse verifier qu'il n'a pas ete choisi apres coup.\n",
+ "baseline.sort_values(\"IC train moyen\", ascending=False)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b976c9c1",
+ "metadata": {},
+ "source": [
+ "## 4. Evolution d'expressions avec `deap`\n",
+ "\n",
+ "On evolue des arbres d'expressions. Le fitness est l'IC transversal sur la **fenetre\n",
+ "d'entrainement** (`eaMuPlusLambda`, selection par tournoi). Chaque seed est une execution\n",
+ "independante : c'est le plancher **multi-seed ≥ 4** de la doctrine §C.\n",
+ "\n",
+ "La taille de l'arbre est **bornee** : un alpha doit rester lisible, et une expression obeese\n",
+ "sur-ajuste."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "id": "ad7a43dd",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-10-09T11:40:26.290259Z",
+ "iopub.status.busy": "2026-10-09T11:40:26.289962Z",
+ "iopub.status.idle": "2026-10-09T11:41:00.613812Z",
+ "shell.execute_reply": "2026-10-09T11:41:00.612306Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "seed 0 IC train 0.1273 taille 7 (zscore (mean20 (div (tsrank20 ret) (rank logclose))))\n"
+ ]
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "seed 1 IC train 0.0711 taille 9 (div (div (rank logclose) (std20 ret)) (tsrank20 (std20 vol)))\n"
+ ]
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "seed 7 IC train 0.0956 taille 4 (mean20 (neg (std20 ret)))\n"
+ ]
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "seed 42 IC train 0.0835 taille 7 (zscore (mul vol (div vol (std20 ret))))\n",
+ "duree : 34.3 s\n"
+ ]
+ }
+ ],
+ "source": [
+ "import random\n",
+ "from deap import base, creator, tools, algorithms\n",
+ "\n",
+ "MAX_DEPTH = 3\n",
+ "POP, NGEN, LAMBDA = 60, 8, 60\n",
+ "\n",
+ "PRIM_NAMES = list(PRIMITIVES)\n",
+ "TERM_NAMES = list(TERMINALS)\n",
+ "\n",
+ "\n",
+ "def random_expr(rng, depth=0):\n",
+ " if depth >= MAX_DEPTH or (depth > 0 and rng.random() < 0.35):\n",
+ " return rng.choice(TERM_NAMES)\n",
+ " name = rng.choice(PRIM_NAMES)\n",
+ " arity = PRIMITIVES[name][1]\n",
+ " return [name] + [random_expr(rng, depth + 1) for _ in range(arity)]\n",
+ "\n",
+ "\n",
+ "def mutate_expr(expr, rng):\n",
+ " if rng.random() < 0.3:\n",
+ " return random_expr(rng)\n",
+ " if isinstance(expr, str):\n",
+ " return rng.choice(TERM_NAMES)\n",
+ " idx = rng.randrange(1, len(expr))\n",
+ " expr = list(expr)\n",
+ " expr[idx] = mutate_expr(expr[idx], rng)\n",
+ " return expr\n",
+ "\n",
+ "\n",
+ "if not hasattr(creator, \"FitnessMaxAlpha\"):\n",
+ " creator.create(\"FitnessMaxAlpha\", base.Fitness, weights=(1.0,))\n",
+ "if not hasattr(creator, \"IndividualAlpha\"):\n",
+ " creator.create(\"IndividualAlpha\", list, fitness=creator.FitnessMaxAlpha)\n",
+ "\n",
+ "\n",
+ "def evolution(tr0, tr1, seed):\n",
+ " # Fait evoluer des expressions sur la fenetre d'entrainement [tr0, tr1). Rend le meilleur prefixe.\n",
+ " # evaluate() rend le signal sur TOUTE la serie : la fitness doit le decouper sur la\n",
+ " # meme fenetre que la cible, sinon daily_ic compare (T, N) a (tr1-tr0, N).\n",
+ " rng = random.Random(seed)\n",
+ " # DEAP puise dans le module `random` GLOBAL, pas dans notre generateur local :\n",
+ " # la selection (`selTournament`, qui ne prend aucun argument `rng` en 1.4) ET le\n",
+ " # choix du parent a muter (`algorithms.varOr` -> `random.choice`). Sans ce seed,\n",
+ " # deux executions du meme carnet rendaient des champions differents -- mesure sur\n",
+ " # la fenetre d'entrainement, seed 0 : Sharpe OOS 0.1304 puis 0.6912 -- et la\n",
+ " # validation multi-seed ne voulait plus rien dire. Le seed global est repose a\n",
+ " # chaque appel, donc chaque seed de SEEDS reste independant des autres.\n",
+ " random.seed(seed)\n",
+ " target = fwd.values[tr0:tr1]\n",
+ "\n",
+ " def fit(individual):\n",
+ " v = evaluate(individual)\n",
+ " if v is None:\n",
+ " return (-1.0,)\n",
+ " ic = daily_ic(v[tr0:tr1], target)\n",
+ " if ic is None or not np.isfinite(ic):\n",
+ " return (-1.0,)\n",
+ " return (float(ic) - 0.001 * expression_size(individual),)\n",
+ "\n",
+ " tb = base.Toolbox()\n",
+ " tb.register(\"individual\", tools.initIterate, creator.IndividualAlpha,\n",
+ " lambda: random_expr(rng))\n",
+ " tb.register(\"population\", tools.initRepeat, list, tb.individual)\n",
+ " tb.register(\"evaluate\", fit)\n",
+ " tb.register(\"mate\", lambda a, b: (a, b))\n",
+ " # La mutation doit rendre un IndividualAlpha, pas une list nue : un descendant\n",
+ " # de type list perd son attribut .fitness et selTournament leve\n",
+ " # \"AttributeError: 'list' object has no attribute 'fitness'\". Le noeud racine\n",
+ " # reste une liste (mutate_expr ne remplace qu'un sous-noeud d'indice >= 1).\n",
+ " tb.register(\"mutate\",\n",
+ " lambda ind: (creator.IndividualAlpha(mutate_expr(list(ind), rng)),))\n",
+ " tb.register(\"select\", tools.selTournament, tournsize=3)\n",
+ "\n",
+ " pop = tb.population(n=POP)\n",
+ " hof = tools.HallOfFame(1)\n",
+ " algorithms.eaMuPlusLambda(pop, tb, mu=POP, lambda_=LAMBDA,\n",
+ " cxpb=0.0, mutpb=1.0, ngen=NGEN,\n",
+ " halloffame=hof, verbose=False)\n",
+ " return hof[0]\n",
+ "\n",
+ "\n",
+ "t0 = time.time()\n",
+ "champions = {}\n",
+ "for seed in SEEDS:\n",
+ " tr0, tr1, te0, te1 = FOLDS[0]\n",
+ " best = evolution(tr0, tr1, seed)\n",
+ " champions[seed] = best\n",
+ " v = evaluate(best)\n",
+ " ic = daily_ic(v[tr0:tr1], fwd.values[tr0:tr1]) if v is not None else float(\"nan\")\n",
+ " print(\"seed %-3d IC train %.4f taille %2d %s\" % (\n",
+ " seed, ic, expression_size(best), to_prefix(best)[:90]))\n",
+ "print(\"duree : %.1f s\" % (time.time() - t0))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f6d60645",
+ "metadata": {},
+ "source": [
+ "## 5. Evaluation hors-echantillon et Diebold-Mariano\n",
+ "\n",
+ "Le champion de chaque seed est **re-evalue sur les blocs de test**, qu'il n'a jamais vus. On\n",
+ "mesure :\n",
+ "\n",
+ "- l'**IC hors-echantillon** ;\n",
+ "- le **Sharpe** du portefeuille long-short ;\n",
+ "- un test de **Diebold-Mariano** sur une perte de precision (`mse`) : le modele evolue est\n",
+ " confronte au **meilleur facteur fixe**, jour par jour, sur la meme cible.\n",
+ "\n",
+ "Le champion est entraine **une seule fois** (intervalle d'entrainement du fold 0) et les blocs de\n",
+ "test qu'il rencontre ici sont **exactement** ceux qui portent la valeur de comparaison de la\n",
+ "baseline : la colonne `Sharpe OOS (comparaison)` de la section 3. Sans cette homometrie, on\n",
+ "comparerait une moyenne sur 4 blocs a une moyenne sur 5.\n",
+ "\n",
+ "La doctrine §C demande la **conjonction** de **trois** choses :\n",
+ "\n",
+ "1. un ecart inter-seeds significatif (≥ 2 sigma) ;\n",
+ "2. `dm_p_median < 0.05` ;\n",
+ "3. un **signe favorable** de la statistique de Diebold-Mariano.\n",
+ "\n",
+ "Le troisieme point est celui qui manquait. La statistique est\n",
+ "`moyenne(perte_evolue - perte_fixe) / se` : **positive, elle dit que l'evolue est MOINS precis**.\n",
+ "`p < 0.05` seul recompenserait donc une difference **significative et defavorable** - c'est-a-dire\n",
+ "une defaite nette - exactement comme une victoire. La cellule 5.3 ferme ce trou et le prouve par\n",
+ "un **temoin negatif** execute : deux series de pertes ou l'evolue est significativement moins\n",
+ "precis, que l'ancien critere acceptait et que le nouveau refuse."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "id": "b74f037f",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-10-09T11:41:00.617101Z",
+ "iopub.status.busy": "2026-10-09T11:41:00.616799Z",
+ "iopub.status.idle": "2026-10-09T11:41:01.864178Z",
+ "shell.execute_reply": "2026-10-09T11:41:01.862972Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "facteur fixe de reference (choisi sur l'IC d'entrainement) : lowvol\n",
+ " (pour transparence, Sharpe OOS de comparaison des 4 facteurs :)\n",
+ "facteur\n",
+ "momentum 1.5247\n",
+ "reversal -0.8912\n",
+ "lowvol -0.6359\n",
+ "volume 1.0793\n"
+ ]
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n",
+ "champion evolue, hors-echantillon (walk-forward, 4 blocs)\n"
+ ]
+ },
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " IC OOS | \n",
+ " Sharpe OOS | \n",
+ " DM stat | \n",
+ " DM p | \n",
+ "
\n",
+ " \n",
+ " | seed | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " | 0 | \n",
+ " -0.1192 | \n",
+ " -0.2013 | \n",
+ " 3.7261 | \n",
+ " 0.0002 | \n",
+ "
\n",
+ " \n",
+ " | 1 | \n",
+ " 0.0347 | \n",
+ " 0.1523 | \n",
+ " -4.6425 | \n",
+ " 0.0000 | \n",
+ "
\n",
+ " \n",
+ " | 7 | \n",
+ " 0.0383 | \n",
+ " -0.4268 | \n",
+ " -0.0018 | \n",
+ " 0.9986 | \n",
+ "
\n",
+ " \n",
+ " | 42 | \n",
+ " 0.0577 | \n",
+ " -0.2530 | \n",
+ " -3.6436 | \n",
+ " 0.0003 | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " IC OOS Sharpe OOS DM stat DM p\n",
+ "seed \n",
+ "0 -0.1192 -0.2013 3.7261 0.0002\n",
+ "1 0.0347 0.1523 -4.6425 0.0000\n",
+ "7 0.0383 -0.4268 -0.0018 0.9986\n",
+ "42 0.0577 -0.2530 -3.6436 0.0003"
+ ]
+ },
+ "execution_count": 9,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# Diebold-Mariano implemente localement (Newey-West HAC, lag 1) : aucune dependance exotique.\n",
+ "def dm_test_hac(loss_a, loss_b, lag=1):\n",
+ " d = np.asarray(loss_a, float) - np.asarray(loss_b, float)\n",
+ " d = d[np.isfinite(d)]\n",
+ " n = len(d)\n",
+ " if n < 30:\n",
+ " return np.nan, np.nan\n",
+ " mean_d = d.mean()\n",
+ " dc = d - mean_d\n",
+ " gamma0 = (dc ** 2).sum() / n\n",
+ " s = gamma0\n",
+ " for k in range(1, lag + 1):\n",
+ " gk = (dc[k:] * dc[:-k]).sum() / n\n",
+ " s += 2.0 * (1.0 - k / (lag + 1.0)) * gk\n",
+ " se = np.sqrt(s / n)\n",
+ " if se == 0:\n",
+ " return np.nan, np.nan\n",
+ " stat = mean_d / se\n",
+ " from scipy import stats as st\n",
+ " p = 2 * (1 - st.t.cdf(abs(stat), df=n - 1))\n",
+ " return float(stat), float(p)\n",
+ "\n",
+ "\n",
+ "def oos_losses(sig, target):\n",
+ " # Perte de precision (mse) du signal pris comme predicteur du rendement du lendemain.\n",
+ " s = signal_to_rank(sig)\n",
+ " pred = (s - 0.5) * 2.0\n",
+ " err = pred - pd.DataFrame(target).rank(axis=1, pct=True).values\n",
+ " return np.nanmean(err ** 2, axis=1)\n",
+ "\n",
+ "\n",
+ "# Reference choisie sur l'IC d'ENTRAINEMENT, pas sur le Sharpe OOS de la fenetre de\n",
+ "# comparaison : choisir le meilleur facteur sur la fenetre ou l'on compare reviendrait a\n",
+ "# designer l'adversaire apres avoir vu le score. La table ci-dessus rend les deux colonnes\n",
+ "# visibles pour que le lecteur puisse juger le choix.\n",
+ "best_fixed_name = baseline[\"IC train moyen\"].idxmax()\n",
+ "best_fixed = FIXED[best_fixed_name]\n",
+ "print(\"facteur fixe de reference (choisi sur l'IC d'entrainement) :\", best_fixed_name)\n",
+ "print(\" (pour transparence, Sharpe OOS de comparaison des 4 facteurs :)\")\n",
+ "print(baseline[\"Sharpe OOS (comparaison)\"].to_string())\n",
+ "\n",
+ "rows = []\n",
+ "for seed in SEEDS:\n",
+ " sig = evaluate(champions[seed])\n",
+ " ics, shps, losses = [], [], []\n",
+ " for tr0, tr1, te0, te1 in EVAL_FOLDS:\n",
+ " ics.append(daily_ic(sig[te0:te1], fwd.values[te0:te1]))\n",
+ " shps.append(sharpe(long_short_daily(sig[te0:te1], fwd.values[te0:te1])))\n",
+ " losses.append(oos_losses(sig[te0:te1], fwd.values[te0:te1]))\n",
+ " rows.append({\"seed\": seed, \"IC OOS\": np.nanmean(ics),\n",
+ " \"Sharpe OOS\": np.nanmean(shps),\n",
+ " \"_loss\": np.concatenate(losses)})\n",
+ "\n",
+ "evolved = pd.DataFrame([{k: v for k, v in r.items() if k != \"_loss\"} for r in rows])\n",
+ "\n",
+ "fixed_loss = np.concatenate([oos_losses(best_fixed[te0:te1], fwd.values[te0:te1])\n",
+ " for _, _, te0, te1 in EVAL_FOLDS])\n",
+ "\n",
+ "dm_stats = []\n",
+ "for r in rows:\n",
+ " L = min(len(r[\"_loss\"]), len(fixed_loss))\n",
+ " dm_stats.append(dm_test_hac(r[\"_loss\"][:L], fixed_loss[:L]))\n",
+ "evolved[\"DM stat\"] = [s for s, _ in dm_stats]\n",
+ "evolved[\"DM p\"] = [p for _, p in dm_stats]\n",
+ "evolved = evolved.set_index(\"seed\").round(4)\n",
+ "\n",
+ "print(\"\\nchampion evolue, hors-echantillon (walk-forward, %d blocs)\" % len(EVAL_FOLDS))\n",
+ "evolved"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "id": "64eee2d4",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-10-09T11:41:01.866629Z",
+ "iopub.status.busy": "2026-10-09T11:41:01.866318Z",
+ "iopub.status.idle": "2026-10-09T11:41:01.874906Z",
+ "shell.execute_reply": "2026-10-09T11:41:01.873792Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "facteur fixe de reference : lowvol Sharpe OOS -0.6359\n",
+ "champion evolue : Sharpe OOS -0.1822 (sigma inter-seeds 0.2430)\n",
+ "edge - baseline : 0.4537 >= 2 sigma ? False\n",
+ "Diebold-Mariano p median : 0.0003 < 0.05 ? True\n",
+ "Diebold-Mariano stat med. : -1.8227 < 0 (favorable) ? True\n",
+ "\n",
+ "VERDICT : INCONCLUSIVE\n"
+ ]
+ }
+ ],
+ "source": [
+ "edge = evolved[\"Sharpe OOS\"].mean()\n",
+ "sigma = evolved[\"Sharpe OOS\"].std(ddof=1)\n",
+ "# Meme fenetre que `edge` : la colonne de comparaison restreint la baseline aux blocs de\n",
+ "# test de EVAL_FOLDS, ceux-la memes que le champion vient de parcourir.\n",
+ "fixed_sharpe = baseline.loc[best_fixed_name, \"Sharpe OOS (comparaison)\"]\n",
+ "dm_p_median = float(np.nanmedian(evolved[\"DM p\"]))\n",
+ "dm_stat_median = float(np.nanmedian(evolved[\"DM stat\"]))\n",
+ "\n",
+ "sigma_ok = np.isfinite(sigma) and sigma > 0 and (edge - fixed_sharpe) >= 2 * sigma\n",
+ "# Conjonction complete : significatif ET favorable. `dm_stat_median > 0` signifie que\n",
+ "# l'evolue a une perte PLUS ELEVEE que le facteur fixe -- significatif, mais defavorable.\n",
+ "dm_sign_ok = np.isfinite(dm_stat_median) and dm_stat_median < 0\n",
+ "dm_ok = np.isfinite(dm_p_median) and dm_p_median < 0.05 and dm_sign_ok\n",
+ "\n",
+ "print(\"facteur fixe de reference : %-9s Sharpe OOS %.4f\" % (best_fixed_name, fixed_sharpe))\n",
+ "print(\"champion evolue : Sharpe OOS %.4f (sigma inter-seeds %.4f)\" % (edge, sigma))\n",
+ "print(\"edge - baseline : %.4f >= 2 sigma ? %s\" % (edge - fixed_sharpe, sigma_ok))\n",
+ "print(\"Diebold-Mariano p median : %.4f < 0.05 ? %s\" % (dm_p_median, dm_p_median < 0.05))\n",
+ "print(\"Diebold-Mariano stat med. : %+.4f < 0 (favorable) ? %s\" % (dm_stat_median, dm_sign_ok))\n",
+ "\n",
+ "if sigma_ok and dm_ok:\n",
+ " verdict = \"BEATS\"\n",
+ "elif (not sigma_ok) and (not dm_ok):\n",
+ " verdict = \"NO BEATS\"\n",
+ "else:\n",
+ " verdict = \"INCONCLUSIVE\"\n",
+ "print(\"\\nVERDICT :\", verdict)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "id": "a28dfd07",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-10-09T11:41:01.877650Z",
+ "iopub.status.busy": "2026-10-09T11:41:01.877443Z",
+ "iopub.status.idle": "2026-10-09T11:41:01.904905Z",
+ "shell.execute_reply": "2026-10-09T11:41:01.904100Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "conjonction §C, facteur de reference par facteur :\n"
+ ]
+ },
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " Sharpe OOS compar. | \n",
+ " edge | \n",
+ " >= 2 sigma | \n",
+ " DM p med | \n",
+ " DM stat med | \n",
+ " conjonction | \n",
+ "
\n",
+ " \n",
+ " | facteur | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ " | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " | momentum | \n",
+ " 1.5247 | \n",
+ " -1.7069 | \n",
+ " False | \n",
+ " 0.0085 | \n",
+ " -2.3570 | \n",
+ " False | \n",
+ "
\n",
+ " \n",
+ " | reversal | \n",
+ " -0.8912 | \n",
+ " 0.7090 | \n",
+ " True | \n",
+ " 0.0069 | \n",
+ " -2.0056 | \n",
+ " True | \n",
+ "
\n",
+ " \n",
+ " | lowvol | \n",
+ " -0.6359 | \n",
+ " 0.4537 | \n",
+ " False | \n",
+ " 0.0002 | \n",
+ " -1.8227 | \n",
+ " False | \n",
+ "
\n",
+ " \n",
+ " | volume | \n",
+ " 1.0793 | \n",
+ " -1.2615 | \n",
+ " False | \n",
+ " 0.1057 | \n",
+ " 0.5090 | \n",
+ " False | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " Sharpe OOS compar. edge >= 2 sigma DM p med DM stat med conjonction\n",
+ "facteur \n",
+ "momentum 1.5247 -1.7069 False 0.0085 -2.3570 False\n",
+ "reversal -0.8912 0.7090 True 0.0069 -2.0056 True\n",
+ "lowvol -0.6359 0.4537 False 0.0002 -1.8227 False\n",
+ "volume 1.0793 -1.2615 False 0.1057 0.5090 False"
+ ]
+ },
+ "execution_count": 11,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# Robustesse au choix de la reference. La conjonction §C depend du facteur fixe auquel on se\n",
+ "# compare : un verdict qui ne tient que sous un choix de reference n'est pas un verdict, c'est\n",
+ "# un artefact de selection. On recalcule donc les deux jambes pour les QUATRE facteurs et on\n",
+ "# publie la table entiere, plutot que le seul chiffre du facteur retenu.\n",
+ "robust = []\n",
+ "for name, sig in FIXED.items():\n",
+ " fl = np.concatenate([oos_losses(sig[te0:te1], fwd.values[te0:te1])\n",
+ " for _, _, te0, te1 in EVAL_FOLDS])\n",
+ " ps, ss = [], []\n",
+ " for r in rows:\n",
+ " L = min(len(r[\"_loss\"]), len(fl))\n",
+ " st, p = dm_test_hac(r[\"_loss\"][:L], fl[:L])\n",
+ " ss.append(st); ps.append(p)\n",
+ " p_med, s_med = float(np.nanmedian(ps)), float(np.nanmedian(ss))\n",
+ " dm_ok_f = np.isfinite(p_med) and p_med < 0.05 and np.isfinite(s_med) and s_med < 0\n",
+ " fs = baseline.loc[name, \"Sharpe OOS (comparaison)\"]\n",
+ " s_ok_f = np.isfinite(sigma) and sigma > 0 and (edge - fs) >= 2 * sigma\n",
+ " robust.append({\"facteur\": name, \"Sharpe OOS compar.\": fs, \"edge\": edge - fs,\n",
+ " \">= 2 sigma\": s_ok_f, \"DM p med\": p_med, \"DM stat med\": s_med,\n",
+ " \"conjonction\": bool(s_ok_f and dm_ok_f)})\n",
+ "robust = pd.DataFrame(robust).set_index(\"facteur\").round(4)\n",
+ "print(\"conjonction §C, facteur de reference par facteur :\")\n",
+ "robust"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "id": "38d151be",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-10-09T11:41:01.907421Z",
+ "iopub.status.busy": "2026-10-09T11:41:01.907102Z",
+ "iopub.status.idle": "2026-10-09T11:41:01.914393Z",
+ "shell.execute_reply": "2026-10-09T11:41:01.913245Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "temoin defavorable : DM stat +7.7405 p 0.0000\n",
+ " ancien critere (p < 0.05 seul) : True\n",
+ " critere retenu (p < 0.05 ET stat < 0) : False\n",
+ "temoin negatif OK : significatif, defavorable, et refuse par le critere retenu.\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Temoin negatif du critere directionnel. Deux series de pertes ou le modele \"evolu\"\n",
+ "# est significativement MOINS precis que le fixe : la difference est reelle et\n",
+ "# significative (p < 0.05), mais elle est defavorable. Le critere retenu doit la refuser,\n",
+ "# l'ancien (p < 0.05 seul) l'aurait acceptee comme une victoire.\n",
+ "rng_w = np.random.RandomState(0)\n",
+ "_l_fixe = rng_w.normal(0.0, 1.0, 400)\n",
+ "_l_evol = _l_fixe + 0.25 + rng_w.normal(0.0, 0.5, 400) # perte systematiquement plus elevee\n",
+ "_w_stat, _w_p = dm_test_hac(_l_evol, _l_fixe)\n",
+ "print(\"temoin defavorable : DM stat %+.4f p %.4f\" % (_w_stat, _w_p))\n",
+ "print(\" ancien critere (p < 0.05 seul) : %s\" % (_w_p < 0.05))\n",
+ "print(\" critere retenu (p < 0.05 ET stat < 0) : %s\" % (_w_p < 0.05 and _w_stat < 0))\n",
+ "assert _w_p < 0.05 and _w_stat > 0, \"le temoin doit etre significatif ET defavorable\"\n",
+ "assert not (_w_p < 0.05 and _w_stat < 0), \"le critere directionnel doit refuser ce cas\"\n",
+ "print(\"temoin negatif OK : significatif, defavorable, et refuse par le critere retenu.\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "90f2a44e",
+ "metadata": {},
+ "source": [
+ "## 6. Lecture du verdict\n",
+ "\n",
+ "Le verdict ci-dessus est produit par la **conjonction** exigee par la doctrine §C, pas par une\n",
+ "impression. Trois cas, et ce qu'ils veulent dire :\n",
+ "\n",
+ "- **BEATS** - l'ecart au facteur fixe depasse 2 sigma inter-seeds **et** Diebold-Mariano est\n",
+ " significatif **et favorable** (statistique negative : l'evolue est plus precis). Une difference\n",
+ " significative mais defavorable n'ouvre pas cette branche : c'est le troisieme terme ajoute par la\n",
+ " cellule 5.3.\n",
+ "- **NO BEATS** - aucune des conditions ne tient. Resultat honnete, et le plus frequent sur un panier\n",
+ " de 10 actifs : l'evolution sur-ajuste le bloc d'entrainement.\n",
+ "- **INCONCLUSIVE** - une partie seulement de la conjonction tient. C'est le piege que la doctrine\n",
+ " nomme : un `+N sigma` avec un `p` non significatif n'est **pas** une victoire ; un `p`\n",
+ " significatif avec un signe defavorable n'en est pas une non plus.\n",
+ "\n",
+ "### Le verdict depend du facteur de reference - et la table le montre\n",
+ "\n",
+ "La cellule precedente publie la conjonction pour les **quatre** facteurs fixes, pas seulement pour\n",
+ "celui retenu : un verdict qui ne tient que sous un choix de reference n'est pas un verdict, c'est un\n",
+ "artefact de selection. Ici elle ne tient que pour `reversal`, qui est justement le **plus faible**\n",
+ "des quatre hors-echantillon - autrement dit, le seul facteur que le champion evolue bat est celui\n",
+ "dont la baseline est la moins bonne.\n",
+ "\n",
+ "La reference retenue est choisie sur l'**IC d'entrainement**, sans regarder la fenetre d'evaluation ;\n",
+ "sous ce choix, la conjonction echoue sur la jambe sigma et le verdict est `INCONCLUSIVE`. C'est le\n",
+ "verdict publie, et la table donne au lecteur de quoi le contester.\n",
+ "\n",
+ "### Le biais, separe de la precision\n",
+ "\n",
+ "Un modele peut « gagner » parce qu'il est **biaise** dans le bon sens, pas parce qu'il est plus\n",
+ "**precis**. La cellule suivante separe les deux, comme l'exige §C.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "id": "1dbb67d0",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-10-09T11:41:01.916990Z",
+ "iopub.status.busy": "2026-10-09T11:41:01.916667Z",
+ "iopub.status.idle": "2026-10-09T11:41:01.991549Z",
+ "shell.execute_reply": "2026-10-09T11:41:01.990280Z"
+ }
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " biais signe moyen | \n",
+ " ecart-type erreur | \n",
+ "
\n",
+ " \n",
+ " | modele | \n",
+ " | \n",
+ " | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " | evolue (seed 0) | \n",
+ " -0.44 | \n",
+ " 0.6584 | \n",
+ "
\n",
+ " \n",
+ " | fixe: lowvol | \n",
+ " -0.45 | \n",
+ " 0.6323 | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " biais signe moyen ecart-type erreur\n",
+ "modele \n",
+ "evolue (seed 0) -0.44 0.6584\n",
+ "fixe: lowvol -0.45 0.6323"
+ ]
+ },
+ "execution_count": 13,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "def bias_report(sig, target, te0, te1):\n",
+ " s = signal_to_rank(sig[te0:te1])\n",
+ " t = pd.DataFrame(target[te0:te1]).rank(axis=1, pct=True).values\n",
+ " pred = (s - 0.5) * 2.0\n",
+ " e = pred - t\n",
+ " return float(np.nanmean(e)), float(np.nanstd(e))\n",
+ "\n",
+ "\n",
+ "acc = []\n",
+ "for label, sig in [(\"evolue (seed 0)\", evaluate(champions[SEEDS[0]])),\n",
+ " (\"fixe: %s\" % best_fixed_name, best_fixed)]:\n",
+ " bs, ss = [], []\n",
+ " for _, _, te0, te1 in FOLDS[1:]:\n",
+ " b, s = bias_report(sig, fwd.values, te0, te1)\n",
+ " bs.append(b); ss.append(s)\n",
+ " acc.append({\"modele\": label, \"biais signe moyen\": np.nanmean(bs),\n",
+ " \"ecart-type erreur\": np.nanmean(ss)})\n",
+ "\n",
+ "pd.DataFrame(acc).set_index(\"modele\").round(4)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4de6f5f4",
+ "metadata": {},
+ "source": [
+ "## 7. Exercices\n",
+ "\n",
+ "Trois exercices, dans l'ordre de difficulte. Ils ne bloquent pas l'execution du carnet : les\n",
+ "cellules ci-dessous s'executent telles quelles meme non completees.\n",
+ "\n",
+ "### Exercice 1 - enrichir le jeu de primitives\n",
+ "\n",
+ "Ajoutez un operateur `corr20` (correlation glissante 20 jours entre le signal courant et un\n",
+ "second signal) au dictionnaire `PRIMITIVES`, en respectant l'aritie 2. Verifiez que\n",
+ "`random_expr` peut le tirer et que `evaluate` l'evalue sans `None`.\n",
+ "\n",
+ "### Exercice 2 - changer la cible\n",
+ "\n",
+ "Le carnet predit le rendement du **lendemain**. Remplacez la cible par le rendement cumule sur\n",
+ "5 jours, puis relancez l'evolution. L'IC monte-t-il, et le verdict change-t-il ? Attention : la\n",
+ "fenetre de prediction plus longue reduit le nombre d'observations independantes.\n",
+ "\n",
+ "### Exercice 3 - mesurer la sur-ajustement\n",
+ "\n",
+ "Comparez l'IC **d'entrainement** du champion a son IC **hors-echantillon**. Un ecart large est la\n",
+ "signature du sur-ajustement. Proposez une modification du fitness qui penalise cet ecart."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "id": "1a0c9765",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-10-09T11:41:01.994023Z",
+ "iopub.status.busy": "2026-10-09T11:41:01.993826Z",
+ "iopub.status.idle": "2026-10-09T11:41:02.000173Z",
+ "shell.execute_reply": "2026-10-09T11:41:01.999021Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "cible 5 jours disponible : (1643, 10)\n",
+ "Exercices a completer : 1, 2, 3\n"
+ ]
+ }
+ ],
+ "source": [
+ "# --- Exercice 1 : ajouter une primitive -------------------------------------\n",
+ "# TODO etudiant : definir corr20(x, y) puis l'enregistrer dans PRIMITIVES.\n",
+ "def corr20(x, y):\n",
+ " # Indice : pd.DataFrame(x).rolling(20).corr(pd.Series(...)) ne diffuse pas directement ;\n",
+ " # calculer la correlation glissante ligne a ligne sur les deux panneaux.\n",
+ " return None # TODO etudiant : remplacer par le calcul\n",
+ "\n",
+ "\n",
+ "# --- Exercice 2 : changer la cible ------------------------------------------\n",
+ "fwd_5 = close.pct_change(5).shift(-5).reindex(ret.index) # TODO etudiant : ne pas oublier l'anti-fuite\n",
+ "print(\"cible 5 jours disponible :\", fwd_5.shape)\n",
+ "\n",
+ "\n",
+ "# --- Exercice 3 : ecart train / hors-echantillon -----------------------------\n",
+ "def gap_train_oos(individual):\n",
+ " # TODO etudiant : rendre (IC train, IC OOS) du meme individu sur les memes blocs.\n",
+ " return None # TODO etudiant\n",
+ "\n",
+ "\n",
+ "print(\"Exercices a completer : 1, 2, 3\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "0b07ad56",
+ "metadata": {},
+ "source": [
+ "## 8. Ce que ce carnet ne fait pas\n",
+ "\n",
+ "- **Pas de mini-boucle LLM.** La comparaison « evolution vs LLM » demande un acces modele et un\n",
+ " budget d'evaluation identique pour les deux approches ; elle est suivie separement (#20041).\n",
+ "- **Pas de backtest QuantConnect.** Le carnet mesure un signal de recherche (IC, portefeuille\n",
+ " long-short jouet) ; le passage a une strategie LEAN est un autre travail, avec ses couts.\n",
+ "- **Pas de couts de transaction** dans le Sharpe affiche : un alpha a fort turnover y perdrait\n",
+ " l'essentiel de son edge. C'est une limite **declaree**, pas un oubli.\n",
+ "\n",
+ "## References\n",
+ "\n",
+ "- Li, Yang, Yang, Xu, Wang, Liu, Bian - *R&D-Agent-Quant: A Multi-Agent Framework for Data-Centric\n",
+ " Factors and Model Joint Optimization*, Microsoft Research Asia, arXiv:2505.15155. Archive au\n",
+ " gisement : `G:\\Mon Drive\\MyIA\\IA\\Bibliographie IA\\Trading\\`.\n",
+ "- Serie `Search/Part1-Foundations/Search-05-GeneticAlgorithms.ipynb` - l'organe evolutionnaire natif.\n",
+ "- `.claude/rules/pr-review-discipline.md` §C - la doctrine de validation appliquee comme fitness :\n",
+ " walk-forward **5 blocs**, **4 seeds**, Diebold-Mariano sur une perte de precision (`mse`) et\n",
+ " **conjonction avec le signe**. Le carnet applique §C ; il ne pretend pas faire plus.\n",
+ "\n",
+ "Issue de suivi : #20041 (Part of #19306)."
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (CoursIA-2)",
+ "language": "python",
+ "name": "python3-coursia2"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.13.15"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/MyIA.AI.Notebooks/QuantConnect/Python/README.md b/MyIA.AI.Notebooks/QuantConnect/Python/README.md
index e7c7dd3228..6de83ec676 100644
--- a/MyIA.AI.Notebooks/QuantConnect/Python/README.md
+++ b/MyIA.AI.Notebooks/QuantConnect/Python/README.md
@@ -82,6 +82,7 @@ Suite à l'audit du 5 mai 2026, voici l'état honnête de chaque notebook. **Auc
| QC-Py-32-RL-DQN-Trading | EXÉCUTÉ | |
| QC-Py-40-PaperTrading-Binance | EXÉCUTÉ | |
| QC-Py-41-PaperTrading-IBKR | EXÉCUTÉ | |
+| QC-Py-42-Alpha-Mining-Evolution | EXÉCUTÉ | cellules exécutées sur le panier crypto local (10 actifs ; fenêtre effective 2020-09 → 2025-03) — verdict hors-échantillon `INCONCLUSIVE` |
| QC-Py-Cloud-01-FinBERT-Sentiment | EXÉCUTÉ | |
| QC-Py-Cloud-02-ML-Classification | EXÉCUTÉ | |
| QC-Py-Cloud-12-SectorRotation-Momentum | doc cloud | markdown-only — backtest sur QC Cloud |
@@ -228,6 +229,22 @@ Approfondissement RL au-delà du DQN de la Phase 8 : PPO, SAC/A2C, application p
| [QC-Py-40-PaperTrading-Binance](QC-Py-40-PaperTrading-Binance.ipynb) | Binance |
| [QC-Py-41-PaperTrading-IBKR](QC-Py-41-PaperTrading-IBKR.ipynb) | Interactive Brokers |
+## Recherche — alpha mining par évolution (QC-Py-42)
+
+Recherche *from scratch* : plutôt que de coder un facteur à la main, on **fait évoluer des
+expressions alpha** (arbres d'opérateurs sur des features de base) avec `deap`, puis on les
+confronte à une baseline de facteurs fixes. C'est le pont entre la série `Search/`
+(algorithmes évolutionnaires) et la série `QuantConnect` (validation financière).
+
+| Notebook | Méthode | Ce qu'il démontre |
+|----------|---------|-------------------|
+| [QC-Py-42-Alpha-Mining-Evolution](QC-Py-42-Alpha-Mining-Evolution.ipynb) | Évolution génétique d'expressions (`deap`), walk-forward 5 blocs, 4 seeds, Diebold-Mariano à critère directionnel | Un moteur de recherche peut **produire** des alphas plausibles — et le verdict hors-échantillon dit s'il bat la baseline. Ici : non (`INCONCLUSIVE`), ce qui est le résultat, pas l'échec |
+
+Le carnet tourne sur les données **locales** du dépôt (panier crypto, 10 actifs) : aucune
+dépendance réseau, exécution reproductible. Sa fonction de fitness applique la doctrine de
+validation du dépôt (walk-forward, multi-seed, Diebold-Mariano sur une perte de précision) —
+l'objectif pédagogique est autant la **méthode de validation** que le résultat.
+
## Stratégies Cloud (QC-Py-Cloud-*)
Notebooks de recherche et stratégies exécutées sur QuantConnect Cloud.