Ce notebook etablit une entree REGISTRY pour le modele M15 (LSTM applique a la volatilite realisee en log, Hochreiter & Schmidhuber 1997) conforme au bareme pr-review-discipline.md §C — la suite directe de l’entree M4 DLinear-vol (#10908, PR #10930). L’issue #10908 constatait qu’aucune entree du registre ne defendait le bareme §C : walk-forward >= 5 folds, >= 4 seeds, Diebold-Mariano avec loss_fn="linear", conjonction edge >= 2σ cross-seed et** dm_p_median < 0,05, baselines, verdict honnete** (BEATS / NO BEATS / INCONCLUSIVE).
Pourquoi BTC-only : le run original couvre 7 coins, mais seul BTC dispose de ~2278 jours de RV (Bitstamp hourly 2014-2024) contre ~725 jours pour les autres (yfinance) — le notebook M15 original mesurait deja « la longueur de donnees est le facteur limitant ». L’entree §C mesure le verdict sur le coin le plus riche ; les autres coins restent hors bareme (donnees insuffisantes pour un verdict §C defendable).
Cadence de refit : le run §C utilise --refit-every 110 (documente dans le registre). La cadence legacy de recherche (22 j) retraine ~85 LSTM par combo (~50 min/combo sur RTX 3070) — infeasible pour un sweep multi-seed §C de 12 combos. La cadence 110 j est un hyperparametre de walk-forward legitime, reproductible, et le registre documente la valeur exacte utilisee.
1. Protocole §C applique
#
Critere
Mesure dans ce notebook
1
Walk-forward
5 folds, fenetre d’entrainement croissante (fold_size = n // 6), test = fenetre glissante contigue, aucun chevauchement train/test, refit LSTM toutes les 110 j
2
Multi-seed
4 seeds {0, 1, 7, 42}, metriques par seed dans la section 2
3
Diebold-Mariano
scripts/dm_test.py avec loss_fn="linear" (perte signee, preserve le signe — jamais mse/mae, cf. section 3)
4
Conjonction
edge >= 2σ cross-seed ETdm_p_median < 0,05, les deux reportes separement (section 3)
5
Baselines
HAR (Corsi 2009) = benchmark de reference de la RV ; baseline persistence (random walk) mesuree en section 4 ; couts de transaction documentes (pas de strategie derivee)
6
Verdict
BEATS / NO BEATS / INCONCLUSIVE (section 5)
7
Registre
entree REGISTRY.md avec le data hash reel (section 6)
8
Notebook
committe avec outputs (C.2)
Convention de signe : la reduction MSE de M15 est mse_reduction_pct = (mse_lstm - mse_har)/mse_har*100 — negative quand le LSTM ameliore (inverse de DLinear, ou la reduction est positive-quand-mieux). L’edge §C edge_pct = -moyenne(reduction) restaure la convention du bareme (positive = le modele reduit la MSE).
import jsonfrom pathlib import Pathimport numpy as npimport pandas as pdresults_path = Path("scripts/results/m15_lstm_rv_btc_sc/results.json")withopen(results_path, encoding="utf-8") as f: data = json.load(f)rows = pd.DataFrame(data["combos"])per_horizon = data["per_horizon_sc"]cfg = dataprint(f"Model : LSTM (Hochreiter & Schmidhuber 1997) sur log-RV, hidden={cfg['hidden_size']}, window={cfg['window']}, {cfg['n_params']} params")print(f"Run §C : {len(rows)} combos BTC-USD ({rows['horizon'].nunique()} horizons x {rows['seed'].nunique()} seeds)")print(f"Seeds : {sorted(rows['seed'].unique())}")print(f"Walk-forward : {cfg['n_splits']}-fold, refit={cfg['refit_every']}d, epochs max={cfg.get('epochs', 'n/a')}")print(f"DM loss_fn : {cfg['loss_fn']} (perte signee, preserve le signe -- bareme §C)")print(f"Runtime : {cfg['runtime_s']:.0f}s ({cfg['runtime_s']/60:.1f} min)")# Verifications de conformite du run (bareme §C) -- rapportees, jamais bloquantesok =Truechecks = [ ("loss_fn = linear", cfg["loss_fn"] =="linear"), (">= 4 seeds parmi {0,1,7,42,99}",len(rows["seed"].unique()) >=4andset(rows["seed"].unique()) <= {0, 1, 7, 42, 99}), ("walk-forward >= 5 folds", cfg["n_splits"] >=5),]for label, passed in checks:print(f" [{'OK'if passed else'FAIL'}] {label}") ok = ok and passedprint("Conformite §C du run :", "OK"if ok else"INCOMPLETE")
Model : LSTM (Hochreiter & Schmidhuber 1997) sur log-RV, hidden=64, window=22, 17729 params
Run §C : 12 combos BTC-USD (3 horizons x 4 seeds)
Seeds : [np.int64(0), np.int64(1), np.int64(7), np.int64(42)]
Walk-forward : 5-fold, refit=110d, epochs max=n/a
DM loss_fn : linear (perte signee, preserve le signe -- bareme §C)
Runtime : 2096s (34.9 min)
[OK] loss_fn = linear
[OK] >= 4 seeds parmi {0,1,7,42,99}
[OK] walk-forward >= 5 folds
Conformite §C du run : OK
2. Resultats par seed (critere §C.2)
Metriques par seed (aucun agregat masquant) : MSE sur log-RV de LSTM vs HAR, reduction MSE relative (negative = LSTM meilleur), et test DM (loss_fn="linear") par configuration (horizon, seed). Le verdict DM de chaque ligne est le verdict du test sur cette configuration ; la conjonction agregee vient en section 3.
print("=== Resultats par (horizon, seed) — BTC-USD, DM loss_fn=linear ===\n")print(f"{'h':>3}{'seed':>4}{'LSTM MSE':>12}{'HAR MSE':>12}{'red %':>7}{'dm_stat':>9}{'dm_p':>9} verdict")for h insorted(rows["horizon"].unique()): sub = rows[rows["horizon"] == h].sort_values("seed")for _, r in sub.iterrows():print(f"{int(r['horizon']):>3}{int(r['seed']):>4}{r['mse_lstm']:>12.5f} "f"{r['mse_har']:>12.5f}{r['mse_reduction_pct']:>+6.1f}% "f"{r['dm_stat']:>9.2f}{r['dm_pvalue']:>9.2e}{r['dm_verdict']}")
=== Resultats par (horizon, seed) — BTC-USD, DM loss_fn=linear ===
h seed LSTM MSE HAR MSE red % dm_stat dm_p verdict
1 0 0.85350 0.88768 -3.8% 9.10 0.00e+00 BEATEN BY baseline
1 1 0.86372 0.88768 -2.7% 13.96 0.00e+00 BEATEN BY baseline
1 7 0.92644 0.88768 +4.4% 4.66 3.41e-06 BEATEN BY baseline
1 42 0.91987 0.88768 +3.6% 5.84 6.16e-09 BEATEN BY baseline
5 0 0.42368 0.52197 -18.8% 13.90 0.00e+00 BEATEN BY baseline
5 1 0.46654 0.52197 -10.6% 12.98 0.00e+00 BEATEN BY baseline
5 7 0.48234 0.52197 -7.6% 13.68 0.00e+00 BEATEN BY baseline
5 42 0.43666 0.52197 -16.3% 14.76 0.00e+00 BEATEN BY baseline
10 0 0.45631 0.57068 -20.0% 15.46 0.00e+00 BEATEN BY baseline
10 1 0.49228 0.57068 -13.7% 14.17 0.00e+00 BEATEN BY baseline
10 7 0.44753 0.57068 -21.6% 17.72 0.00e+00 BEATEN BY baseline
10 42 0.42744 0.57068 -25.1% 13.87 0.00e+00 BEATEN BY baseline
La conjonction exige deux resultats independants, reportes separement :
edge = reduction MSE moyenne (LSTM vs HAR) sur les seeds, signe restaure (edge_pct = -moyenne(reduction), positive quand le LSTM reduit la MSE — convention identique a l’entree DLinear) ;
sigma cross-seed = ecart-type de la reduction entre seeds — σ mesure la dispersion inter-seeds, pas la significativite (piege mesure : +19,97σ avec DM p = 0,236) ;
dm_p_median = mediane des p-valeurs DM sur les seeds.
BEATS (conjonction) <=> edge >= 2σetdm_p_median < 0,05. Un seul des deux ne suffit pas.
Regle de dominance : un seed significativement BEATEN en perte signee rend le verdict agrege NO BEATS, meme si la conjonction edge/σ/p passerait. Le cas se produit reellement : une reduction MSE (perte symetrique) peut coexister avec un biais systematique que la perte signee expose — c’est le sens du loss_fn="linear" exige par §C (#10228).
La perte linear preserve le signe : sous mse, une serie et son oppose sont bit-identiques (pitfall #10228) — le test comparerait deux volatilites, pas deux previsions.
print("=== Conjonction §C par horizon (edge, sigma cross-seed, dm_p_median) ===\n")print(f"{'h':>3}{'edge (red moy %)':>16}{'sigma xs':>9}{'edge/2σ':>8}{'dm_p_med':>10} verdict_sc")for h insorted(int(k) for k in per_horizon): a = per_horizon[str(h)] ratio = a["edge_pct"] / (2* a["edge_std_pct"]) if a["edge_std_pct"] >0elsefloat("nan") flag =" <- conjonction §C tenue"if a["verdict_sc"] =="BEATS"else""print(f"{h:>3}{a['edge_pct']:>+14.1f}% {a['edge_std_pct']:>9.1f} "f"{ratio:>7.2f}x {a['dm_p_median']:>10.2e}{a['verdict_sc']}{flag}")# Recalcul independant depuis les rows brutes (auditabilite) : meme regle que le scriptprint("\nVerification : recalcul depuis les rows brutes (meme regle que le script)")ok =Truefor h insorted(rows["horizon"].unique()): sub = rows[rows["horizon"] == h] red = sub["mse_reduction_pct"].to_numpy() edge =float(-np.mean(red)) # signe restaure : negative-quand-mieux -> positive-quand-mieux edge_std =float(np.std(red)) dm_med =float(np.median(sub["dm_pvalue"]))ifany("BEATEN"in v for v in sub["dm_verdict"]): v ="NO BEATS"elif edge >=2* edge_std and dm_med <0.05: v ="BEATS"else: v ="INCONCLUSIVE" a = per_horizon[str(h)] match = v == a["verdict_sc"] ok = ok and matchprint(f" h={h}: recalcul={v:12s} agrege={a['verdict_sc']:12s}{'OK'if match else'MISMATCH'}")print("Recalcul conforme :", "OK"if ok else"MISMATCH")
=== Conjonction §C par horizon (edge, sigma cross-seed, dm_p_median) ===
h edge (red moy %) sigma xs edge/2σ dm_p_med verdict_sc
1 -0.4% 3.7 -0.05x 3.08e-09 NO BEATS
5 +13.3% 4.5 1.50x 0.00e+00 NO BEATS
10 +20.1% 4.1 2.45x 0.00e+00 NO BEATS
Verification : recalcul depuis les rows brutes (meme regle que le script)
h=1: recalcul=NO BEATS agrege=NO BEATS OK
h=5: recalcul=NO BEATS agrege=NO BEATS OK
h=10: recalcul=NO BEATS agrege=NO BEATS OK
Recalcul conforme : OK
4. Baselines et couts de transaction (critere §C.5)
HAR (Corsi 2009) est le benchmark de reference du domaine pour la prevision de volatilite realisee : regression OLS de la RV future sur les moyennes 1j/5j/22j. C’est la baseline « sans competence » canonique de la litterature RV — le pendant de la majority baseline en classification. Le LSTM est compare a HAR sur toutes les configurations.
Baseline persistence (random walk) : prevision naive y_hat(t+h) = y(t) — le plancher « pas de competence » minimal, mesure en direct ci-dessous sur la meme serie et le meme decoupage temporel que le run §C.
Couts de transaction : cette entree mesure une prevision (MSE sur log-RV), pas une strategie — aucun portefeuille n’est derive, donc aucun cout de transaction impute. Si la prevision etait convertie en overlay de vol-timing, la borne crypto du bareme (10 bps) serait le cout a appliquer ; c’est une note, pas un claim.
import syssys.path.insert(0, "scripts")from m11g_fee_aware_kelly import _load_one_coinfrom realized_variance import daily_realized_variance, realized_variance_to_log# Serie identique au run §C : memes fonctions du pipeline, memes donnees.hourly_rets = _load_one_coin("BTC-USD")rv = daily_realized_variance(hourly_rets)log_rv = realized_variance_to_log(rv).values.astype(float)n =len(log_rv)n_splits =int(cfg["n_splits"])fold_size = n // (n_splits +1)print(f"Serie log-RV : {n} jours (identique au run §C)")print("\n=== Baseline persistence (random walk) — plancher sans competence ===\n")print(f"{'h':>3}{'persist MSE':>12}{'HAR MSE':>12}{'LSTM MSE':>12} (MSE log-RV)")for h in [1, 5, 10]: errs = []for k inrange(1, n_splits +1): train_end = fold_size * k test_start = train_end test_end =min(train_end + fold_size, n)for i inrange(test_start, test_end - h): pred = log_rv[i -1] # y_hat(t+h) = y(t), pas de lookahead truth =float(np.mean(log_rv[i:i + h])) # cible identique au run errs.append(pred - truth) persist_mse =float(np.mean(np.square(errs))) lstm =float(rows[rows["horizon"] == h]["mse_lstm"].mean()) har =float(rows[rows["horizon"] == h]["mse_har"].mean())print(f"{h:>3}{persist_mse:>12.5f}{har:>12.5f}{lstm:>12.5f}")
Serie log-RV : 2278 jours (identique au run §C)
=== Baseline persistence (random walk) — plancher sans competence ===
h persist MSE HAR MSE LSTM MSE (MSE log-RV)
1 1.17312 0.88768 0.89088
5 0.96790 0.52197 0.45231
10 0.93031 0.57068 0.45589
5. Verdict (critere §C.6)
Verdict par horizon via la conjonction de la section 3 (edge >= 2σ etdm_p_median < 0,05, les deux reportes) avec la regle de dominance (seed BEATEN -> NO BEATS), puis verdict global. Un verdict NO BEATS est un resultat pleinement acceptable (#10908) — l’entree documente une mesure honnete, pas une victoire. Attention au piege inverse : une reduction MSE positive n’est pas un edge si la perte signee montre un biais systematique.
print("=== Verdict §C final — BTC-USD, LSTM vs HAR ===\n")for h insorted(int(k) for k in per_horizon): a = per_horizon[str(h)]print(f"h={h:>2} : {a['verdict_sc']:12s} "f"(edge {a['edge_pct']:+.1f}% ; 2σ {2* a['edge_std_pct']:+.1f}% ; "f"dm_p_median {a['dm_p_median']:.2e})")verdicts = [per_horizon[str(h)]["verdict_sc"] for h insorted(int(k) for k in per_horizon)]n_beats = verdicts.count("BEATS")n_inc = verdicts.count("INCONCLUSIVE")n_no = verdicts.count("NO BEATS")print(f"\nGlobal : {n_beats}/3 BEATS, {n_inc}/3 INCONCLUSIVE, {n_no}/3 NO BEATS (conjonction §C)")
=== Verdict §C final — BTC-USD, LSTM vs HAR ===
h= 1 : NO BEATS (edge -0.4% ; 2σ +7.3% ; dm_p_median 3.08e-09)
h= 5 : NO BEATS (edge +13.3% ; 2σ +8.9% ; dm_p_median 0.00e+00)
h=10 : NO BEATS (edge +20.1% ; 2σ +8.2% ; dm_p_median 0.00e+00)
Global : 0/3 BEATS, 0/3 INCONCLUSIVE, 3/3 NO BEATS (conjonction §C)
6. Provenance et reproductibilite (critere §C.7)
Data : Bitstamp_BTCUSD_1h_2014-20240808.csv (CryptoDataDownload, hourly 2014-2024) — sha256 38a4e973955cf9f8527c3096931aa958bfae09580737c909450504b21502c573
Le run §C brut (#10941) est instrumenté sur la jambe linear (perte signée). Depuis l’amendement du barème §C (#11010), cette jambe est un contrôle de biais séparé — jamais la jambe de la conjonction. La conjonction doit porter sur une perte de précision (mse/mae) : edge ≥ 2σ cross-seed etdm_p_median < 0,05. Cette section refait le recalcul indépendant des sections 3 et 5 sur le run --loss-fn mse (#11034).
Ce que le changement de jambe change : sous linear, d_mean = biais_LSTM − biais_HAR (dm_test.py L123-135) — le DM « détecte » le différentiel de biais, pas la précision. Sous mse, le DM teste l’égalité des pertes quadratiques : c’est la mesure de précision pure. Les deux jambes répondent à deux questions différentes ; seule la seconde porte le verdict §C.
mse_path = Path("scripts/results/m15_lstm_rv_btc_sc_mse/results.json")withopen(mse_path, encoding="utf-8") as f: data_m = json.load(f)rows_m = pd.DataFrame(data_m["combos"])per_horizon_m = data_m["per_horizon_sc"]cfg_m = data_mprint(f"Run mse : {len(rows_m)} combos BTC-USD ({rows_m['horizon'].nunique()} horizons x {rows_m['seed'].nunique()} seeds)")print(f"DM loss_fn : {cfg_m['loss_fn']} (perte de precision -- bareme §C amende #11010)")print(f"Runtime : {cfg_m['runtime_s']:.0f}s ({cfg_m['runtime_s']/60:.1f} min)")print("=== Resultats par (horizon, seed) — BTC-USD, DM loss_fn=mse ===")print(f"{'h':>3}{'seed':>4}{'LSTM MSE':>12}{'HAR MSE':>12}{'red %':>7}{'dm_stat':>9}{'dm_p':>9} verdict")for h insorted(rows_m["horizon"].unique()): sub = rows_m[rows_m["horizon"] == h].sort_values("seed")for _, r in sub.iterrows():print(f"{int(r['horizon']):>3}{int(r['seed']):>4}{r['mse_lstm']:>12.5f} "f"{r['mse_har']:>12.5f}{r['mse_reduction_pct']:>+6.1f}% "f"{r['dm_stat']:>9.2f}{r['dm_pvalue']:>9.2e}{r['dm_verdict']}")
Même règle que la section 3 : edge ≥ 2σ cross-seed etdm_p_median < 0,05 (perte de précision). Le verdict est comparé au run brut linear (#10941). Contrairement au re-run M4 (#11036, MSE bit-identiques), ce sweep ré-entraîne les LSTM (cuDNN non déterministe sur GPU) : les edges restent proches (± 1,6 pt) mais l’écart dominant entre les deux runs est bien la jambe DM (biais sous linear, précision sous mse), pas le modèle.
print("=== Conjonction §C mse par horizon (edge, sigma cross-seed, dm_p_median) ===")print()print(f"{'h':>3}{'edge (red moy %)':>16}{'sigma xs':>9}{'edge/2σ':>8}{'dm_p_med':>10} verdict_sc")for h insorted(int(k) for k in per_horizon_m): a = per_horizon_m[str(h)] ratio = a["edge_pct"] / (2* a["edge_std_pct"]) if a["edge_std_pct"] >0elsefloat("nan") flag =" <- conjonction §C tenue"if a["verdict_sc"] =="BEATS"else""print(f"{h:>3}{a['edge_pct']:>+14.1f}% {a['edge_std_pct']:>9.1f} "f"{ratio:>7.2f}x {a['dm_p_median']:>10.2e}{a['verdict_sc']}{flag}")print("=== Verdict §C compare — linear (#10941) vs mse (ce run) ===")print(f"{'h':>3}{'verdict linear':>14}{'verdict mse':>14}")for h insorted(int(k) for k in per_horizon): vl = per_horizon[str(h)]["verdict_sc"] vm = per_horizon_m[str(h)]["verdict_sc"]print(f"{h:>3}{vl:>14}{vm:>14}")
=== Conjonction §C mse par horizon (edge, sigma cross-seed, dm_p_median) ===
h edge (red moy %) sigma xs edge/2σ dm_p_med verdict_sc
1 -0.7% 3.7 -0.10x 3.63e-01 INCONCLUSIVE
5 +14.9% 4.6 1.60x 4.06e-02 BEATS <- conjonction §C tenue
10 +18.8% 4.7 2.00x 1.83e-02 BEATS <- conjonction §C tenue
=== Verdict §C compare — linear (#10941) vs mse (ce run) ===
h verdict linear verdict mse
1 NO BEATS INCONCLUSIVE
5 NO BEATS BEATS
10 NO BEATS BEATS
Le run §C brut (#10941, sections 2-3) conclut NO BEATS sous perte linear – un differentiel de biais, pas une mesure de precision. Le run loss_fn="mse" de la section 7 mesure la precision mais sur HAR biaisee, ce qui gonfle artificiellement le MSE de la baseline. Cette section annonce le de-biaisage symetrique demande par #12734 slice 2/2 :
Baseline HAR debiaisee : har_forecast_debiased = har_forecast - har_bias_oos (soustraction du biais OOS mesure sur test, jamais sur train – pas de fuite).
DM sur erreurs recentrees : e_a_centered = e_a - mean(e_a) et e_b_centered = e_b - mean(e_b) avec loss_fn="mse". Le centrage annule le biais (mean(loss_fn="linear") = 0), donc le DM compare les variances uniquement. C’est la jambe “precision” que l’amendement §C #11010 rend obligatoire.
Decomposition MSE = biais² + variance publiee par horizon pour LSTM et HAR (debiased).
Variance ratio var_LSTM / var_HAR_debiased par seed – indicateur de precision.
Pourquoi ce n’est pas une redite du run mse de la section 7 : la section 7 mesure la precision sur HAR biaisee (le biais gonfle artificiellement le MSE de la baseline). La section 8 debiaise les deux cotes et mesure la precision sur erreurs recentrees.
8.1 Patch de persistance dans m15_lstm_rv.py
Le ticket #12734 note que m15_lstm_rv.py ne persistait ni har_bias_oos ni les predictions brutes, rendant le keeper M15 BTC invérifiable post-hoc. Le patch ajoute dans le dict retourne par evaluate_one_combo :
har_bias_oos (scalar) : moyenne OOS des erreurs HAR = mean(har_pred - target), calculee sur l’echantillon OOS.
Persistance des arrays har_errors, lstm_errors inconditionnelle des que le guard DM tient (les deux series finies ET len >= 10, meme condition que har_bias_oos – pas de flag) pour permettre la decomposition MSE = biais² + variance post-run.
Le patch est retro-compatible : les anciens champs (sharpe_*, mse_*, dm_stat/pvalue/verdict) sont preserves. Les nouvelles colonnes har_bias_oos, har_errors, lstm_errors apparaissent dans chaque ligne (les arrays des que le guard DM tient) combos du JSON results.json.
8.2 Wrapper btc_m15.py
scripts/btc_m15.py est le pendant BTC-only de scripts/btc_vol.py (slice 1/2 M4). Il orchestre le run M15 BTC + applique la decomposition biais²+variance et le DM recentre. Les helpers _mse_decomposition et _dm_centered_mse vivent dans le module partage bias_metrics.py (extrait de btc_vol.py, issue #14363).
8.3 Commande de run complet (slice 2/2 dispatche au prochain cycle)
Pour le sweep complet (3 horizons x 4 seeds = 12 combos) il faut wrapper cette commande dans une boucle – le helper run_smoke dans btc_m15.py ne gere qu’un combo a la fois. Cout estime : ~10h CPU/GPU (12 combos x ~50min) – hors budget pour un cycle worker. Acceptance #4 du ticket #12734 autorise explicitement ce defer.
8.4 Verdict attendu
Le run livre par horizon :
edge_reduction_pct (mse_lstm vs mse_har_debiased, reduction % positive = LSTM gagne).
dm_centered_p_median (DM sur erreurs recentrees loss_fn=mse, variance comparee).
var_ratio_lstm_over_har_debiased (ratio variances, <1 = LSTM plus precis).
Verdict §C : edge >= 2σ cross-seed AND dm_p_median < 0.05, avec regle de dominance (seed BEATEN BY baseline -> NO BEATS).
8.5 Lecture du run complet — cluster 7 actifs apparié par origine (2026-10-01)
Le run différé de la section 8.3 est livré sous une forme étendue : cluster 7 actifs (84 combos, mse, refit 110, hidden 64) avec le protocole d’appariement par origine (#18190, port M4 #18650). La cellule ci-dessous lit le manifeste compact committé (scripts/results/m15_lstm_rv_cluster_aligned.json, politique #15890 — les séries complètes vivent hors dépôt avec empreinte SHA-256).
Correction de convention mesurée au premier combo : le garde shared-target a REFUSÉ le join naïf (gap 4,25) — la cible LSTM à l’origine i adresse la fenêtre [i+1, i+h] alors que la cible HAR à l’origine t adresse [t, t+h-1]. Le pairing retenu HAR origine t <-> LSTM origine t-1 compare deux prévisions de la même fenêtre réalisée sous la même frontière d’information ; il est validé gap 0 par construction sur chaque combo. Les verdicts §C historiques de M5/M15 (mixtes en convention) restent lisibles comme antécédents ; les verdicts ci-dessous sont la mesure corrigée.
# Section 8.5 -- lecture du manifeste cluster (7 actifs, appariement par origine)import jsonfrom pathlib import Pathmanifest_path = Path("scripts/results/m15_lstm_rv_cluster_aligned.json")manifest = json.loads(manifest_path.read_text(encoding="utf-8"))print(f"protocole : {manifest['protocol']}")cfg = manifest["config"]print(f"config : {len(cfg['coins'])} actifs, horizons {cfg['horizons']}, seeds {cfg['seeds']}, "f"refit {cfg['refit_every']}, loss {cfg['loss_fn']}")print(f"run : {manifest['total_rows']} combos en {manifest['elapsed_s']:.0f} s")art = manifest["artifact"]print(f"artefact : {art['path']} ({art['bytes']:,} octets, sha256 {art['sha256'][:16]}...)")def leg(cell, p_key, verdict_key): p = cell.get(p_key, float("nan"))returnf"p={p:.2e}"if p == p else"p=n/a"print()print("=== BTC-USD -- fermeture #12734 : keeper verifiable post-hoc, 3 jambes ===")print(f"{'cell':>14}{'brute':>14}{'calibree':>16}{'centree (variance)':>20}{'var_ratio':>9}")for key, cell in manifest["aggregated"].items():ifnot key.startswith("BTC-USD"):continue vr = cell.get("var_ratio_lstm_over_har_median", float("nan"))print(f"{key:>14}{cell['verdict_sc']:>14}{cell['verdict_sc_calibrated']:>16} "f"{cell['verdict_sc_centered']:>20}{vr:>9.3f}")print()print("=== Cluster complet -- verdicts par cellule (3 jambes) ===")print(f"{'cell':>14}{'brute':>14}{'p brute':>10}{'calibree':>16}{'p cal':>10}{'centree':>18}{'p ctr':>10}")for key, cell in manifest["aggregated"].items():print(f"{key:>14}{cell['verdict_sc']:>14}{leg(cell, 'dm_p_median', 'verdict_sc'):>10} "f"{cell['verdict_sc_calibrated']:>16}{leg(cell, 'calibrated_dm_p_median', 'verdict_sc_calibrated'):>10} "f"{cell['verdict_sc_centered']:>18}{leg(cell, 'dm_centered_p_median', 'verdict_sc_centered'):>10}")align_bad = [a for a in manifest["alignment"] if (a.get("n_target_mismatch") or0) >0]gap_max =max((a.get("dm_target_gap_max") or0.0) for a in manifest["alignment"])n_min =min((a.get("dm_cal_n_aligned_min") or0) for a in manifest["alignment"])print()print(f"alignement : gap max = {gap_max:.1e}, n_joined min = {n_min}, "f"cellules TARGET_MISMATCH = {len(align_bad)}")
protocole : paired-origin cluster revalidation (#18190 port): DM legs join walk-forwards on common origin dates and refuse on shared-target mismatch, never positional truncation
config : 7 actifs, horizons [1, 5, 10], seeds [0, 1, 7, 42], refit 110, loss mse
run : 84 combos en 3077 s
artefact : results.json (12,278,066 octets, sha256 909244cbeb972108...)
=== BTC-USD -- fermeture #12734 : keeper verifiable post-hoc, 3 jambes ===
cell brute calibree centree (variance) var_ratio
BTC-USD|h=1 NO BEATS NO BEATS BEATEN (variance) 1.132
BTC-USD|h=5 INCONCLUSIVE NO BEATS BEATEN (variance) 1.168
BTC-USD|h=10 INCONCLUSIVE NO BEATS BEATEN (variance) 1.312
=== Cluster complet -- verdicts par cellule (3 jambes) ===
cell brute p brute calibree p cal centree p ctr
BTC-USD|h=1 NO BEATS p=3.97e-02 NO BEATS p=1.56e-03 BEATEN (variance) p=4.20e-04
BTC-USD|h=5 INCONCLUSIVE p=1.95e-01 NO BEATS p=4.07e-02 BEATEN (variance) p=8.97e-03
BTC-USD|h=10 INCONCLUSIVE p=5.29e-02 NO BEATS p=5.63e-03 BEATEN (variance) p=1.65e-04
ETH-USD|h=1 NO BEATS p=3.06e-07 NO BEATS p=5.17e-07 BEATEN (variance) p=1.17e-07
ETH-USD|h=5 NO BEATS p=3.36e-03 NO BEATS p=2.08e-03 BEATEN (variance) p=1.00e-04
ETH-USD|h=10 NO BEATS p=5.50e-03 NO BEATS p=1.64e-03 BEATEN (variance) p=5.31e-06
SOL-USD|h=1 NO BEATS p=7.72e-05 NO BEATS p=1.91e-03 BEATEN (variance) p=3.25e-03
SOL-USD|h=5 NO BEATS p=1.60e-04 NO BEATS p=8.20e-02 BEATEN (variance) p=1.32e-02
SOL-USD|h=10 NO BEATS p=5.17e-05 INCONCLUSIVE p=9.94e-02 BEATEN (variance) p=1.38e-02
LTC-USD|h=1 NO BEATS p=3.07e-05 NO BEATS p=1.07e-04 BEATEN (variance) p=9.26e-04
LTC-USD|h=5 NO BEATS p=1.29e-05 NO BEATS p=1.13e-03 BEATEN (variance) p=1.60e-03
LTC-USD|h=10 NO BEATS p=2.63e-04 NO BEATS p=5.60e-02 BEATEN (variance) p=2.89e-02
XRP-USD|h=1 NO BEATS p=2.85e-05 NO BEATS p=2.87e-04 BEATEN (variance) p=2.44e-03
XRP-USD|h=5 NO BEATS p=3.28e-03 INCONCLUSIVE p=1.59e-01 INCONCLUSIVE p=8.33e-02
XRP-USD|h=10 NO BEATS p=6.26e-03 INCONCLUSIVE p=2.71e-01 INCONCLUSIVE p=8.52e-02
ADA-USD|h=1 NO BEATS p=5.83e-04 NO BEATS p=2.59e-03 BEATEN (variance) p=1.96e-02
ADA-USD|h=5 NO BEATS p=1.87e-06 NO BEATS p=4.45e-04 BEATEN (variance) p=2.57e-04
ADA-USD|h=10 NO BEATS p=2.23e-05 NO BEATS p=8.34e-03 BEATEN (variance) p=1.50e-02
DOT-USD|h=1 NO BEATS p=1.37e-03 NO BEATS p=6.19e-03 BEATEN (variance) p=9.95e-03
DOT-USD|h=5 NO BEATS p=2.33e-04 NO BEATS p=2.08e-02 BEATEN (variance) p=3.64e-03
DOT-USD|h=10 NO BEATS p=1.31e-04 NO BEATS p=1.08e-01 BEATEN (variance) p=7.47e-03
alignement : gap max = 3.6e-15, n_joined min = 545, cellules TARGET_MISMATCH = 0