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,244 @@
|
||||
"""LEVERAGE SWEEP — cosa succede a PORT06 con leva 1..10 nei vari scenari (2026-06-18).
|
||||
|
||||
Domanda utente: "se porto a leva 10 cosa succede?". Estende lev_frontier() di
|
||||
accel50_research.py da 1-6 a 1-10 e su PIU' scenari, e soprattutto aggiunge le
|
||||
NON-LINEARITA' che il modello daily-lineare NASCONDE e che mordono a leva alta:
|
||||
|
||||
1) DRAWDOWN: nel modello daily scala ~lineare con la leva (DD_L ~ DD_1 * L) fino
|
||||
alla rovina. Ma e' un DD su daily AGGREGATI: nasconde le escursioni intraday.
|
||||
2) VOLATILITY DRAG: la crescita GEOMETRICA (CAGR) e' concava nella leva: cresce,
|
||||
ha un massimo (~leva di Kelly), poi CROLLA. Oltre Kelly piu' leva = MENO ritorno
|
||||
E piu' rischio. Con questa serie ad altissimo Sharpe il picco e' lontano -> il
|
||||
modello fa sembrare la leva "gratis": e' l'illusione da smontare.
|
||||
3) ROVINA / LIQUIDAZIONE: equity non puo' andare sotto zero. A leva L un ritorno di
|
||||
periodo r da' (1 + L*r); se L*r <= -1 -> conto azzerato. Il modello daily lo
|
||||
vede solo sul worst-day aggregato (docile); la realta' e' INTRADAY (gap, flash
|
||||
crash, maintenance-margin Deribit) -> la leva di rovina REALE e' molto piu' bassa.
|
||||
|
||||
Modello base: scala lineare dei daily return canonici (== live, parita' validata),
|
||||
`base` corrisponde a leva 2 (come in accel50). Per leva L: r_L = base * (L/2).
|
||||
|
||||
uv run python scripts/analysis/leverage_sweep.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))
|
||||
|
||||
|
||||
def _load_base() -> tuple[pd.Series, int]:
|
||||
"""Daily return PORT06 canonici (== live a leva 2) + indice di split OOS."""
|
||||
from scripts.portfolios._defs import PORTFOLIOS
|
||||
from scripts.analysis.combine_portfolio import port_returns, SPLIT
|
||||
from src.portfolio.sleeves import all_sleeve_equities, sleeve_returns_df
|
||||
|
||||
p = PORTFOLIOS["PORT06"]
|
||||
eq = all_sleeve_equities()
|
||||
members = {sid: eq[sid] for sid in p.sleeve_ids}
|
||||
w = p.weight_vector(sleeve_returns_df(p.sleeve_ids))
|
||||
return port_returns(members, w), SPLIT
|
||||
|
||||
|
||||
def _metrics(r: pd.Series) -> dict:
|
||||
"""CAGR geometrica, maxDD, Sharpe, worst-day, equity finale e flag rovina.
|
||||
Clip a -100%/giorno: oltre, il conto e' azzerato (rovina) e resta a zero."""
|
||||
ruined = bool((r <= -1.0).any())
|
||||
rc = r.clip(lower=-1.0) # un -100% azzera; non si recupera da zero
|
||||
curve = (1 + rc).cumprod()
|
||||
if ruined: # congela a zero dal primo azzeramento
|
||||
first = int(np.argmax(rc.values <= -1.0))
|
||||
curve.iloc[first:] = 0.0
|
||||
years = len(r) / 365.0
|
||||
final = float(curve.iloc[-1])
|
||||
cagr = (final ** (1 / years) - 1) * 100 if final > 0 else -100.0
|
||||
peak = curve.cummax()
|
||||
dd = float(((curve - peak) / peak).min() * 100)
|
||||
sharpe = float(r.mean() / r.std() * np.sqrt(365)) if r.std() > 0 else 0.0
|
||||
simple = (final - 1) * 100 if final > 0 else -100.0 # ritorno totale intra-periodo
|
||||
return {"cagr": cagr, "cagr_simple": simple, "dd": dd, "sharpe": sharpe,
|
||||
"worst": float(r.min() * 100), "final": final, "ruined": ruined}
|
||||
|
||||
|
||||
def sweep(base: pd.Series, split: int) -> None:
|
||||
full, oos = base, base.iloc[split:]
|
||||
print(f"PORT06 daily — FULL {base.index[0].date()}→{base.index[-1].date()} "
|
||||
f"({len(full)}g) | OOS {base.index[split].date()}→ ({len(oos)}g, regime CALMO)")
|
||||
print(f" worst-day base(leva2) = {base.min()*100:+.2f}% | Sharpe leva-invariante "
|
||||
f"(modello lineare): FULL {_metrics(full).get('sharpe'):.2f} / OOS {_metrics(oos).get('sharpe'):.2f}")
|
||||
print()
|
||||
hdr = ("leva CAGR_full% DD_full% x_full CAGR_oos% DD_oos% "
|
||||
"worstday% anni→€50/g(2k) rovina?")
|
||||
print(hdr); print("-" * len(hdr))
|
||||
for L in range(1, 11):
|
||||
f = L / 2.0
|
||||
mf, mo = _metrics(full * f), _metrics(oos * f)
|
||||
# anni a €50/g da €2000, su CAGR OOS geometrica (come accel50)
|
||||
co = mo["cagr"]
|
||||
if co > 0 and mo["final"] > 0:
|
||||
k = 2020 * (50 / (2020 * ((1 + co/100) ** (1/365) - 1))) # capitale per €50/g
|
||||
anni = np.log(k / 2020) / np.log(1 + co / 100)
|
||||
anni_s = f"{max(anni,0):.1f}"
|
||||
else:
|
||||
anni_s = "∞"
|
||||
flag = "💀 RUIN" if (mf["ruined"] or mo["ruined"]) else ("⚠ alto" if mf["dd"] < -25 else "ok")
|
||||
print(f"{L:>3} {mf['cagr']:>9.0f} {mf['dd']:>8.1f} {mf['final']:>7.1f}x "
|
||||
f"{mo['cagr']:>9.0f} {mo['dd']:>8.1f} {mf['worst']:>8.2f} "
|
||||
f"{anni_s:>12} {flag}")
|
||||
|
||||
|
||||
def kelly_and_ruin(base: pd.Series, split: int) -> None:
|
||||
"""Leva di Kelly (picco crescita geometrica) e leve di rovina sotto shock realistici."""
|
||||
print("\n=== NON-LINEARITA' che il modello daily nasconde ===\n")
|
||||
# Kelly: f* (in unita' di leva) che massimizza E[log(1+ (L/2)*base)]
|
||||
grid = np.linspace(0.1, 60, 600)
|
||||
def glog(L, r):
|
||||
rr = (r * (L / 2.0)).clip(lower=-0.999)
|
||||
return np.mean(np.log1p(rr))
|
||||
for label, r in [("FULL", base), ("OOS-calmo", base.iloc[split:])]:
|
||||
gl = [glog(L, r) for L in grid]
|
||||
kstar = grid[int(np.argmax(gl))]
|
||||
print(f" Kelly {label:9}: leva ottimale ~{kstar:.0f}x "
|
||||
f"(oltre, piu' leva = MENO crescita geometrica). "
|
||||
f"Half-Kelly prudente ~{kstar/2:.0f}x")
|
||||
print("\n -> Sharpe altissimo + regime OOS calmo spingono Kelly a leve assurde:")
|
||||
print(" e' l'ARTEFATTO del backtest, NON un via libera. Il drag morde tardi.")
|
||||
|
||||
print("\n=== ROVINA / LIQUIDAZIONE (il rischio VERO, intraday) ===")
|
||||
mday = base.min() / 2.0 # worst-day a leva 1
|
||||
print(f" worst-DAY storico (leva1) = {mday*100:+.2f}% -> rovina daily a leva "
|
||||
f"{-1/mday:.0f}x (irrealistico: aggregato/diversificato)")
|
||||
# shock INTRADAY realistici sull'esposizione NETTA del book reale
|
||||
# 7 sleeve equal-weight, position_size 0.5 -> esposizione lorda per leva = L*0.5
|
||||
# (frazione del capitale a mercato); un crash correlato colpisce quella frazione.
|
||||
ps = 0.5
|
||||
print(f" Config live: 7 sleeve EW, position_size={ps} -> esposizione ~ leva×{ps} "
|
||||
f"del capitale (a leva 3 = 1.5x; a leva 10 = 5x).")
|
||||
for shock in (0.10, 0.20, 0.40):
|
||||
Lruin = 1.0 / (shock * ps)
|
||||
print(f" crash correlato intraday {shock*100:>4.0f}% sull'esposizione: "
|
||||
f"azzera il conto a leva ~{Lruin:.0f}x "
|
||||
f"(disaster-SL on-book a -30%/pos mitiga ma NON in un gap-through)")
|
||||
print("\n NB: il backtest e' su daily AGGREGATI di un paniere con stop in regime "
|
||||
"calmo. Sottostima la coda intraday, i gap, lo slippage a size grande e la "
|
||||
"maintenance-margin Deribit (che liquida PRIMA del -100% del modello).")
|
||||
|
||||
|
||||
def per_year(base: pd.Series) -> None:
|
||||
"""Sweep leva 1→10 ANNO PER ANNO: la media FULL nasconde l'anno peggiore.
|
||||
Per ogni anno civile: ritorno geometrico e maxDD INTRA-anno ad ogni leva."""
|
||||
years = sorted({d.year for d in base.index})
|
||||
levs = list(range(1, 11))
|
||||
print("\n=== ANNO PER ANNO (ritorno % geometrico intra-anno) ===")
|
||||
print("anno g wDay@L1 " + "".join(f"L{L:>2} " for L in levs))
|
||||
print("-" * (24 + 9 * len(levs)))
|
||||
worst_dd = {}
|
||||
for y in years:
|
||||
r = base[base.index.year == y]
|
||||
cells = []
|
||||
for L in levs:
|
||||
m = _metrics(r * (L / 2.0))
|
||||
cells.append("RUIN" if m["ruined"] else f"{m['cagr_simple']:+.0f}")
|
||||
worst_dd.setdefault(L, []).append((y, _metrics(r * (L / 2.0))["dd"]))
|
||||
wd = r.min() / 2.0 * 100
|
||||
tag = " <calmo" if y >= 2025 else ""
|
||||
print(f"{y} {len(r):>3} {wd:>6.2f}% " + "".join(f"{c:>7} " for c in cells) + tag)
|
||||
|
||||
print("\n=== ANNO PER ANNO (maxDD % intra-anno) ===")
|
||||
print("anno g wDay@L1 " + "".join(f"L{L:>2} " for L in levs))
|
||||
print("-" * (24 + 9 * len(levs)))
|
||||
for y in years:
|
||||
r = base[base.index.year == y]
|
||||
cells = []
|
||||
for L in levs:
|
||||
m = _metrics(r * (L / 2.0))
|
||||
cells.append("RUIN" if m["ruined"] else f"{m['dd']:.1f}")
|
||||
wd = r.min() / 2.0 * 100
|
||||
tag = " <calmo" if y >= 2025 else ""
|
||||
print(f"{y} {len(r):>3} {wd:>6.2f}% " + "".join(f"{c:>7} " for c in cells) + tag)
|
||||
|
||||
# anno peggiore per DD ad ogni leva
|
||||
print("\n=== ANNO PEGGIORE per drawdown, ad ogni leva ===")
|
||||
for L in levs:
|
||||
yw, ddw = min(worst_dd[L], key=lambda t: t[1])
|
||||
print(f" leva {L:>2}: worst-anno {yw} maxDD {ddw:.1f}%"
|
||||
+ (" 💀 RUIN in qualche anno" if any(_metrics(base[base.index.year == yy] * (L/2.0))["ruined"] for yy in years) else ""))
|
||||
|
||||
|
||||
def shock_2022() -> None:
|
||||
"""STRESS con gli shock REALI 2022 (LUNA/3AC/FTX) sull'esposizione netta del book.
|
||||
Smonta il fill-al-livello del backtest daily: le fade+DIP01 COMPRANO i dip -> in un
|
||||
crollo sono net-LONG (catturate dalla parte sbagliata). Perdita = netta×shock; rovina
|
||||
a L_ruin = 1/(beta·ps·shock). I dati sono BTC/ETH 1h storici nostri, anno 2022."""
|
||||
from src.data.downloader import load_data
|
||||
print("\n=== STRESS SHOCK REALE 2022 (BTC/ETH storici, fill al GAP non al livello) ===")
|
||||
res = {}
|
||||
for a in ("BTC", "ETH"):
|
||||
df = load_data(a, "1h").set_index("datetime")
|
||||
d = df[(df.index >= "2022-01-01") & (df.index < "2023-01-01")]
|
||||
g = d.resample("1D").agg(open=("open", "first"), high=("high", "max"),
|
||||
low=("low", "min"), close=("close", "last")).dropna()
|
||||
res[a] = {"c2c": g["close"].pct_change().min(),
|
||||
"o2l": (g["low"] / g["open"] - 1).min(),
|
||||
"h1": d["close"].pct_change().min()}
|
||||
print(f" {a}: worst-day {res[a]['c2c']*100:+.1f}% | worst intraday "
|
||||
f"{res[a]['o2l']*100:+.1f}% | worst 1h-candle {res[a]['h1']*100:+.1f}%")
|
||||
print(" crolli multi-giorno (close→low): LUNA BTC-29%/ETH-36% · 3AC-giu BTC-44%/ETH-52%"
|
||||
" · FTX BTC-26%/ETH-32%")
|
||||
ps = 0.5
|
||||
scen = {"ETH intraday (-27%)": 0.267, "BTC stretch giu (-44%)": 0.44,
|
||||
"ETH stretch giu (-52%)": 0.52}
|
||||
print("\n LEVA DI ROVINA per net-exposure beta (ps=0.5):")
|
||||
print(f" {'scenario':24}" + "".join(f" b={b}" for b in (0.3, 0.5, 0.7, 1.0)))
|
||||
for name, dlt in scen.items():
|
||||
row = f" {name:24}"
|
||||
for b in (0.3, 0.5, 0.7, 1.0):
|
||||
L = 1.0 / (b * ps * dlt)
|
||||
row += f" {L:>4.1f}" + ("!" if L < 10 else " ")
|
||||
print(row)
|
||||
print(" ('!' = conto azzerato a leva <10). Le fade fadano = net-long nel crollo "
|
||||
"(beta alto proprio quando è pericoloso); il disaster-SL -30% NON scatta sotto "
|
||||
"-30% di mossa singola -> 2022 (max 1h -22%) il book mangia tutto.")
|
||||
|
||||
|
||||
def recommended_leverage() -> None:
|
||||
"""Leva MAX raccomandata data-driven: quella che tiene il DD di un 2022-repeat sotto
|
||||
una soglia recuperabile, dato un net-long realistico. Book-shock pesato sulla
|
||||
composizione reale (4 sleeve BTC + 3 ETH). Vincolo = capitulation peggiore (giugno)."""
|
||||
from src.data.downloader import load_data
|
||||
wB, wE, ps = 4 / 7, 3 / 7, 0.5
|
||||
|
||||
def stretch(a, s, e):
|
||||
df = load_data(a, "1h").set_index("datetime")
|
||||
g = df[(df.index >= s) & (df.index <= e)]
|
||||
return g["low"].min() / g["open"].iloc[0] - 1
|
||||
|
||||
june = wB * stretch("BTC", "2022-06-10", "2022-06-19") + wE * stretch("ETH", "2022-06-10", "2022-06-19")
|
||||
print("\n=== LEVA MAX RACCOMANDATA (vincolo: sopravvivi a un 2022-repeat) ===")
|
||||
print(f" book-shock pesato (BTC {wB:.0%}/ETH {wE:.0%}), capitulation giugno = {june*100:.0f}%")
|
||||
print(f" {'tolleranza DD nel 2022-repeat':30} beta=0.5 beta=0.6 beta=0.7")
|
||||
for label, dd in [("30% conservativo", .30), ("50% recuperabile", .50),
|
||||
("70% pre-liquidazione", .70)]:
|
||||
row = f" {label:30}"
|
||||
for b in (0.5, 0.6, 0.7):
|
||||
row += f" {dd/(b*ps*abs(june)):6.1f}x"
|
||||
print(row)
|
||||
rec = 0.50 / (0.6 * ps * abs(june))
|
||||
print(f"\n >>> RACCOMANDATA ~{rec:.1f}x (beta=0.6, DD<=50% recuperabile) -> PRUDENTE {int(rec)}x.")
|
||||
print(f" A leva 3 (attuale): DD 2022-repeat ~{0.6*ps*3*abs(june)*100:.0f}% (recuperabile). "
|
||||
f"Leva 5 ~{0.6*ps*5*abs(june)*100:.0f}% (pre-liquidazione). Leva 10 = rovina.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
base, split = _load_base()
|
||||
print("=== SWEEP LEVA 1→10 su PORT06 (modello daily lineare, == live) ===\n")
|
||||
sweep(base, split)
|
||||
per_year(base)
|
||||
kelly_and_ruin(base, split)
|
||||
shock_2022()
|
||||
recommended_leverage()
|
||||
Reference in New Issue
Block a user