Files
PythagorasGoal/Old/scripts/analysis/funding_carry_research.py
T
Adriano Dal Pastro 14522262e6 chore(reset): v2.0.0 — storico certificato Deribit mainnet, ripartenza pulita
Reset del progetto su fondamenta verificate dopo la scoperta che l'intera
libreria "validata OOS" era artefatto di feed contaminato (print fantasma del
feed Cerbero TESTNET + storico Binance/USDT).

- Storico ricostruito da Deribit MAINNET (ccxt pubblico, tokenless) e
  CERTIFICATO (certify_feed.py): BTC/ETH puliti su TUTTA la storia
  (mediana 2-6 bps vs Coinbase USD), integrita' OHLC + coerenza resample
  (maxΔ 0.00) + cross-venue OK. Alt esclusi (illiquidi/divergenti: LTC/DOGE
  50-82% barre flat; XRP/BNB non certificabili).
- Verdetto sul feed pulito: FADE / PAIRS / XS01 / TSM01 morti (ogni
  portafoglio Sharpe -2.3..-3.0, DD ~40%); solo SH01 e frammenti HONEST
  con segnale residuo, da ri-validare in isolamento.
- Cleanup "restart pulito": strategie, stack live (src/live, src/portfolio,
  runner/executor, yml, docker), ~100 script ricerca/gate, waste/games/
  portfolios, dati non certificati + cache e 60+ diari -> archiviati in Old/
  (preservati, non cancellati). Diario consolidato in un unico documento.
- Skeleton ricerca tenuto: Strategy ABC + indicatori + src/fractal +
  src/backtest/engine + load_data; tool dati certificati (rebuild_history,
  certify_feed, audit_feed, multi_source_check).
- Universo dati ATTIVO: solo BTC/ETH (5m/15m/1h); guardrail fisico
  (load_data su alt -> FileNotFoundError). Esecuzione DISABILITATA, conto flat.

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

232 lines
9.0 KiB
Python

