Repository navigation
QC-Py-31 : le « split temporel » est un split par actif, et l'axe du backtest empile 30 actions (mesures non calendaires) #17571
Description
Constat
QC-Py-31-Transformer-Training.ipynb presente un « split temporel » qui n'en est pas un, et un backtest dont l'axe de temps empile 30 historiques d'actions. Les deux sont pre-existants sur origin/main (bloc identique verifie a la source, pas deduit) ; #17521 est la premiere PR a publier les chiffres du backtest comme des mesures, c'est ce qui les rend visibles.
1. Le « split temporel » est un split par actif
build_sequences (cellule 15) concatene dans l'ordre d'insertion du dictionnaire :
for ticker, df in all_features.items():
...
all_X.append(features[i - seq_len:i])L'ordre obtenu est donc (actif, date) — jamais (date, actif) — et la fonction ne rend que X, y_reg, y_cls, y_plain : ni le ticker ni la date ne remontent. Le bloc qui suit coupe des tranches contiguës en annoncant l'inverse :
# Split temporel strict (les sequences sont deja ordonnees par date)
X_train = X_all[:train_end]
X_test = X_all[val_end:]Mesure (sortie de la cellule 15) : Sequences totales: 81090, Shape X: (81090, 60, 12), soit 30 actions × 2 703 sequences (2 768 seances 2014-2025 moins seq_len + pred_len). Le test (Test: 12164 samples, 15 %) est donc la queue de la concatenation : environ les 4,5 dernieres actions, sur toute la periode, pas une fenetre temporelle sur l'univers.
Consequence : le modele est entraine sur ~25,5 actions et evalue sur ~4,5 autres actions, sur la meme periode. Ce n'est pas un holdout temporel — c'est un holdout d'actifs, et il est annonce comme temporel.
Lecture alternative ecartee, avec la mesure : « 30 actions x ~405 seances » (soit ~1,6 an) ne decrit pas la structure du code — 405 x 30 = 12 150, c'est-a-dire la taille du test, pas une grille. Le perimetre par actif est de 2 703 sequences.
2. Le backtest trie des jours d'une action, pas des actions entre elles
Cellule 40 :
batch_preds = preds_reg[i:i+rebalance_freq] # 20 echantillons CONSECUTIFS
sorted_indices = np.argsort(batch_preds)
top_indices = sorted_indices[-top_n:]
port_return = batch_realized[top_indices].mean()Sur un tableau ordonne (actif, date), 20 echantillons consecutifs sont 20 fenetres du meme actif. Le tri porte donc sur des jours d'une action, alors que la docstring annonce « Trier les actions par prediction de rendement ». Le backtest est un timing intra-actif ; la selection transversale decrite n'existe pas.
3. Les metriques sont sur un axe empile, pas calendaire
n_days = max(len(port_returns) * REBALANCE_FREQ, 1) # 607 x 20 = 12 140
annualized_return = (1 + total_return) ** (252 / n_days) - 112 140 « jours » ≈ 48 ans, obtenus en mettant bout a bout les 30 historiques, alors que la fenetre reelle de chaque actif est ~11 ans. Sortie publiee a la tete 23853f70db :
Rendement total: 3852.05%
Rendement annualise: 7.92%
Volatilite: 16.20%
Sharpe Ratio: 0.489
Max Drawdown: -37.59%
Ces quatre chiffres sont coherents entre eux et faux en echelle : ils decrivent un backtest de 48 ans qui n'a pas eu lieu. Le total (3852 %) est d'ailleurs le genre de nombre qu'un etudiant lit comme un resultat.
Pourquoi c'est un sujet separe, pas une retouche de #17521
#17516 nomme trois defauts (inf/nan, entrainement saute, prose 14 vs 12 features) ; celui-ci n'y figure pas. Le corriger demande de faire remonter (ticker, date) a travers build_sequences puis le split, donc de toucher trois cellules (15, split, 40) et de re-entrainer completement — un second sujet, pas un correctif du premier. En attendant, #17521 declare la limite en prose la ou les chiffres sont imprimes.
Ce qui est attendu
- Propager
(ticker, date)dans la construction du jeu de sequences, et decouper par date (globale, tous actifs confondus) : le test doit etre une fenetre temporelle sur l'univers, pas une queue d'actifs sous un nom qui dit le contraire. - Backtest transversal : a chaque date de rebalancement, trier les actifs entre eux (cross-section), puis composer les rendements ; metriques sur un axe calendaire.
- Si l'intention pedagogique est de rester volontairement simplifie : l'ecrire noir sur blanc a l'endroit du chiffre (axe en echantillons empiles, pas de selection transversale), et cesser de publier un « rendement annualise » calcule sur cet axe.
Critere de fermeture
Une PR mergée dont la tête : (a) ne présente plus un split par actif comme un split temporel ; (b) n'imprime plus de rendement annualisé dérivé d'un axe empilé ; (c) exécute son backtest sur une cross-section par date, ou déclare explicitement la limite là où le chiffre est imprimé.
Activity
fix(qc,#17516): QC-Py-31 — re-execution de reference honnete, nbforma…
jsboige commentedon Sep 23, 2026
[CLAIMED] lane myia-po-2023:CoursIA — corriger le split (par date globale, univers entier) et le backtest (cross-section par date, metriques calendaires) de QC-Py-31, re-entrainement complet -- paths: MyIA.AI.Notebooks/QuantConnect/Python/QC-Py-31-Transformer-Training.ipynb
jsboige commentedon Sep 23, 2026
Livraison : PR #17583 (Grain: DEEP/notebook-python — lane myia-po-2023:CoursIA).
Acceptance : (a) split temporel global par date de cible — train 2014-03→2021-10 (56 790), val →2023-05 (12 150), test 2023-05-16→2024-12-23 (12 150, fenêtre commune aux 30 actifs), assert anti-fuite ; (b) backtest cross-sectionnel (top quintile trié entre actifs à chaque rebalancement, rendements forward 5 j non chevauchants) avec métriques sur axe calendaire et benchmark equal-weight imprimé ; (c) re-entraînement complet from scratch — le .pt committé du 2026-07-02 était entraîné sous l'ancien split fuyard et sautait l'entraînement au chargement ; supprimé et régénéré (10 epochs, early stopping, val_loss 2.1165), papermill 52/52 exit 0.
Verdict honnête : NO BEATS ajusté du risque — corrélation prédiction/réalisation −0,014 (signal nul), stratégie 116,09 % total / Sharpe 3,008 vs benchmark 61,82 % / Sharpe 3,495 : l'excès de +54 % est du risque de concentration en bull market, pas de la prédiction. Exactement ce que l'ancien protocole maquillait.
fix(qc,#17516): QC-Py-31 -- run de reference sans injection papermill…
fix(qc,#17571): QC-Py-31 repare sur la reserve 5799662310 — prints ba…
fix(qc,#17571): QC-Py-31 re-execution from scratch (52/52, 43 min) --…
fix(qc,#17571): cell-37 re-ancree sur les chiffres backtest du run v2…
fix(qc,#17516): QC-Py-31 backtest inf/nan (unites risque-ajuste), ent…
merge(#17571): resout le conflit QC-Py-31 — porte les fixes de main s…
chore(qc,#17571): regenere transformer_multiasset_model.pt sur le run…
fix(notebook,#17571): re-execution fraiche QC-Py-31 + prose reelignee…
fix(notebook,#17571): QC-Py-31 — split temporel global par date + bac…