"""r0724_premium_wave — premi cross-venue AUTO-CALCOLATI: Coinbase premium + Kimchi (2026-07-24). Seconda parte dell'ondata "trova altre strategie" (dopo r0724_onchain_wave): la ricerca web (agente on-chain/dati) indica i premi regionali come UNICA famiglia flow con dati 100% auto-calcolabili da candele raw -> zero rischio-revisione/vintage del vendor (coerente con la dottrina dati del progetto). Evidenza accademica: kimchi = anomalia documentata (violazione persistente della legge del prezzo unico, capital controls); lead-lag ASIMMETRICO e TEMPO-VARIANTE (MDPI 2026) -> nessuna regola pubblicata onesta, qui si meccanizza da zero. SEGNALI (mai il prezzo): per asset a e giorno d CBPREM_a(d) = close Coinbase USD (00:00 UTC) / close feed certificato - 1 KIMCHI_a(d) = close Upbit KRW (00:00 UTC, ancora 09:00 KST) / (USDKRW_ECB x close cert) - 1 Le candele Upbit daily sono ancorate a mezzanotte UTC (=09:00 KST) -> stesso istante di chiusura del feed certificato, nessun premio finto da mismatch orario. FX = fixing ECB del giorno (ffill weekend; il KRW si muove ~nulla vs la vol crypto — caveat dichiarato). NB: il feed certificato Deribit e' esso stesso un indice multi-exchange che include Coinbase -> il CBPREM misurato e' SMORZATO (caveat strutturale). Griglie (piccole, tutte contate nel deflated-Sharpe): z-score rolling 180g del livello (follow / contrarian) e segno della variazione 30g (follow), LF e LS -> 6 celle/famiglia. Gate: study_family_honest (cella in-sample, DSR, marginal scorer vs TP01). Dati: data/external/premium/ (fetch: scratchpad/fetch_premium.py — Coinbase Exchange public candles, Upbit public candles, frankfurter.app ECB; tutti tokenless). Uso: `uv run python scripts/research/r0724_premium_wave.py` """ from __future__ import annotations import sys from pathlib import Path import numpy as np import pandas as pd ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) import altlib # noqa: E402 from altlib import study_family_honest, fmt_marginal, get # noqa: E402 EXT = ROOT / "data" / "external" / "premium" def _cert_close_by_day(asset: str) -> pd.Series: df = get(asset, "1d") days = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)).floor("D") return pd.Series(df["close"].values.astype(float), index=days) def _cb_close(asset: str) -> pd.Series: d = pd.read_csv(EXT / f"cb_{asset.lower()}.csv") idx = pd.DatetimeIndex(pd.to_datetime(d["ts"], unit="s", utc=True)).floor("D") return pd.Series(d["close"].values.astype(float), index=idx) def _upbit_close(asset: str) -> pd.Series: d = pd.read_csv(EXT / f"upbit_{asset.lower()}.csv") idx = pd.DatetimeIndex(pd.to_datetime(d["utc"], utc=True)).floor("D") return pd.Series(d["close_krw"].values.astype(float), index=idx) def _fx() -> pd.Series: d = pd.read_csv(EXT / "usdkrw.csv") idx = pd.DatetimeIndex(pd.to_datetime(d["date"], utc=True)) s = pd.Series(d["usdkrw"].values.astype(float), index=idx) full = pd.date_range(s.index[0], s.index[-1] + pd.Timedelta(days=3), freq="D", tz="UTC") return s.reindex(full).ffill() def _premia() -> dict: fx = _fx() out = {} for a in ("BTC", "ETH"): cert = _cert_close_by_day(a) cb = _cb_close(a).reindex(cert.index) up = _upbit_close(a).reindex(cert.index) out[a] = pd.DataFrame({ "cbprem": cb / cert - 1.0, "kimchi": up / (fx.reindex(cert.index) * cert) - 1.0, }, index=cert.index) return out _PREM = _premia() def prem_factory_maker(col: str): def factory(tf: str, sig: str = "z180", mode: str = "folLF"): def fn(df, asset): p = _PREM[asset][col] if sig == "z180": mu = p.rolling(180, min_periods=90).mean() sd = p.rolling(180, min_periods=90).std() z = (p - mu) / sd else: # d30: variazione 30g del premio z = p - p.shift(30) s = np.sign(z) if mode.startswith("fol") else -np.sign(z) pos = s if mode.endswith("LS") else (s > 0).astype(float) days = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)).floor("D") return np.nan_to_num(pos.reindex(days).values.astype(float)) return fn return factory GRID = [ dict(sig="z180", mode="folLF"), dict(sig="z180", mode="conLF"), dict(sig="z180", mode="folLS"), dict(sig="z180", mode="conLS"), dict(sig="d30", mode="folLF"), dict(sig="d30", mode="folLS"), ] def main() -> None: print("=" * 100) print(" ONDATA PREMI CROSS-VENUE — CBPREM + KIMCHI via study_family_honest") for a in ("BTC", "ETH"): P = _PREM[a].dropna() print(f" {a}: {len(P)} giorni | cbprem medio {P['cbprem'].mean()*1e4:+.1f}bps " f"(p1/p99 {P['cbprem'].quantile(0.01)*1e4:+.0f}/{P['cbprem'].quantile(0.99)*1e4:+.0f}) | " f"kimchi medio {P['kimchi'].mean()*1e4:+.1f}bps " f"(p1/p99 {P['kimchi'].quantile(0.01)*1e4:+.0f}/{P['kimchi'].quantile(0.99)*1e4:+.0f})") print("=" * 100) for fam, col in (("CBPREM-coinbase", "cbprem"), ("KIMCHI-korea", "kimchi")): print("-" * 100) rep = study_family_honest(fam, prem_factory_maker(col), GRID, tfs=("1d",)) if rep.get("chosen") is None: print(f"=== {fam}: nessuna cella valida in-sample") continue ch = rep["chosen"] print(f"=== {fam}: cella IS {ch['params']} (IS Sh {ch['insample_sharpe']}, " f"full {ch['full_sharpe']}) su {rep['n_cells']} celle") print(f" deflated-Sharpe {rep['deflated_sharpe']} (null-max {rep['expected_null_max']})" f" dsr_pass={rep['dsr_pass']}") print(fmt_marginal(rep["marginal"])) print(f" >>> EARNS_SLOT_HONEST = {rep['earns_slot_honest']}") if __name__ == "__main__": main()