Lab · ML Experiments

ML — Pattern Discovery

Inverted workflow: find conditional edges in BTC data first, build strategies second.
55 experiments

Lookahead Audit

audit
What happens here
Forensic check for lookahead bias in retrospectively stamped data sources (ETF flows etc.) — which findings survived the residual check.

Lookahead-Audit — Master-LGBM Feature Panel (Mai 2026)

2026-05-19 · ausgelöst durch B4-Finding (ETF-Flow Exp #18 hatte Lookahead-Bias)

Anlass

Exp #18 (ETF-Flow Event Study) hat IC = +0.37 gemeldet und wurde "promoted". Der Residualcheck Exp #18b (ETF-Flow Residual) hat aber gezeigt:

  • Mit korrektem 22:00-UTC-Marker schrumpft Original-IC auf +0.044
  • Same-day-confound IC = +0.405 (verheerend hoch — flow_t und ret_t auf demselben Tag korrelieren stark, weil ETF-Flow-Daten Releases AM SELBEN TAG der Bewegung reflektieren)
  • Residualisiert: IC +0.075, CI inkludiert Null

Wurzel des Bias in Exp #18: Daily resampling mit label="right", closed="right" setzt den Close-Bar-Stempel auf "Mitternacht des nächsten Tages". Wenn dann flow_t an "2024-01-11 00:00" mit fwd_ret_1d.shift(-1) aligned wird, schiebt das auf BTC-Return der selben Tageshandlung (2024-01-11), nicht auf den darauf folgenden Tag. Flow wird aber erst um 22:00 UTC dieses Tages reportet → klassischer Lookahead.

Konsequenz für die anderen Experimente: alle prüfen, ob die Feature-Construction kausal ist. Master-LGBM ist das wichtigste Stück.


Master-LGBM Feature-by-Feature Audit

# Feature Native Granularität Native Timestamp Information physisch verfügbar Angewandter Shift Tatsächliche Verzögerung im Panel Causal?
1 ret_1h, ret_4h, ret_24h, ret_7d 1h right-closed bar end sofort am Bar-Ende keiner (verwenden close ≤ t) 0h
2 log_rv_1h_ann, log_rv_4h_ann, log_rv_1d_ann, log_rv_7d_ann 1m → 1h via .last() right-closed sofort am Bar-Ende keiner 0h
3 log_vol, vol_z_1d, range_4h 1h aggregate right-closed sofort keiner 0h
4 hour_sin, hour_cos, dow timestamp-derived n/a sofort keiner 0h
5 funding_rate, funding_z_30d, funding_cum_1d 8h Hyperliquid 00:00, 08:00, 16:00 UTC sofort am Settlement ffill auf 1h Panel 0–7h (Bar-Position im Settlement-Fenster)
6 bocpd_p_short 15m posterior bar end sofort (forward filter by construction) ffill auf 1h 0–14 min
7 hmm_p_state1 1h per walk-forward split nur train-Daten in HMM-fit, dann causal forward filter refit pro Split 0h
8 iv_ann (DVOL), vrp daily Deribit 00:00 UTC sofort (DVOL ist Live-Index) .ffill().shift(1) ⇒ 1h-Delay 1h
9 stablecoin_d7 daily DefiLlama 00:00 UTC wenige Stunden Delay .diff(7).shift(1) ⇒ 1d-Delay 24h
10 etf_flow daily Farside 00:00 UTC der Flow-Date 22:00 UTC der Flow-Date .shift(1, freq="D") ⇒ flow_t verschoben auf d+1 00:00 UTC 2h
11 dxy_z_4h yfinance 1h bar end yfinance hat ~15min Delay für free tier keine zusätzliche Shift ~15 min ⚠️ kleine Verzögerung nicht modelliert
12 target_log_rv_fwd4h (LABEL) 4h forward .shift(-4) auf rv_4h_1h per Definition forward shift(-4) auf rv_4h_1h -4h (forward) ✅ Label

Befund: alle Features sind kausal. Die einzige kleinere Anmerkung: - dxy_z_4h: yfinance free tier hat ~15min Publication-Delay. Wenn wir live trading machen, müssten wir entweder eine zusätzliche .shift(1) einbauen oder eine bezahlte Real-Time-Quelle nutzen. Für Backtest auf historischen yfinance-Daten ist der Delay irrelevant, weil die Werte rückwirkend so geschrieben werden, wie sie nach den 15 Minuten verfügbar waren — d.h. der Backtest ist konservativer als die Realität.


Warum Exp #18 fehlerhaft war

Verbose Rekonstruktion des Bias:

flows index:        flow_t @ 2024-01-11 00:00 UTC   (was reportet ~22:00 UTC dieses Tages)
df_1d index:        right-closed → close_at_2024-01-12_00:00 = end-of-day 2024-01-11
btc_ret_1d:         log(close_2024-01-12_00:00) - log(close_2024-01-11_00:00) = return DURING 2024-01-11
fwd_ret_1d.shift(-1) at index 2024-01-11_00:00 → btc_ret_1d at 2024-01-12_00:00 = return DURING 2024-01-11
                                                                                  ^^^^^^^^^^^^^^^^^^^^^^
                                                                                  derselbe Tag wie flow_t!

In Worten: bei flows_total an "2024-01-11 00:00" wird auf fwd_ret_1d an demselben Index aligned, was der Return realisiert während des 11.01.2024 ist. Aber flow_t wurde erst um 22:00 UTC dieses Tages bekannt — das Modell hätte praktisch in die Zukunft gesehen, indem es einen Return als "forward" labeled, der bereits zu 90% gelaufen war, bevor das Feature überhaupt existierte.


Korrekturen / Konsequenzen

Verdict-Updates

Exp Alt Neu Begründung
#18 ETF-Flow Event Study ⭐ promoted (IC +0.37) dropped (lookahead) Same-day Confound, korrektes Setup gibt IC +0.04
#18b ETF-Flow Residual n/a ❌ dropped (IC residual +0.075, CI⊃0) Audit-Experiment
#22 Master-LGBM ⭐ promoted (+10.6 pp R²) promoted (validiert) Alle Features kausal verifiziert. ETF-Feature ist nicht in top-3 importance → der Lift kommt aus log_rv_7d_ann + iv_ann

Synthese-Update

Aus 4 Promotes wird 3 Promotes + 1 Lookahead-Drop:

  • ⭐ BOCPD p_short (Exp #15)
  • ⭐ DVOL/VRP (Exp #19)
  • ⭐ Master-LGBM (Exp #22, validiert)
  • ❌ ETF-Flow Event Study (Exp #18, Lookahead-Bias gefunden)
  • 🟡 HMM-Regime (Exp #16, pursue)
  • 🟡 Stablecoin Δ7d (Exp #17, weak)
  • 🟡 DXY-Shock (Exp #20, weak)
  • ❌ HAR-RV-J (Exp #13)
  • ❌ VPIN (Exp #14)

Master-LGBM bleibt promoted — aber warum?

ETF-Feature hat im Master-LGBM eine 1-Tages-Verschiebung (flow_d.shift(1, freq="D")) und ist damit kausal. Da Exp #18 selbst kein Lift gibt (residual IC +0.07), wird die ETF-Feature im LGBM-Importance-Ranking weit unten sein (nicht in den Top-3). Das R²-Lift von +10.6 pp kommt aus:

  1. log_rv_7d_ann (Top-1 Feature) — Vol-Persistence-Erweiterung über die HAR-RV-Kaskade hinaus (HAR-RV nutzt 1h/4h/1d; LGBM bekommt zusätzlich 7d).
  2. iv_ann (Top-2) — DVOL bringt Implied-Vol als orthogonale Informationsquelle. Das ist die VRP-Erkenntnis aus Exp #19.
  3. hour_cos (Top-3) — Vol hat einen klaren intraday-Pattern; LGBM nutzt das nonlinear besser als HAR-RV ohne Time-Feature.

Diese drei sind alle kausal sauber. Der ETF-Feature könnte sogar leicht schaden (Rauschen). Eine Robustheits-Variante ohne ETF wäre eine sinnvolle Validierung — siehe Next Steps.


Empfohlene Folge-Audits

Diese Audits sind noch offen für die anderen Promotes:

BOCPD (Exp #15) — wie kausal ist der Z-Score?

Der BOCPD-Input ist z = r / rolling_std(r, 96 bars). Der rolling-std nutzt vergangene Bars — kausal. Aber: r.rolling(96, min_periods=48).std() mit den Default-Pandas-Settings — ist das wirklich kausal (nur backward) und nicht etwa centered? .rolling(N) ist by default right-aligned (label = right edge), nutzt also nur die letzten N Werte. ✅ Kausal.

VRP (Exp #19) — DVOL-Timing genau

DVOL daily close stamped 00:00 UTC. Wir ffillen auf 4h-Panel ohne explicit shift. Das bedeutet: am Bar 2024-01-11 00:00 ist DVOL = der heutige Daily-Close. Der ist aber erst um 00:00 UTC verfügbar. Wenn wir prediction für die NÄCHSTE 4h-Vol bauen (also Vol von 00:00-04:00), nutzen wir DVOL der gerade erst "abgeschlossen" wurde. Knapp aber okay — die Information ist physisch live.

Im Master-LGBM verwenden wir .shift(1) für 1h-Lag — konservativer. ✓

Stablecoin (Exp #17) — sc_daily Timestamp-Semantik

Wann genau veröffentlicht DefiLlama den totalCirculatingUSD für "2024-01-11"? Die API hat keinen explizit dokumentierten Release-Zeitpunkt. Konservativ: 1d shift in Master-LGBM ist ausreichend.

Walk-Forward Embargo prüfen

walk_forward_splits(embargo_minutes=max(1440, 4*60)) ist 1440 min (1 Tag), was bei einem fwd-4h Target ausreicht. Aber für fwd-1d Targets müsste embargo ≥ 1d sein. In Master-LGBM target ist fwd-4h, also 1440 reicht ≥ 4h.

In Exp #15 BOCPD: embargo_minutes=max(1440, 4*60) = 1440 min, target ist fwd-4h. ✓ In Exp #18 ETF-Flow: embargo_minutes=1440 mit target fwd-1d. Wir bräuchten ≥ 1d Embargo. Es ist genau 1d → grenzwertig aber OK.


Lessons Learned

  1. Pandas-Resampling-Semantik prüfen. label="right", closed="right" bei daily aggregation packt einen ganzen Tag in das Mitternacht-des-FOLGE-Tages-Label. Das ist intuitiv, aber wenn man danach shift(-1) macht, landet man in der Vergangenheit. Bei nicht-trivialen Datenquellen (z. B. Farside) immer den Release-Zeitpunkt explizit denken, nicht den parquet-Timestamp blind übernehmen.

  2. Same-day Confound erkennen. Jedes Mal wenn ein Feature und ein Label am selben Tag stattfinden (selbst wenn das Label im DataFrame als "forward" gelabelt ist), Confound prüfen. Standardtest: feature_t → ret_pre_t IC messen. Wenn das hoch ist, mit Residualisierung gegenchecken.

  3. Causal Forward Filter ≠ Smoothing. Bei HMM-/BOCPD-Featuren ist die Wahl zwischen forward-only Filter und backward-smoothing der häufigste Lookahead-Spot. Wir nutzen strikt forward — gut.

  4. Feature Importance hilft beim Sanity Check. Wenn ein vermeintliches Edge-Feature (ETF) im Master-LGBM nicht in Top-Importance landet, ist das ein Hinweis dass der Standalone-IC vielleicht Confound war.


Action Items

  1. ✅ Synthese-Doc updaten — ETF-Flow als ❌ statt ⭐
  2. ⏳ Master-LGBM-Validierung ohne ETF-Feature: erwarteter R²-Lift identisch oder leicht höher → bestätigt, dass ETF nicht wesentlich beiträgt
  3. ⏳ Bei zukünftigen Experimenten: vor dropna() und shift() einen Trace-Plot erstellen (Feature-Wert + Label-Wert plotten über Time mit Annotations für Release-Zeitpunkte)
  4. ⏳ Embargo in walk_forward_splits für 1d-Targets auf 2d setzen (Sicherheitspuffer)