"""FC01 — Funding-carry market-neutral (ricerca, 2026-06-10).
Idea: su Deribit i long pagano gli short quando il funding e' positivo (e
viceversa). W12 (scartata) shortava il perp su funding alto = direzionale.
Qui il meccanismo NUOVO e' il CARRY NEUTRALE: short della gamba con funding
alto / long della gamba con funding basso (BTC vs ETH, dollar-neutral),
incassando il DIFFERENZIALE di funding con esposizione residua = solo lo
spread ETH/BTC (correlazione ~0.95).
Dati REALI: data/regime/{btc,eth}_funding.parquet (orario, 2019-12 -> 2026-06,
interest_1h effettivo + index_price). Causale: decisione al close t con
funding noto fino a t; accrual dal bar t+1; fee 0.10% RT per GAMBA.
Varianti:
FC-A spread-carry 2 gambe (il candidato): entra quando lo spread di funding
smussato supera la soglia, esce quando rientra / max_bars.
FC-B single-asset carry direzionale (confronto onesto con W12): short se
funding smussato > thr, long se < -thr.
Protocollo: TRAIN fino a OOS_DATE (2023-11-01) per scegliere la config,
OOS dopo; griglia robustezza; sweep fee; breakdown annuale.
uv run python scripts/analysis/funding_carry_research.py
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
FEE_RT = 0.001 # 0.10% RT per gamba (taker, baseline progetto)
OOS_DATE = "2023-11-01"
HRS_YEAR = 24 * 365
def load_panel():
btc = pd.read_parquet("data/regime/btc_funding.parquet")
eth = pd.read_parquet("data/regime/eth_funding.parquet")
for d in (btc, eth):
d["dt"] = pd.to_datetime(d["timestamp"], unit="ms")
m = btc.set_index("dt")[["interest_1h", "index_price"]].rename(
columns={"interest_1h": "f_btc", "index_price": "p_btc"}).join(
eth.set_index("dt")[["interest_1h", "index_price"]].rename(
columns={"interest_1h": "f_eth", "index_price": "p_eth"}),
how="inner").sort_index()
m = m.dropna()
return m
def explore(m):
print("=" * 96)
print(" [0] ESPLORAZIONE — funding orario reale Deribit, "
f"{m.index[0].date()} -> {m.index[-1].date()} ({len(m)} ore)")
print("=" * 96)
for a in ("btc", "eth"):
f = m[f"f_{a}"] * HRS_YEAR * 100 # annualizzato %
print(f" {a.upper()}: funding annualizzato mean {f.mean():+6.2f}% "
f"med {f.median():+6.2f}% p10 {f.quantile(.1):+7.2f}% "
f"p90 {f.quantile(.9):+7.2f}% %ore>0 {100*(f>0).mean():.0f}%")
sp = (m["f_eth"] - m["f_btc"]) * HRS_YEAR * 100
print(f" SPREAD ETH-BTC annualizzato: mean {sp.mean():+6.2f}% "
f"p10 {sp.quantile(.1):+7.2f}% p90 {sp.quantile(.9):+7.2f}%")
# persistenza: autocorr dello spread smussato 24h a vari lag
s24 = (m["f_eth"] - m["f_btc"]).rolling(24).mean()
for lag in (24, 72, 168):
c = s24.autocorr(lag)
print(f" autocorr spread(24h-smooth) lag {lag:>4}h: {c:+.3f}")
# quanto duramo sopra soglia? episodi |spread ann| > 10%
thr = 0.10 / HRS_YEAR
above = (s24.abs() > thr).astype(int)
runs = (above.groupby((above != above.shift()).cumsum()).sum())
runs = runs[runs > 0]
if len(runs):
print(f" episodi |spread|>10% ann: {len(runs)} durata mediana "
f"{runs.median():.0f}h p90 {runs.quantile(.9):.0f}h")
# ---------------------------------------------------------------------------
# Backtest FC-A: spread-carry 2 gambe
# ---------------------------------------------------------------------------
def carry_pair(m, smooth=72, thr_ann=10.0, exit_frac=0.0, max_bars=24 * 30,
fee_rt=FEE_RT, sl=None):
"""Entra quando |spread smussato| > thr (annualizzato %); short la gamba
col funding alto, long l'altra, 1x notional per gamba. Esce quando lo
spread smussato scende sotto exit_frac*thr (o cambia segno) o max_bars.
Ritorna array di net-return per trade + serie equity oraria (additiva)."""
f_sp = (m["f_eth"] - m["f_btc"]).rolling(smooth).mean().to_numpy()
fe = m["f_eth"].to_numpy()
fb = m["f_btc"].to_numpy()
pe = m["p_eth"].to_numpy()
pb = m["p_btc"].to_numpy()
n = len(m)
thr = thr_ann / 100 / HRS_YEAR
ex = exit_frac * thr
sli = m.index[:n] if sl is None else None
rets, lens, accs = [], [], []
eq = np.zeros(n)
i = smooth
while i < n - 1:
s = f_sp[i]
if not np.isfinite(s) or abs(s) <= thr:
i += 1
continue
d = -1 if s > 0 else 1 # s>0: ETH paga di piu' -> short ETH/long BTC
e_eth, e_btc = pe[i], pb[i]
acc = 0.0
j = i + 1
end = min(n - 1, i + max_bars)
while j <= end:
# accrual del funding sull'ora j: short riceve +f, long paga f
acc += (-d) * fe[j] + d * fb[j]
if abs(f_sp[j]) <= ex or np.sign(f_sp[j]) != np.sign(s):
break
j += 1
j = min(j, end)
price_leg = d * (pe[j] - e_eth) / e_eth - d * (pb[j] - e_btc) / e_btc
net = price_leg + acc - 2 * fee_rt
rets.append(net)
lens.append(j - i)
accs.append(acc)
eq[j] += net
i = j + 1
rets = np.array(rets)
eqs = pd.Series(eq, index=m.index).cumsum()
return rets, np.array(lens), np.array(accs), eqs
# ---------------------------------------------------------------------------
# Backtest FC-B: carry direzionale single-asset (confronto/W12 onesto)
# ---------------------------------------------------------------------------
def carry_single(m, asset="eth", smooth=72, thr_ann=20.0, exit_frac=0.0,
max_bars=24 * 30, fee_rt=FEE_RT):
f = m[f"f_{asset}"].rolling(smooth).mean().to_numpy()
fr = m[f"f_{asset}"].to_numpy()
p = m[f"p_{asset}"].to_numpy()
n = len(m)
thr = thr_ann / 100 / HRS_YEAR
ex = exit_frac * thr
rets = []
i = smooth
while i < n - 1:
s = f[i]
if not np.isfinite(s) or abs(s) <= thr:
i += 1
continue
d = -1 if s > 0 else 1 # funding alto -> short (incassa)
e = p[i]
acc = 0.0
j = i + 1
end = min(n - 1, i + max_bars)
while j <= end:
acc += (-d) * fr[j]
if abs(f[j]) <= ex or np.sign(f[j]) != np.sign(s):
break
j += 1
j = min(j, end)
net = d * (p[j] - e) / e + acc - fee_rt
rets.append(net)
i = j + 1
return np.array(rets)
def stats(rets, idx_len_hours, label="", lens=None, accs=None):
if len(rets) == 0:
return f" {label:<28s} 0 trade"
yrs = idx_len_hours / HRS_YEAR
pnl = rets.sum() * 100
win = (rets > 0).mean() * 100
tpy = len(rets) / yrs
sh = rets.mean() / (rets.std() + 1e-12) * np.sqrt(max(tpy, 1e-9))
extra = ""
if lens is not None and len(lens):
extra = f" | hold med {np.median(lens):.0f}h"
if accs is not None and len(accs):
extra += f" | carry quota {100*np.sum(accs)/max(np.sum(rets),1e-9):.0f}%"
return (f" {label:<28s} {len(rets):>4d} tr | win {win:>4.0f}% | "
f"PnL {pnl:>+7.1f}% | {tpy:>5.1f} tr/anno | Sh {sh:>5.2f}{extra}")
def main():
m = load_panel()
explore(m)
cut = m.index.searchsorted(pd.Timestamp(OOS_DATE))
mtr, moo = m.iloc[:cut], m.iloc[cut:]
print(f"\n TRAIN {m.index[0].date()} -> {OOS_DATE} | OOS -> {m.index[-1].date()}")
print("\n" + "=" * 96)
print(" [1] FC-A spread-carry 2 gambe (fee 0.10% RT x2 gambe) — griglia su TRAIN")
print("=" * 96)
grid = []
for smooth in (24, 72, 168):
for thr in (5.0, 10.0, 20.0):
r, ln, ac, _ = carry_pair(mtr, smooth=smooth, thr_ann=thr)
grid.append((smooth, thr, r))
print(stats(r, len(mtr), f"TRAIN s{smooth} thr{thr:.0f}%", ln, ac))
print("\n Le stesse config in OOS (mai usate per scegliere):")
for smooth in (24, 72, 168):
for thr in (5.0, 10.0, 20.0):
r, ln, ac, _ = carry_pair(moo, smooth=smooth, thr_ann=thr)
print(stats(r, len(moo), f"OOS s{smooth} thr{thr:.0f}%", ln, ac))
print("\n" + "=" * 96)
print(" [2] FC-B carry direzionale single-asset (confronto, fee 0.10% RT)")
print("=" * 96)
for a in ("btc", "eth"):
for thr in (10.0, 30.0):
rtr = carry_single(mtr, a, thr_ann=thr)
roo = carry_single(moo, a, thr_ann=thr)
print(stats(rtr, len(mtr), f"TRAIN {a} thr{thr:.0f}%"))
print(stats(roo, len(moo), f"OOS {a} thr{thr:.0f}%"))
print("\n" + "=" * 96)
print(" [3] FC-A: sweep fee (config mediana s72 thr10) e breakdown annuale")
print("=" * 96)
for fee in (0.0005, 0.001, 0.002):
r, ln, ac, _ = carry_pair(m, smooth=72, thr_ann=10.0, fee_rt=fee)
print(stats(r, len(m), f"FULL fee {fee*100:.2f}% RT/gamba", ln, ac))
_, _, _, eq = carry_pair(m, smooth=72, thr_ann=10.0)
yr = eq.groupby(eq.index.year).apply(lambda s: (s.iloc[-1] - s.iloc[0]) * 100)
print(" annuale (PnL additivo %):",
{int(k): round(float(v), 1) for k, v in yr.items()})
if __name__ == "__main__":
main()