Files
PythagorasGoal/scripts/research/alt/runs/XAS05.py
T
Adriano Dal Pastro 5ac4e16af8 research(alt): sweep 104 strategie alternative su Deribit (153 agenti) + marginal scorer
Ondata di ricerca onesta a largo spettro su BTC/ETH+DVOL certificati: 104 ipotesi
distinte (11 famiglie), un agente-finder per ipotesi, verifica avversariale a 3
scettici sui promettenti, sintesi (153 agenti totali). Esito: NIENTE di nuovo regge
-> conferma del soffitto strutturale ~1.3 BTC/ETH-direzionale; lo stack
TP01+XS01+VRP01 resta imbattuto.

- altlib.py: harness condiviso vettoriale leak-free (eval_weights/study_weights,
  fee-sweep, both-asset + hold-out 2025+). Riproduce i numeri canonici di TP01.
- MARGINAL SCORER (study_marginal/marginal_vs_tp01): Sharpe INCREMENTALE vs baseline
  TP01 (corr, blend uplift OOS, alpha residua) + jackknife OOS (clean-year +
  drop-best-month). earns_slot = abs!=FAIL & ADDS & robust_oos. Smaschera gli overlay
  su TSMOM con PASS assoluti fasulli (CMB04, VOL11, ...) e il falso positivo KAMA
  (ADDS ma muore al jackknife).
