Files
PythagorasGoal/scripts/research/r0724_premium_wave.py
T
Adriano Dal Pastro b65f067f69 research(premium): ondata premi cross-venue auto-calcolati — CBPREM morto, KIMCHI d30 ucciso dallo scettico (spike+lag)
CBPREM (Coinbase vs feed cert, 2015->): HEDGE, hold -0.46. KIMCHI (Upbit/ECB, 2017->,
ancora 00:00 UTC verificata): EARNS_SLOT=True al marginal scorer (ADDS, robust_oos,
uplift + ogni anno) ma DSR 0.891 -> scettico obbligatorio: niente plateau
(d15/30/45/60 = 0.44/1.07/0.51/0.24) e lag +1g azzera (hold 0.74->0.14) = parameter
luck. Lezione codificata: EARNS_SLOT con DSR<0.95 -> sempre plateau+lag skeptic.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 22:23:16 +00:00

142 lines
5.8 KiB
Python

"""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()