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>
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user