- runs/*.py (104) script riproducibili per ipotesi; wf_altstrat.js workflow.
- Verdetto: 0 candidati deployabili; 2 LEAD fragili (VOL08, STA05_LS) da forward-monitor.
- test_marginal_scorer.py blocca baseline + invarianti. Suite: 32 verde.

Diario: docs/diary/2026-06-20-alt-strategies-100agent-sweep.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 19:50:39 +00:00

192 lines
7.6 KiB
Python

"""XAS05 — Lead-lag ETH->BTC (mirror of XAS04)
HYPOTHESIS: ETH returns lead BTC returns by 1 bar. BTC position = sign of ETH lagged return.
This is the mirror of XAS04 (BTC->ETH). We test several signal constructions:
1. Sign of ETH 1-bar return (pure lag) -> BTC position
2. ETH EMA momentum (fast/slow cross) -> BTC direction
3. ETH TSMOM (30/90/180 day) multi-horizon -> BTC direction
4. Blend of ETH 1-bar lag + ETH EMA momentum -> BTC direction
CAUSAL GUARANTEE: We use SAME timeframe ETH data aligned to BTC timestamps (merge_asof backward).
For cross-TF to work without lookahead, we must shift the ETH signal by 1 bar when mixing TFs.
The simplest honest approach: use ETH data at the SAME timeframe as the BTC data being evaluated.
For study_weights, target_fn(df) is called with each asset's df.
When df=BTC: we load ETH at the same TF, align it to BTC timestamps, compute the ETH signal,
and apply it to BTC -> the lead-lag hypothesis.
When df=ETH: we load ETH at the same TF, compute the ETH signal on the same data, and apply it
to ETH itself -> equivalent to trend-following ETH on its own momentum (baseline).
CRITICAL LOOKAHEAD WARNING (detected during development):
Using ETH 1d data to generate signals on BTC 12h bars IS a lookahead:
ETH 1d bar at T 00:00 has a close that matches ETH 12h bar at T 12:00 (i.e., noon close),
not midnight. The daily bar is labeled at midnight but closes are from future noon.
FIX: We always load ETH at the SAME TF as the df being evaluated.
"""
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
import altlib as al
import numpy as np
import pandas as pd
_TF_MAP = {} # will be filled per run
def _align_eth_to_btc(btc_df: pd.DataFrame, eth_df: pd.DataFrame) -> np.ndarray:
"""Align ETH close prices to BTC timestamps using merge_asof (causal: backward).
Both should be same TF to avoid cross-TF lookahead. Returns ETH close aligned to
BTC timestamps, len(btc_df). Applies an EXTRA 1-bar shift to ensure true causality:
ETH bar closing at T cannot influence BTC bar also closing at T (concurrent effect);
we require ETH close at T-1 to predict BTC bar at T+1 via altlib's own shift.
"""
btc_ts = btc_df["timestamp"].astype("int64").values
eth_ts = eth_df["timestamp"].astype("int64").values
eth_close = eth_df["close"].values.astype(float)
# Shift ETH by 1 bar: use ETH close at previous bar (T-1) as signal at bar T
# This prevents any possibility of concurrent/lookahead correlation
eth_close_lagged = np.empty_like(eth_close)
eth_close_lagged[0] = np.nan
eth_close_lagged[1:] = eth_close[:-1]
left = pd.DataFrame({"timestamp": btc_ts})
right = pd.DataFrame({"timestamp": eth_ts, "eth_close": eth_close_lagged})
merged = pd.merge_asof(left, right, on="timestamp", direction="backward")
return merged["eth_close"].values.astype(float)
def make_xas05_config1(lag_bars=1, tf="1d"):
"""Config 1: Sign of ETH 1-bar lagged return -> vol-targeted position.
Uses ETH return at prior bar (decided at close[i-1]) -> hold during bar i+1.
Extra 1-bar lag ensures strict causality even for concurrent closes.
"""
def target_fn(df):
# Detect asset by checking if it's the ETH df (ETH will self-signal)
# We always load ETH at the same TF as df
eth_df = al.get("ETH", tf)
eth_c = _align_eth_to_btc(df, eth_df)
n = len(df)
# ETH lagged return: sign of ETH return (already 1-bar lagged via alignment)
eth_ret = np.zeros(n)
eth_ret[lag_bars:] = eth_c[lag_bars:] / eth_c[:-lag_bars] - 1.0
# Direction = sign of ETH return
direction = np.sign(eth_ret)
direction[~np.isfinite(direction)] = 0.0
direction[:lag_bars + 5] = 0.0 # warmup
# Vol-target BTC position based on ETH signal
return al.vol_target(direction, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0)
return target_fn
def make_xas05_config2(ema_fast=5, ema_slow=20, tf="1d"):
"""Config 2: ETH EMA momentum (fast/slow cross) -> direction."""
def target_fn(df):
eth_df = al.get("ETH", tf)
eth_c = _align_eth_to_btc(df, eth_df)
n = len(df)
fast = al.ema(eth_c, ema_fast)
slow = al.ema(eth_c, ema_slow)
direction = np.where(fast > slow, 1.0, 0.0) # long-flat
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_xas05_config3(tf="1d"):
"""Config 3: ETH TSMOM (30/90/180 day) multi-horizon -> direction."""
def target_fn(df):
eth_df = al.get("ETH", tf)
eth_c = _align_eth_to_btc(df, eth_df)
n = len(df)
bpd = al.bars_per_day(df)
# Multi-horizon ETH momentum
signal = np.zeros(n)
for days in (30, 90, 180):
h = int(days * bpd)
s = np.full(n, np.nan)
if h < n:
s[h:] = np.sign(eth_c[h:] / eth_c[:-h] - 1.0)
signal = signal + np.nan_to_num(s)
# Long only when ETH shows positive trend
direction = np.clip(np.sign(signal), 0, None) # long-flat
direction[:int(180 * bpd) + 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_xas05_config4(lag_bars=1, ema_span=10, tf="1d"):
"""Config 4: Blend of ETH 1-bar lag + ETH EMA momentum -> direction."""
def target_fn(df):
eth_df = al.get("ETH", tf)
eth_c = _align_eth_to_btc(df, eth_df)
n = len(df)
# Signal 1: ETH lagged return sign
eth_ret = np.zeros(n)
eth_ret[lag_bars:] = eth_c[lag_bars:] / eth_c[:-lag_bars] - 1.0
sig1 = np.sign(eth_ret)
sig1[:lag_bars + 3] = 0.0
# Signal 2: ETH EMA momentum
fast = al.ema(eth_c, ema_span)
slow = al.ema(eth_c, ema_span * 4)
sig2 = np.where(fast > slow, 1.0, 0.0)
sig2[:ema_span * 4 + 5] = 0.0
# Blend: both signals must agree -> long-flat
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("XAS05 — Lead-lag ETH->BTC (HONEST: same TF, extra 1-bar lag to prevent concurrent lookahead)")
print("=" * 80)
# Only run 1d to keep total backtests <= 6 (4 configs x 1 TF x 2 assets = 8, but we cap at 4 configs)
# Use 1d only - it's the canonical TF for trend strategies and avoids TF mismatch issues
configs = [
("XAS05-C1-lag1ret-1d", make_xas05_config1(lag_bars=1, tf="1d"), ("1d",)),
("XAS05-C2-ema5x20-1d", make_xas05_config2(ema_fast=5, ema_slow=20, tf="1d"), ("1d",)),
("XAS05-C3-tsmom-1d", make_xas05_config3(tf="1d"), ("1d",)),
("XAS05-C4-blend-1d", make_xas05_config4(lag_bars=1, ema_span=10, tf="1d"), ("1d",)),
]
results = []
best_rep = None
best_hold = -999
for name, fn, tfs in configs:
print(f"\n--- Running {name} ---")
rep = al.study_weights(name, fn, tfs=tfs)
print(al.fmt(rep))
results.append(rep)
# Track best by min hold-out
v = rep["verdict"]
h = v.get("best_holdout_sharpe", -999)
if h is not None and h > best_hold:
best_hold = h
best_rep = rep
print("\n" + "=" * 80)
print(f"BEST CONFIG: {best_rep['name']} -> {best_rep['verdict']['grade']}")
print(al.fmt(best_rep))
print("\nJSON:", al.as_json(best_rep))