"""XAS04 — Lead-lag BTC->ETH HYPOTHESIS: BTC returns lead ETH returns. ETH position = sign of BTC lagged return. We evaluate on ETH series with BTC-derived signals. BTC and ETH data must be at the SAME timeframe so that timestamp alignment is exact (no cross-TF look-ahead). CAUSAL GUARANTEE: - BTC and ETH are loaded at the SAME timeframe (1d or 12h). - We align BTC to ETH by merging on common timestamps (inner/exact match). - target[i] uses BTC close[i] (same bar as ETH close[i]) -> held during bar i+1. - The altlib eval_weights shift handles the i->i+1 transition. - No cross-TF artifacts. CRITICAL BUG AVOIDED: loading BTC at 1d and aligning to ETH at 12h creates look-ahead because the 1d bar timestamp (midnight) has the day's FINAL close, so the midnight 12h bar gets the full day's close which is the FUTURE relative to the midnight price. FIX: always use the same TF for both BTC and ETH. CONFIGS TESTED: 1. Sign of BTC 1-bar return (pure lag-1 momentum applied to ETH) 2. BTC EMA5/20 cross -> ETH direction (BTC trend applied to ETH) 3. BTC TSMOM multi-horizon (30/90/180d) -> ETH direction 4. Blend: require BTC lag-1 AND BTC EMA trend to agree before entering ETH """ import sys sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt") import altlib as al import numpy as np import pandas as pd def _align_same_tf(eth_df: pd.DataFrame, btc_df: pd.DataFrame) -> np.ndarray: """Align BTC close to ETH timestamps using exact same-TF merge. Both DataFrames have the SAME timeframe, so merge on timestamp directly. Returns BTC close values aligned to ETH bar indices, NaN where missing.""" eth_ts = eth_df["timestamp"].astype("int64") btc_df2 = btc_df[["timestamp", "close"]].copy() btc_df2["timestamp"] = btc_df2["timestamp"].astype("int64") btc_df2 = btc_df2.rename(columns={"close": "btc_close"}) merged = eth_ts.to_frame().merge(btc_df2, on="timestamp", how="left") return merged["btc_close"].values.astype(float) def make_target_lag1(tf: str): """Config 1: Sign of BTC 1-bar return -> ETH vol-targeted position.""" btc_df = al.get("BTC", tf) def target_fn(df): # Align BTC close to ETH at same TF btc_c = _align_same_tf(df, btc_df) n = len(df) # BTC 1-bar lagged return: r[i] = BTC_close[i] / BTC_close[i-1] - 1 btc_ret = np.zeros(n) btc_ret[1:] = btc_c[1:] / btc_c[:-1] - 1.0 # Direction = sign of BTC return direction = np.sign(btc_ret) direction[~np.isfinite(direction)] = 0.0 direction[:5] = 0.0 # warmup # Vol-target position on ETH return al.vol_target(direction, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0) return target_fn def make_target_ema(tf: str, ema_fast: int = 5, ema_slow: int = 20): """Config 2: BTC EMA cross -> ETH direction (long-flat).""" btc_df = al.get("BTC", tf) def target_fn(df): btc_c = _align_same_tf(df, btc_df) n = len(df) fast = al.ema(btc_c, ema_fast) slow = al.ema(btc_c, ema_slow) direction = np.where(fast > slow, 1.0, 0.0) direction[~np.isfinite(direction)] = 0.0 direction[:ema_slow + 5] = 0.0 # warmup return al.vol_target(direction, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0) return target_fn def make_target_tsmom(tf: str): """Config 3: BTC TSMOM multi-horizon (30/90/180 day) -> ETH direction (long-flat).""" btc_df = al.get("BTC", tf) def target_fn(df): btc_c = _align_same_tf(df, btc_df) n = len(df) bpd = al.bars_per_day(df) signal = np.zeros(n) for days in (30, 90, 180): h = int(days * bpd) if h < n: s = np.full(n, np.nan) valid = ~np.isnan(btc_c) & (np.roll(btc_c, h) != 0) s[h:] = np.sign(btc_c[h:] / btc_c[:-h] - 1.0) signal = signal + np.nan_to_num(s) direction = np.clip(np.sign(signal), 0, None) # long-flat direction[:int(180 * bpd) + 5] = 0.0 return al.vol_target(direction, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0) return target_fn def make_target_blend(tf: str, lag: int = 1, ema_span: int = 10): """Config 4: Blend BTC lag-1 sign + BTC EMA trend; enter ETH only when both agree.""" btc_df = al.get("BTC", tf) def target_fn(df): btc_c = _align_same_tf(df, btc_df) n = len(df) # Signal 1: BTC 1-bar lag sign btc_ret = np.zeros(n) btc_ret[lag:] = btc_c[lag:] / btc_c[:-lag] - 1.0 sig1 = np.sign(btc_ret) sig1[:lag + 3] = 0.0 # Signal 2: BTC EMA momentum fast = al.ema(btc_c, ema_span) slow = al.ema(btc_c, ema_span * 4) sig2 = np.where(fast > slow, 1.0, 0.0) sig2[:ema_span * 4 + 5] = 0.0 # Both must agree (long ETH only when BTC shows positive momentum AND positive lag) direction = np.where((sig1 > 0) & (sig2 > 0), 1.0, 0.0) direction[~np.isfinite(direction)] = 0.0 return al.vol_target(direction, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0) return target_fn if __name__ == "__main__": print("XAS04 — Lead-lag BTC->ETH (same-TF, causal alignment)") print("=" * 70) # We test on both 1d and 12h but use SAME TF for BTC signal source. # This avoids the cross-TF look-ahead artifact. # The study_weights will apply the function to BOTH BTC and ETH at each TF. # For BTC asset: BTC signal applied to BTC (degenerate = BTC follows BTC lag) # For ETH asset: BTC signal applied to ETH (the actual lead-lag hypothesis) # Config 1: pure lag-1 BTC signal -> run on 1d and 12h print("\n--- C1: Lag-1 BTC return sign -> ETH (1d) ---") rep_c1_1d = al.study_weights( "XAS04-C1-lag1-1d", make_target_lag1("1d"), tfs=("1d",) ) print(al.fmt(rep_c1_1d)) print("\n--- C1: Lag-1 BTC return sign -> ETH (12h) ---") rep_c1_12h = al.study_weights( "XAS04-C1-lag1-12h", make_target_lag1("12h"), tfs=("12h",) ) print(al.fmt(rep_c1_12h)) print("\n--- C2: BTC EMA5/20 cross -> ETH (1d) ---") rep_c2 = al.study_weights( "XAS04-C2-ema5x20-1d", make_target_ema("1d", 5, 20), tfs=("1d",) ) print(al.fmt(rep_c2)) print("\n--- C3: BTC TSMOM -> ETH (1d) ---") rep_c3 = al.study_weights( "XAS04-C3-tsmom-1d", make_target_tsmom("1d"), tfs=("1d",) ) print(al.fmt(rep_c3)) print("\n--- C4: BTC blend lag+ema -> ETH (1d) ---") rep_c4 = al.study_weights( "XAS04-C4-blend-1d", make_target_blend("1d", lag=1, ema_span=10), tfs=("1d",) ) print(al.fmt(rep_c4)) # Identify best config all_reps = [rep_c1_1d, rep_c1_12h, rep_c2, rep_c3, rep_c4] best_rep = max(all_reps, key=lambda r: r["verdict"].get("best_holdout_sharpe") or -999) print("\n" + "=" * 70) print(f"BEST CONFIG: {best_rep['name']} -> {best_rep['verdict']['grade']}") print(al.fmt(best_rep)) print("\nJSON:", al.as_json(best_rep))