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 M4 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).
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 toutes les 22 obs
2
Multi-seed
4 seeds {0, 7, 42, 99}, 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)
import jsonfrom pathlib import Pathimport numpy as npimport pandas as pdresults_path = Path("scripts/results/m4_dlinear_vol_btc_sc.json")withopen(results_path, encoding="utf-8") as f: data = json.load(f)rows = pd.DataFrame(data["rows"])agg = pd.DataFrame(data["aggregated"])cfg = data["config"]print(f"Model : DLinear-vol (Linear seq_len={cfg['seq_len']} -> horizon, ~22 params)")print(f"Run §C : {len(rows)} combos BTC-USD ({rows['horizon'].nunique()} horizons x {rows['seed'].nunique()} seeds)")print(f"Seeds : {cfg['seeds']}")print(f"Walk-forward : {cfg['n_splits']}-fold, refit={cfg['refit_every']}d, {cfg['epochs']} epochs")print(f"DM loss_fn : {cfg['loss_fn']} (perte signee, preserve le signe -- bareme §C)")print(f"Runtime : {data['elapsed_s']:.0f}s ({data['elapsed_s']/60:.1f} min)")# Verifications de conformite du run (bareme §C) -- rapportes, jamais bloquantsok =Truechecks = [ ("loss_fn = linear", cfg["loss_fn"] =="linear"), (">= 4 seeds parmi {0,1,7,42,99}", set(cfg["seeds"]) >= {0, 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 : DLinear-vol (Linear seq_len=22 -> horizon, ~22 params)
Run §C : 12 combos BTC-USD (3 horizons x 4 seeds)
Seeds : [0, 7, 42, 99]
Walk-forward : 5-fold, refit=22d, 100 epochs
DM loss_fn : linear (perte signee, preserve le signe -- bareme §C)
Runtime : 3508s (58.5 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 DLinear vs HAR, reduction MSE relative, 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}{'DLinear 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['dlinear_mse_logrv']:>12.5f} "f"{r['har_mse_logrv']:>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 DLinear MSE HAR MSE red % dm_stat dm_p verdict
1 0 0.75129 0.88768 +15.4% 39.04 0.00e+00 BEATEN BY baseline
1 7 0.75216 0.88768 +15.3% 40.12 0.00e+00 BEATEN BY baseline
1 42 0.75171 0.88768 +15.3% 39.42 0.00e+00 BEATEN BY baseline
1 99 0.75200 0.88768 +15.3% 39.99 0.00e+00 BEATEN BY baseline
5 0 0.37434 0.52197 +28.3% 42.57 0.00e+00 BEATEN BY baseline
5 7 0.37444 0.52197 +28.3% 43.38 0.00e+00 BEATEN BY baseline
5 42 0.37313 0.52197 +28.5% 42.78 0.00e+00 BEATEN BY baseline
5 99 0.37424 0.52197 +28.3% 43.19 0.00e+00 BEATEN BY baseline
10 0 0.35292 0.57068 +38.2% 47.02 0.00e+00 BEATEN BY baseline
10 7 0.35360 0.57068 +38.0% 46.05 0.00e+00 BEATEN BY baseline
10 42 0.35118 0.57068 +38.5% 46.03 0.00e+00 BEATEN BY baseline
10 99 0.35087 0.57068 +38.5% 46.29 0.00e+00 BEATEN BY baseline
La conjonction exige deux resultats independants, reportes separement :
edge = reduction MSE moyenne (DLinear vs HAR) sur les seeds ;
sigma cross-seed = ecart-type de cette 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(rows["horizon"].unique()): a = agg[agg["horizon"] == h].iloc[0] ratio = a["mean_reduction_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"{int(h):>3}{a['mean_reduction_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)print("\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)) 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 = agg[agg["horizon"] == h].iloc[0] match = v == a["verdict_sc"] ok = ok and matchprint(f" h={int(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 +15.3% 0.0 205.42x 0.00e+00 NO BEATS
5 +28.3% 0.1 139.71x 0.00e+00 NO BEATS
10 +38.3% 0.2 95.12x 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. DLinear 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 intraday_loader import load_bitstamp_btc, hourly_log_returnsfrom realized_variance import daily_realized_variance, realized_variance_to_logfrom dlinear_vol import _make_split_indices # meme decoupage temporel que le run §C# Serie identique au run §C : memes fonctions du pipeline, memes donnees.btc = load_bitstamp_btc()rets = hourly_log_returns(btc)rv = daily_realized_variance(rets)log_rv = realized_variance_to_log(rv).values.astype(float)n =len(log_rv)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}{'DLinear MSE':>12} (MSE log-RV)")for h in [1, 5, 10]: errs = []for train_end, test_start, test_end in _make_split_indices(n, n_splits=5):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))) dl =float(rows[rows["horizon"] == h]["dlinear_mse_logrv"].mean()) har =float(rows[rows["horizon"] == h]["har_mse_logrv"].mean())print(f"{h:>3}{persist_mse:>12.5f}{har:>12.5f}{dl:>12.5f}")
Serie log-RV : 2278 jours (identique au run §C)
=== Baseline persistence (random walk) — plancher sans competence ===
h persist MSE HAR MSE DLinear MSE (MSE log-RV)
1 1.17312 0.88768 0.75179
5 0.96790 0.52197 0.37404
10 0.93031 0.57068 0.35214
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, DLinear vs HAR ===\n")for h insorted(agg["horizon"].unique()): a = agg[agg["horizon"] == h].iloc[0]print(f"h={int(h):>2} : {a['verdict_sc']:12s} "f"(edge {a['mean_reduction_pct']:+.1f}% ; 2σ {2* a['edge_std_pct']:+.1f}% ; "f"dm_p_median {a['dm_p_median']:.2e})")n_beats =int((agg["verdict_sc"] =="BEATS").sum())n_inc =int((agg["verdict_sc"] =="INCONCLUSIVE").sum())n_no =int((agg["verdict_sc"] =="NO BEATS").sum())print(f"\nGlobal : {n_beats}/3 BEATS, {n_inc}/3 INCONCLUSIVE, {n_no}/3 NO BEATS (conjonction §C)")
=== Verdict §C final — BTC-USD, DLinear vs HAR ===
h= 1 : NO BEATS (edge +15.3% ; 2σ +0.1% ; dm_p_median 0.00e+00)
h= 5 : NO BEATS (edge +28.3% ; 2σ +0.2% ; dm_p_median 0.00e+00)
h=10 : NO BEATS (edge +38.3% ; 2σ +0.4% ; 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 (#10930) conclut NO BEATS : sous perte signee (loss_fn="linear"), les 4 seeds sont BEATEN BY baseline (dm_mean_loss_diff ≈ +0,22 log-RV, h=1, quasi deterministe). Or mean_loss_diff est une difference de biais (biais_DL − biais_HAR) : l’attribuer entierement a DLinear supposait « HAR (OLS) non biaise ». Cette section mesure les deux cotes puis re-teste :
le biais propre de HAR out-of-sample est mesure (#10938 acceptance #1) — champ har_bias_oos ;
DLinear est de-biaise honnetement : biais estime sur le fold d’entrainement uniquement (erreur signee moyenne modele-vs-cible), soustrait des previsions de test — jamais de biais estime sur le test (look-ahead interdit, #10938 acceptance #2) ;
run complet : 5 folds, seeds {0, 7, 42, 99}, --loss-fn linear --debias.
La decomposition tranchera l’attribution : biais porte par DLinear (de-biaisage le corrige) ou par HAR (le differentiel lineaire recompense la sous-prevision de la baseline). De-biasing implemente dans scripts/dlinear_vol.py : _train_bias(model, x_train, y_train) -> pred_raw -= biais_entraine * train_std (decalage additif de la cible).
res2_path = Path("scripts/results/m4_dlinear_vol_btc_sc_debiased.json")withopen(res2_path, encoding="utf-8") as f: data2 = json.load(f)rows2 = pd.DataFrame(data2["rows"])agg2 = pd.DataFrame(data2["aggregated"])cfg2 = data2["config"]print(f"Run de-biaise : {len(rows2)} combos BTC-USD, debias={cfg2['debias']}, "f"loss_fn={cfg2['loss_fn']}, refit={cfg2['refit_every']}d, {data2['elapsed_s']:.0f}s")print("\nHAR bias OOS par horizon (mesure, acceptance #1) :")for h insorted(rows2["horizon"].unique()): hb = rows2[rows2["horizon"] == h]["har_bias_oos"]print(f" h={h}: {hb.iloc[0]:+.4f} log-RV (HAR sur-previent si +, sous-previent si -)")print("\n=== Resultats par (horizon, seed) — de-biaise, DM loss_fn=linear ===")print(f"{'h':>3}{'seed':>4}{'DL MSE brut':>12}{'DL MSE de-b':>12}{'HAR MSE':>12}{'dm_stat':>9}{'dm_p':>9} verdict")for h insorted(rows["horizon"].unique()): sub = rows2[rows2["horizon"] == h].sort_values("seed") raw_sub = rows[rows["horizon"] == h].set_index("seed")for _, r in sub.iterrows(): raw_mse =float(raw_sub.loc[r["seed"], "dlinear_mse_logrv"])print(f"{int(r['horizon']):>3}{int(r['seed']):>4}{raw_mse:>12.5f} "f"{r['dlinear_mse_logrv']:>12.5f}{r['har_mse_logrv']:>12.5f} "f"{r['dm_stat']:>9.2f}{r['dm_pvalue']:>9.2e}{r['dm_verdict']}")
Run de-biaise : 12 combos BTC-USD, debias=True, loss_fn=linear, refit=22d, 3964s
HAR bias OOS par horizon (mesure, acceptance #1) :
h=1: -0.2266 log-RV (HAR sur-previent si +, sous-previent si -)
h=5: -0.3432 log-RV (HAR sur-previent si +, sous-previent si -)
h=10: -0.4502 log-RV (HAR sur-previent si +, sous-previent si -)
=== Resultats par (horizon, seed) — de-biaise, DM loss_fn=linear ===
h seed DL MSE brut DL MSE de-b HAR MSE dm_stat dm_p verdict
1 0 0.75129 0.75118 0.88768 40.14 0.00e+00 BEATEN BY baseline
1 7 0.75216 0.75202 0.88768 40.93 0.00e+00 BEATEN BY baseline
1 42 0.75171 0.75160 0.88768 40.55 0.00e+00 BEATEN BY baseline
1 99 0.75200 0.75179 0.88768 41.01 0.00e+00 BEATEN BY baseline
5 0 0.37434 0.37432 0.52197 42.67 0.00e+00 BEATEN BY baseline
5 7 0.37444 0.37446 0.52197 43.47 0.00e+00 BEATEN BY baseline
5 42 0.37313 0.37304 0.52197 42.88 0.00e+00 BEATEN BY baseline
5 99 0.37424 0.37414 0.52197 43.23 0.00e+00 BEATEN BY baseline
10 0 0.35292 0.35297 0.57068 46.95 0.00e+00 BEATEN BY baseline
10 7 0.35360 0.35348 0.57068 45.96 0.00e+00 BEATEN BY baseline
10 42 0.35118 0.35107 0.57068 45.87 0.00e+00 BEATEN BY baseline
10 99 0.35087 0.35075 0.57068 46.28 0.00e+00 BEATEN BY baseline
7.1 Conjonction de-biaisee
Meme regle que la section 3 : edge >= 2σ cross-seed etdm_p_median < 0,05 (reportes separement), regle de dominance (seed BEATEN -> NO BEATS). La lecture croisee brut vs de-biaise repond a la question de #10938 : le biais plafonnait-il l’edge (de-biaise -> edge croit) ou le fabriquait-il (de-biaise -> edge disparait) ?
print("=== Conjonction §C de-biaisee par horizon (edge, sigma cross-seed, dm_p_median) ===\n")print(f"{'h':>3}{'edge brut':>11}{'edge de-b':>11}{'sigma xs':>9}{'dm_p_med':>10} verdict_sc")for h insorted(rows2["horizon"].unique()): a = agg2[agg2["horizon"] == h].iloc[0] a_raw = agg[agg["horizon"] == h].iloc[0] flag =" <- conjonction de-biaisee tenue"if a["verdict_sc"] =="BEATS"else""print(f"{int(h):>3}{a_raw['mean_reduction_pct']:>+9.1f}% {a['mean_reduction_pct']:>+9.1f}% "f"{a['edge_std_pct']:>9.1f}{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 (de-biaise)")ok =Truefor h insorted(rows2["horizon"].unique()): sub = rows2[rows2["horizon"] == h] red = sub["mse_reduction_pct"].to_numpy() edge =float(np.mean(red)) 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 = agg2[agg2["horizon"] == h].iloc[0] match = v == a["verdict_sc"] ok = ok and matchprint(f" h={int(h)}: recalcul={v:12s} agrege={a['verdict_sc']:12s}{'OK'if match else'MISMATCH'}")print("Recalcul conforme :", "OK"if ok else"MISMATCH")
=== Conjonction §C de-biaisee par horizon (edge, sigma cross-seed, dm_p_median) ===
h edge brut edge de-b sigma xs dm_p_med verdict_sc
1 +15.3% +15.3% 0.0 0.00e+00 NO BEATS
5 +28.3% +28.4% 0.1 0.00e+00 NO BEATS
10 +38.3% +38.3% 0.2 0.00e+00 NO BEATS
Verification : recalcul depuis les rows brutes (de-biaise)
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
7.2 Verdict compare (brut vs de-biaise)
Verdict §C brut (#10930) et de-biaise cote a cote, avec la decomposition du differentiel lineaire : mean_loss_diff = biais_DL(de-biaise) − biais_HAR(mesure). C’est cette decomposition qui repond a #10938 — le verdict final reste celui de la conjonction §C, l’attribution du biais est la lecture.
print("=== Verdict §C compare — brut vs de-biaise ===\n")print(f"{'h':>3}{'brut':>14}{'de-biaise':>14}")n2_beats = n2_inc = n2_no =0for h insorted(rows2["horizon"].unique()): a1 = agg[agg["horizon"] == h].iloc[0] a2 = agg2[agg2["horizon"] == h].iloc[0]print(f"{int(h):>3}{a1['verdict_sc']:>14}{a2['verdict_sc']:>14}")if a2["verdict_sc"] =="BEATS": n2_beats +=1elif a2["verdict_sc"] =="INCONCLUSIVE": n2_inc +=1else: n2_no +=1print(f"\nDe-biaise global : {n2_beats}/3 BEATS, {n2_inc}/3 INCONCLUSIVE, {n2_no}/3 NO BEATS (conjonction §C)")# Decomposition du differentiel lineaire (perte linear : L(e) = e) :# mean_loss_diff = mean(e_DL) - mean(e_HAR) = biais_DL - biais_HAR# => biais_DL(residuel, de-biaise) = mean_loss_diff + har_bias_oos# => part du differentiel attributable a HAR = -har_bias_oos / mean_loss_diffprint("\n=== Decomposition du differentiel lineaire (de-biaise) ===")print(f"{'h':>3}{'mean_loss_diff':>15}{'biais_HAR':>10}{'biais_DL resid':>14}{'part HAR':>9}")for h insorted(rows2["horizon"].unique()): sub = rows2[rows2["horizon"] == h] mld =float(sub["dm_mean_loss_diff"].mean()) hb =float(sub["har_bias_oos"].mean()) bdl = mld + hb part = (-hb / mld *100.0) ifabs(mld) >1e-12elsefloat("nan")print(f"{int(h):>3}{mld:>+15.4f}{hb:>+10.4f}{bdl:>+14.4f}{part:>8.1f}%")
=== Verdict §C compare — brut vs de-biaise ===
h brut de-biaise
1 NO BEATS NO BEATS
5 NO BEATS NO BEATS
10 NO BEATS NO BEATS
De-biaise global : 0/3 BEATS, 0/3 INCONCLUSIVE, 3/3 NO BEATS (conjonction §C)
=== Decomposition du differentiel lineaire (de-biaise) ===
h mean_loss_diff biais_HAR biais_DL resid part HAR
1 +0.2282 -0.2266 +0.0016 99.3%
5 +0.3512 -0.3432 +0.0080 97.7%
10 +0.4513 -0.4502 +0.0011 99.8%
De-biasing : scripts/dlinear_vol.py — biais = erreur signee moyenne modele-vs-cible sur le fold train (_train_bias), soustrait des previsions de test ; aucune fuite test (acceptance #2).
HAR bias OOS : champ har_bias_oos par ligne (acceptance #1).
Donnees : identiques au run brut (Bitstamp_BTCUSD_1h_2014-20240808.csv, sha256 38a4e973955cf9f8527c3096931aa958bfae09580737c909450504b21502c573).
Entree REGISTRY mise a jour : brut + de-biaise cote a cote (issue #10938, #10908, #10930).
8. Re-run §C perte de précision (issue #11011)
Le run §C brut (#10930) et le run dé-biaisé (section 7, #10938) sont instrumentés 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 de la section 3 sur le run --loss-fn mse (#11011).
Ce que le changement de jambe change : sous linear, d_mean = biais_DL − 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/m4_dlinear_vol_btc_sc_mse.json")withopen(mse_path, encoding="utf-8") as f: data_m = json.load(f)rows_m = pd.DataFrame(data_m["rows"])agg_m = pd.DataFrame(data_m["aggregated"])cfg_m = data_m["config"]print(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 : {data_m['elapsed_s']:.0f}s ({data_m['elapsed_s']/60:.1f} min)")print("=== Resultats par (horizon, seed) — BTC-USD, DM loss_fn=mse ===")print(f"{'h':>3}{'seed':>4}{'DLinear 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['dlinear_mse_logrv']:>12.5f} "f"{r['har_mse_logrv']:>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 (#10930) : la jambe change le sens du DM, pas les MSE — c’est précisément l’écart que l’amendement #11010 rend visible.
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(rows_m["horizon"].unique()): a = agg_m[agg_m["horizon"] == h].iloc[0] ratio = a["mean_reduction_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"{int(h):>3}{a['mean_reduction_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 (#10930) vs mse (ce run) ===")print(f"{'h':>3}{'verdict linear':>14}{'verdict mse':>14}")for h insorted(rows_m["horizon"].unique()): vl = agg[agg["horizon"] == h].iloc[0]["verdict_sc"] vm = agg_m[agg_m["horizon"] == h].iloc[0]["verdict_sc"]print(f"{int(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 +15.3% 0.0 205.42x 0.00e+00 BEATS <- conjonction §C tenue
5 +28.3% 0.1 139.71x 2.25e-10 BEATS <- conjonction §C tenue
10 +38.3% 0.2 95.12x 2.39e-09 BEATS <- conjonction §C tenue
=== Verdict §C compare — linear (#10930) vs mse (ce run) ===
h verdict linear verdict mse
1 NO BEATS BEATS
5 NO BEATS BEATS
10 NO BEATS BEATS
Le run §C brut (#10930, section 2-3) conclut NO BEATS sous perte linear — un différentiel de biais, pas une mesure de précision. Le run dé-biaisé de la section 7 (modèle dé-biaisé) ne corrige pas le côté baseline : la baseline HAR conserve son biais OOS. Le run loss_fn="mse" de la section 8 mesure la précision mais sur HAR biaisé, ce qui gonfle artificiellement le MSE de la baseline (le biais entre dans MSE = biais² + variance). Cette section applique le dé-biaisage symétrique demandé par #12734 :
Baseline HAR dé-biaisée : har_forecast_debiased = har_forecast − har_bias_oos (soustraction du biais OOS mesuré sur test, jamais sur train — pas de fuite, acceptance #2 de #10938) ; har_mse_logrv_debiased recalculé.
DM sur erreurs recentrées : 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 « précision » que l’amendement §C #11010 rend obligatoire pour porter le verdict BEATS.
Pourquoi ce n’est pas une redite du run dé-biaisé de la section 7 : la section 7 dé-biaise DLinear mais laisse HAR biaisée. La section 8 mesure la précision sur HAR biaisée. La section 9 dé-biaise les deux côtés et mesure la précision sur erreurs recentrées — c’est la décomposition biais²+variance complète appliquée à la conjonction §C.
Origine numérique : ce run répond à la question laissée ouverte par #12734 — « BTC keepers M4 (et M15 si temps GPU le permet) survivront-ils hors biais ? » M4 BTC : la décomposition MSE = biais² + variance publiée par horizon, le DM recentré p_value médian et le verdict §C. M15 BTC : patch de persistance biais + rerun (~35 min GPU).
from pathlib import Pathimport numpy as npimport pandas as pdres3_path = Path("scripts/results/m4_dlinear_vol_btc_sc_debiased_recentered.json")withopen(res3_path, encoding="utf-8") as f: data3 = json.load(f)rows3 = pd.DataFrame(data3["rows"])agg3 = pd.DataFrame(data3["aggregated"])cfg3 = data3["config"]print(f"Run de-biaise recentre : {len(rows3)} combos BTC-USD, HAR debiased=True, DM centered=True")print(f"Loss-fn DM : {cfg3['loss_fn']} sur erreurs recentrees")print(f"Seeds : {cfg3['seeds']} | Horizons : {cfg3['horizons']} | Runtime : {data3['elapsed_s']:.0f}s ({data3['elapsed_s']/60:.1f} min)")print("\n=== HAR bias OOS et decomposition biais²+variance (HAR brut vs HAR debiaise) ===")print(f"{'h':>3}{'HAR raw MSE':>11}{'HAR deb MSE':>11}{'bias_oos':>9}{'HAR var raw':>12}{'HAR var deb':>12}{'HAR bias²/var':>13}")for h insorted(rows3["horizon"].unique()): sub = rows3[rows3["horizon"] == h] raw_mse =float(sub["har_mse_logrv_raw"].mean()) deb_mse =float(sub["har_mse_logrv_debiased"].mean()) bias =float(sub["har_bias_oos"].mean()) var_raw =float(sub["har_variance_raw"].mean()) var_deb =float(sub["har_variance_debiased"].mean()) ratio = bias **2/ var_deb if var_deb >0elsefloat("nan")print(f"{int(h):>3}{raw_mse:>11.5f}{deb_mse:>11.5f}{bias:>+9.4f}{var_raw:>12.5f}{var_deb:>12.5f}{ratio:>12.4f}")
Run de-biaise recentre : 12 combos BTC-USD, HAR debiased=True, DM centered=True
Loss-fn DM : mse sur erreurs recentrees
Seeds : [0, 7, 42, 99] | Horizons : [1, 5, 10] | Runtime : 1776s (29.6 min)
=== HAR bias OOS et decomposition biais²+variance (HAR brut vs HAR debiaise) ===
h HAR raw MSE HAR deb MSE bias_oos HAR var raw HAR var deb HAR bias²/var
1 0.88768 0.83633 -0.2266 0.83633 0.83633 0.0614
5 0.52197 0.40420 -0.3432 0.40420 0.40420 0.2914
10 0.57068 0.36799 -0.4502 0.36799 0.36799 0.5508
9.1 Résultats par seed — DM recentré (HAR débiaisée)
Métriques par seed : MSE DLinear, MSE HAR débiaisée, reduction relative, et test DM sur erreurs recentrées (e − mean(e) par prévisionneur), loss_fn="mse". Le DM recentré compare les variances après centrage — la jambe « précision » que l’amendement §C #11010 demande.
print("=== Resultats par (horizon, seed) — BTC-USD, DM loss_fn=mse sur erreurs recentrees ===\n")print(f"{'h':>3}{'seed':>4}{'DL MSE':>11}{'HAR deb MSE':>12}{'red %':>7}{'var ratio DL/HR':>16}{'dm_stat':>9}{'dm_p':>9} verdict")for h insorted(rows3["horizon"].unique()): sub = rows3[rows3["horizon"] == h].sort_values("seed")for _, r in sub.iterrows():print(f"{int(r['horizon']):>3}{int(r['seed']):>4}{r['dlinear_mse_logrv']:>11.5f} "f"{r['har_mse_logrv_debiased']:>12.5f}{r['mse_reduction_pct_vs_debiased_har']:>+6.1f}% "f"{r['dlinear_variance'] / r['har_variance_debiased']:>15.4f} "f"{r['dm_centered_stat']:>9.2f}{r['dm_centered_pvalue']:>9.2e}{r['dm_centered_verdict']}")
9.2 Décomposition biais² + variance (DLinear vs HAR débiaisée)
Decomposition MSE = biais² + variance par horizon, pour les deux modeles. C’est le diagnostic que #12695 documente sur ETF, applique ici a BTC.
har_bias_share_of_mse_debiased : part du MSE HAR debiaise qui reste attribuable au biais. Apres de-biaisage, c’est le residuel numerique (autour de 5 a 6 %), le reste etant la variance pure.
var_ratio_dl_over_har_debiased : ratio variance DLinear / variance HAR debiaisee. Si < 1, DLinear est plus precise (variance plus petite).
print("=== Decomposition biais² + variance — BTC-USD (HAR debiaisee) ===\n")print(f"{'h':>3}{'DL bias²':>10}{'DL var':>10}{'HAR deb bias²':>14}{'HAR deb var':>11}{'var ratio':>10}")for h insorted(rows3["horizon"].unique()): sub = rows3[rows3["horizon"] == h] dl_b2 =float(sub["dlinear_bias_sq"].mean()) dl_v =float(sub["dlinear_variance"].mean()) har_b2 =float(sub["har_bias_sq_debiased"].mean()) har_v =float(sub["har_variance_debiased"].mean()) ratio = dl_v / har_v if har_v >0elsefloat("nan")print(f"{int(h):>3}{dl_b2:>10.5f}{dl_v:>10.5f}{har_b2:>14.5f}{har_v:>11.5f}{ratio:>10.4f}")print("\nLecture : var_ratio < 1 = DLinear plus precis (variance plus petite). har_bias²/var proche de 0 = le de-biaisage a elimine le biais.")
=== Decomposition biais² + variance — BTC-USD (HAR debiaisee) ===
h DL bias² DL var HAR deb bias² HAR deb var var ratio
1 0.00003 0.75186 0.00000 0.83633 0.8990
5 0.00003 0.37472 0.00000 0.40420 0.9271
10 0.00000 0.35439 0.00000 0.36799 0.9631
Lecture : var_ratio < 1 = DLinear plus precis (variance plus petite). har_bias²/var proche de 0 = le de-biaisage a elimine le biais.
9.3 Conjonction §C recentrée + verdict final
Meme regle que la section 3 : edge >= 2σ cross-seed ET dm_p_median < 0,05, les deux reportes separement, regle de dominance (seed BEATEN -> NO BEATS). Mais ici l’edge est sur la reduction MSE vs HAR debiaisee, et le DM est sur les erreurs recentrees (la jambe precision).
Le verdict repond a #12734 : M4 BTC survit-il hors biais ? La reponse depend du verdict par horizon.
print("=== Conjonction §C recentree par horizon ===\n")print(f"{'h':>3}{'edge (red %)':>13}{'sigma xs':>9}{'edge/2σ':>8}{'dm_p_med':>10}{'var ratio':>10} verdict_sc")for h insorted(agg3["horizon"].unique()): a = agg3[agg3["horizon"] == h].iloc[0] ratio = a["edge_reduction_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"{int(h):>3}{a['edge_reduction_pct']:>+11.1f}% {a['edge_std_pct']:>9.1f} "f"{ratio:>7.2f}x {a['dm_centered_p_median']:>10.2e}{a['var_ratio_dl_over_har_debiased']:>10.4f}{a['verdict_sc']}{flag}")# Recalcul independant depuis les rows brutes (auditabilite)print("\nVerification : recalcul depuis les rows brutes (meme regle que le script)")ok =Truefor h insorted(rows3["horizon"].unique()): sub = rows3[rows3["horizon"] == h] red = sub["mse_reduction_pct_vs_debiased_har"].to_numpy() edge =float(np.mean(red)) edge_std =float(np.std(red)) dm_med =float(np.median(sub["dm_centered_pvalue"]))ifany("BEATEN"in v for v in sub["dm_centered_verdict"]): v ="NO BEATS"elif edge >=2* edge_std and dm_med <0.05: v ="BEATS"else: v ="INCONCLUSIVE" a = agg3[agg3["horizon"] == h].iloc[0] match = v == a["verdict_sc"] ok = ok and matchprint(f" h={int(h)}: recalcul={v:12s} agrege={a['verdict_sc']:12s}{'OK'if match else'MISMATCH'}")print("Recalcul conforme :", "OK"if ok else"MISMATCH")# Verdict globaln_beats =int((agg3["verdict_sc"] =="BEATS").sum())n_inc =int((agg3["verdict_sc"] =="INCONCLUSIVE").sum())n_no =int((agg3["verdict_sc"] =="NO BEATS").sum())print(f"\nVerdict global : {n_beats}/3 BEATS, {n_inc}/3 INCONCLUSIVE, {n_no}/3 NO BEATS (conjonction §C recentree)")
=== Conjonction §C recentree par horizon ===
h edge (red %) sigma xs edge/2σ dm_p_med var ratio verdict_sc
1 +10.1% 0.1 33.80x 1.54e-09 0.8990 BEATS <- conjonction §C tenue
5 +7.3% 0.2 15.55x 9.66e-05 0.9271 BEATS <- conjonction §C tenue
10 +3.7% 0.2 9.79x 1.02e-01 0.9631 INCONCLUSIVE
Verification : recalcul depuis les rows brutes (meme regle que le script)
h=1: recalcul=BEATS agrege=BEATS OK
h=5: recalcul=BEATS agrege=BEATS OK
h=10: recalcul=INCONCLUSIVE agrege=INCONCLUSIVE OK
Recalcul conforme : OK
Verdict global : 2/3 BEATS, 1/3 INCONCLUSIVE, 0/3 NO BEATS (conjonction §C recentree)
9.4 Verdict comparé et provenance
Les trois runs cote a cote :
Run brut (#10930, section 2-3) : verdict sur loss_fn="linear", modele et baseline bruts. NO BEATS (DM signe).
Run dé-biaisé asymétrique (section 7) : modèle dé-biaisé, baseline brute. NO BEATS (DM signé).
Run mse asymétrique (section 8) : modèle brut, baseline brute, loss_fn="mse". BEATS sur h=1/5/10 (DM précision sur HAR biaisée — la baseline gonfle son MSE par son biais).
Run dé-biaisé symétrique recentré (cette section, #12734) : modèle brut, baseline débiaisée, DM sur erreurs recentrées loss_fn="mse". Verdict : à mesurer sur les 3 horizons.
Données : identiques aux runs précédents (Bitstamp_BTCUSD_1h_2014-20240808.csv, sha256 38a4e973955cf9f8527c3096931aa958bfae09580737c909450504b21502c573).
Script : scripts/btc_vol.py — wrapper BTC-only de dlinear_vol.py qui orchestre le mode dé-biaisé récentré (HAR debiased + DM centered). Le coeur multi-coin scripts/dlinear_vol.py reste inchangé pour préserver les runs ETF.
Entree REGISTRY mise a jour : verdict par horizon dans la section M4 DLinear-vol, cote a cote avec les runs precedents.