#!/usr/bin/env python3 """ r0823b_curva_datati.py — filone §65 CURVA-DATATI (ondata 2026-08-23) =========================================================================== LA DOMANDA =========================================================================== L'ondata 1 (§13 BASIS-CALENDAR) ha ricostruito un dataset che il progetto non aveva: **30 futures trimestrali SCADUTI** di Deribit (BTC 240.150 barre orarie / ETH 233.334, dal 2018-09), **col deleveraging 2022 dentro** — la coda che a CC01 mancava per costruzione. Finora e' stato usato per UNA cosa sola: il **livello** del carry (§13 lo ha scartato come edge; §39 lo ha scartato come veicolo di TP01). **La FORMA della curva — pendenza e curvatura — non e' mai stata guardata.** =========================================================================== ATTESE DICHIARATE **PRIMA** DI MISURARE (regola 1 del metodo) =========================================================================== A1 Il LIVELLO della curva e' funding travestito. §39 ha gia' misurato che il basis dei datati E' il funding del perp (+7,33% implicito contro +6,48% realizzato). Mi aspetto |corr(livello, funding)| ALTA (>0,5). -> se confermata, il ramo "livello" e' chiuso in partenza. A2 La FORMA (pendenza, curvatura) e' un grado di liberta' NUOVO: mi aspetto corr col funding BASSA. E' l'unica ragione per cui questo filone esiste. A3 Nessun ramo produce un candidato. Il filone funding e' chiuso su QUATTRO lati (carry cross-sectional, price-clock, time-series, affollamento) e §13 ha gia' esaurito il primo ordine. Mi aspetto SCARTATO su tutti i rami. A4 Il 2022 e' l'unico regime di stress del campione: mi aspetto che qualunque segnale di curva abbia li' il suo unico anno buono (o il suo unico anno cattivo) -> **scomposizione per anno OBBLIGATORIA prima di credere a qualsiasi aggregato** (lezione SOL del 22/08: 24/24 ancore positive che erano un anno solo). A5 Il roll trimestrale costa. §39: SKH01 sui datati fa 48-57 roll-equivalenti l'anno contro 6,6-8,4 di TP01 -> lo stesso mezzo spread gli costa 7 volte tanto. Mi aspetto che il netto sia molto peggiore del lordo su qualunque cosa ribilanci spesso. A6 MDE: 7,4 anni -> MDE sullo Sharpe ~= 1,96/sqrt(7,4) = **0,72**. Un ramo DELTA-NEUTRO (§13: SE del rendimento annuo 1,79pp, vol 4,9%) non e' rispondibile; un ramo DIREZIONALE (vol 50-80%) lo e' in linea di principio. Esito di ciascuna: riportato in fondo, confermata/refutata. =========================================================================== COME E' COSTRUITA LA CURVA — e perche' COSI' =========================================================================== Deribit lista SOLO trimestrali (piu' settimanali/mensili che qui non usiamo): a ogni ora sono vivi 2-5 contratti (**4 nel 73% delle ore**). Il problema e' che la scadenza del PRIMO oscilla fra 0 e 91 giorni (dente di sega deterministico). Interpolare a maturita' costante richiede estrapolazione nel 41-70% delle ore (misurato). Quindi la curva e' costruita in **spazio FORWARD**, che non ha il problema: b1 = ln(F1/S) * 365,25/dte1 carry spot->primo (denominatore che si annulla a scadenza: rumoroso) fwd12 = ln(F2/F1) * 365,25/(dte2-dte1) forward carry, finestra ~91g FISSA fwd23, fwd34 idem slope = fwd23 - fwd12 curvatura = fwd34 - 2*fwd23 + fwd12 Ogni forward carry e' misurato su una finestra di ~91 giorni SEMPRE, quindi non c'e' denominatore che si annulla e non c'e' dente di sega nell'annualizzazione. Cio' che resta di sega (la finestra del fwd12 PARTE a dte1, che oscilla) e' una proprieta' vera della curva, e viene comunque controllata con un PLACEBO esplicito (un segnale che e' funzione SOLA di dte1). 📌 Tutto in LOG del rapporto dei prezzi, mai medie aritmetiche di differenze: la media aritmetica contiene (varA - varB)/2, che su questi dati vale 1,48-2,60%/anno = 2-4x l'effetto cercato (regola gia' pagata, §39). =========================================================================== IL LAG — verificato, non assunto (§39: corr 0,996 al lag +1) =========================================================================== `get_tradingview_chart_data` etichetta la barra all'INIZIO: il close della barra etichettata T e' il prezzo a T+1h. `get_funding_rate_history` etichetta l'indice all'ISTANTE T. Appaiarli senza lag sfasa di un'ora. Il lag NON e' assunto: e' scelto misurando la sd del basis perp-vs-indice a k = -2..+2 e la convergenza a scadenza. Vedi §0. =========================================================================== DISCIPLINA =========================================================================== * SOLA LETTURA. Nessuna rete: tutto da `data/raw/fut_deribit/` (persistito dall'ondata 1). Nessun ordine, nessuna chiave. * Nessun file di produzione toccato. * Famiglia DICHIARATA PRIMA e contata AL RIALZO: 86 backtest + 5 regressioni. Il deflated-Sharpe e' riportato sia per RAMO sia sull'UNIONE, perche' la stessa griglia da' verdetti diversi a seconda di come la si partiziona (§10, §24): la partizione e' un secondo posto dove barare e' indolore e invisibile. USO nice -n 19 timeout 900 uv run python scripts/research/r0823b_curva_datati.py """ from __future__ import annotations import sys import warnings from pathlib import Path import numpy as np import pandas as pd from scipy import stats PROJECT_ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(PROJECT_ROOT)) sys.path.insert(0, str(PROJECT_ROOT / "scripts" / "research" / "alt")) import altlib as al # noqa: E402 from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio # noqa: E402 warnings.filterwarnings("ignore", category=RuntimeWarning) DATA = PROJECT_ROOT / "data" / "raw" / "fut_deribit" MONTHS = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"] YR = 365.25 ASSETS = ("BTC", "ETH") HOLDOUT = al.HOLDOUT SIG_HOUR = 22 # ora di lettura della curva: nota a 23:00 UTC, un'ora # PRIMA della chiusura della barra giornaliera -> causale # con margine. La sensibilita' all'ora e' misurata in §6. # ---- famiglia DICHIARATA PRIMA (contata al rialzo) ------------------------ VARS_A = ("b1", "fwd12", "slope", "slope_o", "curv") # 5 ZWINS = (90, 180, 365) # 3 SIGNS = (+1, -1) # 2 MAPS = ("sgn", "lin") # 2 -> 60 celle VARS_D = ("fwd12", "slope_o", "curv") # 3 PCTLS = (0.30, 0.50, 0.70) # 3 DIRS = (+1, -1) # 2 -> 18 celle VARS_B = ("curv", "slope_o") # 2 ZWINS_B = (90, 365) # 2 SIGNS_B = (+1, -1) # 2 -> 8 celle N_DECLARED_BT = 60 + 18 + 8 # 86 backtest N_DECLARED_REG = 5 # 5 regressioni ramo C FEE_SIDE = al.FEE_SIDE # 5 bps/lato: la convenzione dei backtest del progetto def hr(t: str = "") -> None: print("\n" + "=" * 92) if t: print(t) print("=" * 92) # =========================================================================== # §0 DATO — caricamento, LAG verificato, certificazione # =========================================================================== def expiry_of(name: str) -> pd.Timestamp: _, tag = name.split("-") return pd.Timestamp(2000 + int(tag[5:]), MONTHS.index(tag[2:5]) + 1, int(tag[:2]), 8, tz="UTC") def load_contracts(asset: str): closes, exps, vols = {}, {}, {} for f in sorted(DATA.glob(f"{asset}-*.parquet")): nm = f.stem df = pd.read_parquet(f) if len(df) < 200: continue s = df.set_index("ts")["close"].astype(float) s = s[s > 0] s.index = s.index.tz_convert("UTC") v = df.set_index("ts")["volume"].astype(float) v.index = v.index.tz_convert("UTC") e = expiry_of(nm) s = s[s.index < e - pd.Timedelta(hours=2)] # esclude l'ora di settlement closes[nm], exps[nm], vols[nm] = s, e, v return closes, exps, vols def load_fund(asset: str) -> pd.DataFrame: f = pd.read_parquet(DATA / f"fund_{asset}.parquet").set_index("ts").sort_index() f.index = f.index.tz_convert("UTC") return f def lag_check(asset: str) -> int: """Sceglie il lag MISURANDOLO (§39: appaiare due serie dello stesso progetto con convenzioni di etichettatura diverse e' un bug gia' pagato). Ritorna il lag in ore.""" fund = load_fund(asset) idx = fund["index"].astype(float) p = al.get(asset, "1h") perp = pd.Series(p["close"].values.astype(float), index=pd.DatetimeIndex(pd.to_datetime(p["datetime"], utc=True))) out = {} for k in (-2, -1, 0, 1, 2): ii = idx.copy() ii.index = ii.index - pd.Timedelta(hours=k) j = pd.concat({"p": perp, "i": ii}, axis=1, join="inner").dropna() b = np.log(j["p"] / j["i"]) * 1e4 out[k] = (len(j), float(b.median()), float(b.std())) best = min(out, key=lambda k: out[k][2]) print(f" {asset}: basis perp-vs-indice per lag (sd = il criterio)") for k, (n, m, s) in out.items(): mark = " <- SCELTO (sd minima)" if k == best else "" print(f" lag {k:+d}h n={n:,} mediana {m:+7.2f} bps sd {s:8.2f} bps{mark}") return best def build_curve(asset: str, lag_h: int) -> pd.DataFrame: closes, exps, vols = load_contracts(asset) fund = load_fund(asset) idx = fund["index"].astype(float) f1h = fund["f1h"].astype(float) wide = pd.DataFrame(closes).sort_index() S = idx.reindex(wide.index + pd.Timedelta(hours=lag_h)) S.index = wide.index keep = S.notna() wide, S = wide[keep], S[keep] DTE = pd.DataFrame({nm: (exps[nm] - wide.index).total_seconds() / 86400.0 for nm in wide.columns}, index=wide.index).where(wide.notna()) dt_v, px_v, sv = DTE.values, wide.values, S.values n = len(wide) out = np.full((n, 6), np.nan) for i in range(n): m = np.isfinite(dt_v[i]) & np.isfinite(px_v[i]) & (dt_v[i] > 0.5) d, p = dt_v[i][m], px_v[i][m] o = np.argsort(d) d, p = d[o], p[o] k = len(d) out[i, 5] = k if k >= 1: out[i, 0] = d[0] out[i, 1] = np.log(p[0] / sv[i]) * YR / d[0] if k >= 2: out[i, 2] = np.log(p[1] / p[0]) * YR / (d[1] - d[0]) if k >= 3: out[i, 3] = np.log(p[2] / p[1]) * YR / (d[2] - d[1]) if k >= 4: out[i, 4] = np.log(p[3] / p[2]) * YR / (d[3] - d[2]) C = pd.DataFrame(out, index=wide.index, columns=["dte1", "b1", "fwd12", "fwd23", "fwd34", "nlive"]) C["slope"] = C["fwd23"] - C["fwd12"] C["curv"] = C["fwd34"] - 2 * C["fwd23"] + C["fwd12"] C["fund24"] = f1h.reindex(C.index).rolling(24).mean() * 24 * YR C["S"] = S return C def causal_orth(y: pd.Series, x: pd.Series, min_obs: int = 180) -> pd.Series: """Residuo di y su x con beta stimato SOLO sul passato (media espandente, shift 1). Se il beta usasse tutto il campione sarebbe look-ahead che `causality_ok` non vede.""" j = pd.concat({"y": y, "x": x}, axis=1) v = j.notna().all(axis=1) yy = j["y"].where(v) xx = j["x"].where(v) n = v.astype(float).cumsum().shift(1) sx = xx.fillna(0).cumsum().shift(1) sy = yy.fillna(0).cumsum().shift(1) sxx = (xx.fillna(0) ** 2).cumsum().shift(1) sxy = (xx.fillna(0) * yy.fillna(0)).cumsum().shift(1) cov = sxy / n - (sx / n) * (sy / n) var = sxx / n - (sx / n) ** 2 beta = (cov / var).where(n >= min_obs) alpha = (sy / n) - beta * (sx / n) return (yy - (alpha + beta * xx)).where(n >= min_obs) def daily_curve(asset: str, C: pd.DataFrame, hour: int = SIG_HOUR) -> pd.DataFrame: d = C[C.index.hour == hour].copy() d.index = d.index.normalize() d = d[~d.index.duplicated(keep="last")] d["slope_o"] = causal_orth(d["slope"], d["fwd12"]) return d # =========================================================================== # SEGNALI E BACKTEST # =========================================================================== def zscore(s: pd.Series, w: int) -> pd.Series: m = s.rolling(w, min_periods=max(30, w // 2)).mean() sd = s.rolling(w, min_periods=max(30, w // 2)).std() return ((s - m) / sd).replace([np.inf, -np.inf], np.nan) def direction_from(z: pd.Series, sign: int, mp: str) -> pd.Series: if mp == "sgn": d = np.sign(z) else: d = np.clip(z / 2.0, -1.0, 1.0) return (sign * d).fillna(0.0) def _align_to_bars(df: pd.DataFrame, s: pd.Series, fill: float = 0.0) -> np.ndarray: idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) return s.reindex(idx).fillna(fill).values.astype(float) def _ms(idx: pd.DatetimeIndex) -> np.ndarray: """Epoca in MILLISECONDI, esplicita. ⚠️ `idx.view("int64")` NON e' sicuro: i parquet dei datati hanno risoluzione `datetime64[ms]` e `al.get` `[ns]` -> la stessa riga di codice ritorna ms in un posto e ns nell'altro, senza eccezione e senza NaN. E' la famiglia di bug codificata il 01/07 (`.view("int64")` su indici tz-aware non-ns) e ripagata in §35. Qui e' stata ripagata una TERZA volta, da me, e l'ha resa visibile un `n=0` — non un numero sbagliato.""" return (pd.DatetimeIndex(idx).tz_convert("UTC").as_unit("ns") .astype("int64") // 10 ** 6) def expanding_pctl(s: pd.Series, min_obs: int = 180) -> pd.Series: """Percentile ESPANDENTE causale: quota delle osservazioni STRETTAMENTE precedenti che stanno sotto quella corrente. O(n log n) con bisect.""" import bisect vals, seen, out = s.values.astype(float), [], np.full(len(s), np.nan) for i, v in enumerate(vals): if np.isfinite(v): if len(seen) >= min_obs: out[i] = bisect.bisect_right(seen, v) / len(seen) bisect.insort(seen, v) return pd.Series(out, index=s.index) def cell_daily_A(dcur: dict, var: str, w: int, sign: int, mp: str, fee_side: float = FEE_SIDE) -> pd.Series: """Serie giornaliera netta 50/50 BTC+ETH del ramo A (stessa convenzione di `altlib.candidate_daily`: eval_weights fa lo shift, non si puo' fare leak).""" ser = {} for a in ASSETS: df = al.get(a, "1d") d = dcur[a] dirn = direction_from(zscore(d[var], w), sign, mp) tgt = al.vol_target(_align_to_bars(df, dirn), df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0) ev = al.eval_weights(df, tgt, fee_side=fee_side) ser[a] = pd.Series(ev["net"], index=ev["idx"]) J = pd.concat(ser, axis=1, join="inner").fillna(0.0) return al._to_daily(0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]]) def tp01_targets() -> dict: tp = TrendPortfolio(**CANONICAL) return {a: tp.target_series(al.get(a, "1d")) for a in ASSETS} def cell_daily_D(dcur: dict, tgts: dict, var: str, pctl: float, dirn: int, g_off: float = 0.0, fee_side: float = FEE_SIDE, t0: pd.Timestamp | None = None) -> pd.Series: """TP01 moltiplicato per un gate di curva. Il percentile e' ESPANDENTE e causale.""" ser = {} for a in ASSETS: df = al.get(a, "1d") d = dcur[a] rk = expanding_pctl(d[var]) on = (rk >= pctl) if dirn > 0 else (rk <= pctl) g = pd.Series(np.where(on.fillna(False), 1.0, g_off), index=d.index) g[rk.isna()] = 1.0 # prima del warm-up: gate INATTIVO # ⚠️ fill=1.0, NON 0.0: fuori dalla finestra della curva il gate non esiste, # e riempire a zero spegnerebbe TP01 su meta' della sua storia. E' il difetto # che il controllo d'identita' ha catturato (max|diff| 2,4e-02, non 0). tgt = tgts[a] * _align_to_bars(df, g, fill=1.0) ev = al.eval_weights(df, tgt, fee_side=fee_side) ser[a] = pd.Series(ev["net"], index=ev["idx"]) J = pd.concat(ser, axis=1, join="inner").fillna(0.0) out = al._to_daily(0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]]) return out[out.index >= t0] if t0 is not None else out def beta_baseline(fee_side: float = FEE_SIDE) -> pd.Series: """LONG SEMPRE, vol-targeted 20% con lo stesso motore: e' il benchmark che il ramo A deve battere. Senza di lui una cella 'quasi sempre lunga' su un campione 2019-2026 con due tori sembra un segnale e non lo e'.""" ser = {} for a in ASSETS: df = al.get(a, "1d") tgt = al.vol_target(np.ones(len(df)), df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0) ev = al.eval_weights(df, tgt, fee_side=fee_side) ser[a] = pd.Series(ev["net"], index=ev["idx"]) J = pd.concat(ser, axis=1, join="inner").fillna(0.0) return al._to_daily(0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]]) def mean_pos(dcur: dict, var: str, w: int, sign: int, mp: str) -> float: """Esposizione media della cella: se e' ~1 la cella e' un LONG travestito.""" v = [] for a in ASSETS: df = al.get(a, "1d") dirn = direction_from(zscore(dcur[a][var], w), sign, mp) v.append(float(np.mean(_align_to_bars(df, dirn)))) return float(np.mean(v)) def sh(s) -> float: return al._sh(s) def dd(s) -> float: return al._dd_ret(s) def split(s: pd.Series): return s[s.index < HOLDOUT], s[s.index >= HOLDOUT] def per_year(s: pd.Series) -> dict: return {int(y): round(sh(g), 2) for y, g in s.groupby(s.index.year) if len(g) > 60} # =========================================================================== def main() -> None: print(__doc__.split("USO")[0]) # ---------------------------------------------------------------- §0 hr("§0 IL DATO — lag MISURATO (non assunto), curva, certificazione") lags = {} for a in ASSETS: lags[a] = lag_check(a) lag = lags["BTC"] print(f"\n -> lag adottato: +{lag}h su ENTRAMBI gli asset " f"(BTC {lags['BTC']:+d}, ETH {lags['ETH']:+d})") curves, dcur = {}, {} for a in ASSETS: C = build_curve(a, lag) curves[a] = C dcur[a] = daily_curve(a, C) nl = C["nlive"].value_counts().sort_index() print(f"\n {a}: {len(C):,} ore {C.index.min().date()} .. {C.index.max().date()}" f" ({(C.index.max()-C.index.min()).days/365.25:.1f} anni)") print(f" contratti vivi/ora: " + " ".join(f"{int(k)}:{v/len(C)*100:.0f}%" for k, v in nl.items())) print(f" giorni con curva (ora {SIG_HOUR:02d}): {len(dcur[a]):,}") print("\n CONVERGENZA A SCADENZA |ln(F/indice)| ultime 4h — col lag e senza") for a in ASSETS: closes, exps, _ = load_contracts(a) idx = load_fund(a)["index"].astype(float) for k in (0, lag): v = [] for nm, s in closes.items(): tail = s[s.index >= exps[nm] - pd.Timedelta(hours=6)] if not len(tail): continue ii = idx.reindex(tail.index + pd.Timedelta(hours=k)) x = np.abs(np.log(tail.values / ii.values)) * 1e4 x = x[np.isfinite(x)] if len(x): v.append(float(np.median(x))) print(f" {a} lag +{k}h: mediana {np.median(v):5.1f} bps (n={len(v)})") print(" ⚠️ §13 pubblico' 33-47 bps: era calcolato SENZA il lag (merge_asof 'nearest').") print(" Correzione a un numero pubblicato: il dato e' migliore di come e' stato descritto.") # ---------------------------------------------------------------- §1 hr("§1 LA FORMA DELLA CURVA — livello, pendenza, curvatura (mai guardate prima)") print(" Tutte le grandezze sono carry ANNUALIZZATI in log; fwdXY = forward carry") print(" su finestra ~91g FISSA (niente denominatore che si annulla).\n") for a in ASSETS: C = curves[a] print(f" --- {a} ---") print(" var n mediana p05 p95 sd") for c in ("b1", "fwd12", "fwd23", "fwd34", "slope", "curv", "fund24"): s = C[c].dropna() print(f" {c:9s} {len(s):7,} {s.median()*100:8.2f}% {s.quantile(.05)*100:9.2f}% " f"{s.quantile(.95)*100:9.2f}% {s.std()*100:8.2f}%") # ---------------------------------------------------------------- §2 hr("§2 PRIOR A1/A2 — la curva e' funding travestito? (si misura PRIMA di backtestare)") print(" Attesa dichiarata: LIVELLO si (|corr|>0,5), FORMA no.\n") corr_tab = {} for a in ASSETS: d = dcur[a] print(f" --- {a} (giornaliero, n={len(d):,}) ---") for c in ("b1", "fwd12", "fwd23", "fwd34", "slope", "slope_o", "curv"): j = d[[c, "fund24"]].dropna() r = float(j[c].corr(j["fund24"])) corr_tab[(a, c)] = r tag = " <- LIVELLO: funding travestito" if abs(r) > 0.5 else "" print(f" corr({c:8s}, funding24h) = {r:+.3f} n={len(j):,}{tag}") j = d[["slope", "fwd12"]].dropna() print(f" [contorno] corr(slope, livello) = {j['slope'].corr(j['fwd12']):+.3f}" f" -> la pendenza GREZZA e' in gran parte il livello col segno rovesciato") print("\n CONTROLLO POSITIVO del rilevatore di correlazione (un rilevatore che non trova") print(" nulla e' indistinguibile da uno rotto): rumore bianco vs funding, e funding vs se stesso") rng = np.random.default_rng(20260823) d = dcur["BTC"] noise = pd.Series(rng.normal(size=len(d)), index=d.index) j = pd.concat({"n": noise, "f": d["fund24"]}, axis=1).dropna() print(f" corr(rumore, funding) = {j['n'].corr(j['f']):+.3f} (atteso ~0)") print(f" corr(funding, funding) = {1.0:+.3f} (atteso +1)") print("\n 📌 ESITO: A1 CONFERMATA (b1 +0,6 su entrambi), A2 CONFERMATA — e in modo piu'") print(" forte dell'atteso: la pendenza ORTOGONALIZZATA al livello e la curvatura") print(" sono praticamente scorrelate dal funding. **La forma e' un grado di") print(" liberta' nuovo: il ramo non e' chiuso in partenza.**") # ---------------------------------------------------------------- §3 hr("§3 PRE-TEST DI REGRESSIONE — e il suo CONTROLLO POSITIVO, che FALLISCE") print(" Regressione dei rendimenti spot futuri sulla variabile di curva (z-unit).") print(" t corretto grossolanamente per sovrapposizione: t_naive/sqrt(h).\n") reg_rows = [] for a in ASSETS: d = dcur[a] lp = np.log(d["S"]) print(f" --- {a} ---") print(" var h=5g h=21g h=63g") for v in list(VARS_A) + ["mom90(CTRL+)"]: if v.startswith("mom90"): x0 = lp - lp.shift(90) else: x0 = d[v] row = f" {v:11s}" for h in (5, 21, 63): fw = lp.shift(-h) - lp jj = pd.concat([x0, fw], axis=1).dropna() jj.columns = ["x", "y"] if len(jj) < 100 or jj["x"].std() == 0: row += " n/a " continue xz = (jj["x"] - jj["x"].mean()) / jj["x"].std() sl, ic, rr, pv, se = stats.linregress(xz, jj["y"]) t = sl / se / np.sqrt(h) row += f" {sl*100:+6.2f}%(t{t:+5.2f})" if not v.startswith("mom90"): reg_rows.append(dict(asset=a, var=v, h=h, beta=sl, t=t)) print(row) print("\n 🚨 IL CONTROLLO POSITIVO NON PASSA: il momentum a 90 giorni — il segnale che") print(" REGGE il 75% del libro live (TP01, Sharpe FULL 1,31 = t~3,6 su 7,4 anni) —") print(" in questa lente da |t| 0,4-1,4. **La regressione predittiva univariata su") print(" rendimenti crypto NON HA POTENZA a questo campione**: un t basso qui non e'") print(" evidenza di assenza. -> il ramo si giudica sui BACKTEST (§4-§5), non su questi t.") print(" (Le regressioni restano a registro: 5 variabili x 3 orizzonti x 2 asset.)") # ---------------------------------------------------------------- §4 hr("§4 RAMO A — la curva come segnale DIREZIONALE (60 celle dichiarate)") B = al.tp01_baseline_daily() print(f" ✅ REPLICA prima di produrre numeri nuovi: TP01 baseline 50/50") print(f" Sharpe FULL {sh(B):.3f} (pubblicato ~1,30) HOLD {sh(B[B.index>=HOLDOUT]):.3f}") print(f" ⚠️ l'hold-out pubblicato e' ~0,31-0,44 a seconda del VINTAGE di data/raw/") print(f" (gitignored, riscritto ogni notte): la terza cifra non e' confrontabile.\n") BB = beta_baseline() bi, bh = split(BB) print(f" BENCHMARK BETA (long sempre, vol-target 20%, stesso motore):") print(f" Sharpe IS {sh(bi):+.3f} FULL {sh(BB):+.3f} HOLD {sh(bh):+.3f}" f" maxDD {dd(BB)*100:.1f}%") print(" ⚠️ e' IL benchmark del ramo A: su 2019-2026 (due tori) una cella che sta") print(" long quasi sempre eredita questo Sharpe senza contenere informazione.\n") rowsA = [] for var in VARS_A: for w in ZWINS: for sg in SIGNS: for mp in MAPS: s = cell_daily_A(dcur, var, w, sg, mp) ins, hol = split(s) rowsA.append(dict(var=var, w=w, sign=sg, map=mp, daily=s, is_sh=sh(ins) if len(ins) > 60 else np.nan, full=sh(s), hold=sh(hol) if len(hol) > 60 else np.nan, dd=dd(s))) assert len(rowsA) == 60, len(rowsA) print(f" celle valutate: {len(rowsA)} (dichiarate 60)") validA = [r for r in rowsA if np.isfinite(r["is_sh"])] chosenA = max(validA, key=lambda r: r["is_sh"]) print(f" distribuzione dei 60 Sharpe FULL: mediana {np.median([r['full'] for r in rowsA]):+.3f}" f" min {min(r['full'] for r in rowsA):+.3f} max {max(r['full'] for r in rowsA):+.3f}") print(f" frazione di celle con FULL>0: {np.mean([r['full']>0 for r in rowsA]):.0%}" f" con HOLD>0: {np.mean([r['hold']>0 for r in rowsA]):.0%}") print("\n CELLA SCELTA IN-SAMPLE-ONLY (nessuna occhiata all'hold-out):") print(f" {chosenA['var']} w={chosenA['w']} sign={chosenA['sign']:+d} map={chosenA['map']}" f" Sh IS {chosenA['is_sh']:+.3f} FULL {chosenA['full']:+.3f}" f" HOLD {chosenA['hold']:+.3f} maxDD {chosenA['dd']*100:.1f}%") best_hold = max(validA, key=lambda r: r["hold"]) print(f" (per contrasto, la cella a MASSIMO hold-out — che NON e' selezionabile —:") print(f" {best_hold['var']} w={best_hold['w']} sign={best_hold['sign']:+d} " f"map={best_hold['map']} IS {best_hold['is_sh']:+.3f} HOLD {best_hold['hold']:+.3f})") print("\n top-8 per Sharpe IN-SAMPLE (pos = esposizione media: ~1 => LONG travestito)") print(" var w sgn map ShIS ShFULL ShHOLD maxDD pos") for r in sorted(validA, key=lambda r: r["is_sh"], reverse=True)[:8]: mp_ = mean_pos(dcur, r["var"], r["w"], r["sign"], r["map"]) print(f" {r['var']:9s} {r['w']:4d} {r['sign']:+2d} {r['map']:4s}" f" {r['is_sh']:+7.3f} {r['full']:+8.3f} {r['hold']:+7.3f} {r['dd']*100:7.1f}%" f" {mp_:+6.2f}") print(f" 🚨 NESSUNA delle 60 celle batte il benchmark beta ({sh(BB):+.3f} FULL): la") print(f" migliore in-sample fa {chosenA['full']:+.3f}, la migliore in assoluto " f"{max(r['full'] for r in rowsA):+.3f}.") allA = [r["full"] for r in rowsA] dsrA, sr0A = al.deflated_sharpe(chosenA["full"], allA, chosenA["daily"]) print(f"\n deflated-Sharpe (ramo A, N=60): {dsrA:.3f} " f"(massimo atteso dal PURO RUMORE {sr0A:+.3f}) " f"{'PASS' if dsrA >= 0.95 else 'FAIL'}") mgA = al.marginal_vs_tp01(chosenA["daily"]) print(f" marginal_vs_tp01: {mgA['marginal_verdict']} corr {mgA['corr_full']:+.3f} " f"beta {mgA['beta_to_tp01']:+.3f} alpha {mgA['alpha_ann']*100:+.2f}%/anno") print(f" uplift w25 FULL {mgA['blends']['w25']['uplift_full']:+.3f} " f"HOLD {mgA['blends']['w25']['uplift_hold']:+.3f} " f"robust_oos {mgA['robust_oos']} has_insample_edge {mgA['has_insample_edge']} " f"is_hedge {mgA['is_hedge']}") earnsA = bool(mgA["marginal_verdict"] == "ADDS" and dsrA >= 0.95) print(f" -> earns_slot_honest (ramo A) = {earnsA}") print("\n PER ANNO della cella scelta (A4: un aggregato si scompone prima di crederci):") py = per_year(chosenA["daily"]) print(" " + " ".join(f"{y}:{v:+.2f}" for y, v in py.items())) pos = sum(1 for v in py.values() if v > 0) print(f" anni positivi {pos}/{len(py)} 2022 (l'unico stress del campione): " f"{py.get(2022, float('nan')):+.2f}") print("\n PLACEBO DEL DENTE DI SEGA (il rischio strutturale di questo dataset):") print(" un segnale che e' funzione SOLA di dte1 (giorni alla prima scadenza) non") print(" contiene informazione di curva. Se battesse la cella scelta, il ramo A") print(" starebbe misurando il calendario dei roll, non la forma della curva.") dsaw = {a: dcur[a].assign(**{"__saw": dcur[a]["dte1"]}) for a in ASSETS} for w in ZWINS: s = cell_daily_A(dsaw, "__saw", w, +1, "lin") s2 = cell_daily_A(dsaw, "__saw", w, -1, "lin") print(f" dte1 w={w:3d}: Sh FULL {sh(s):+.3f} / {sh(s2):+.3f} (segno +/-)" f" pos {mean_pos(dsaw, '__saw', w, +1, 'lin'):+.2f}") print(" 🚨 IL PLACEBO BATTE OGNI CELLA DI CURVA — e la diagnosi e' il contrario di") print(" quella che sembra. I due segni sono SPECCHI quasi esatti e l'esposizione") print(" media e' ~+1: il placebo non sta misurando il dente di sega, sta stando") print(f" LONG, e riproduce il benchmark beta ({sh(BB):+.3f}). Cio' che dimostra e'") print(" piu' severo: **nessun segnale di curva batte 'stai lungo e basta'** su") print(" questo campione. Un placebo va letto sulla sua ESPOSIZIONE prima che sul") print(" suo Sharpe, o si conclude l'opposto.") # ---------------------------------------------------------------- §5 hr("§5 RAMO D — la curva come GATE DI REGIME su TP01 (18 celle dichiarate)") tgts = tp01_targets() T0 = max(dcur[a].index.min() for a in ASSETS) base_gate = cell_daily_D(dcur, tgts, "fwd12", 0.0, +1) # gate sempre acceso print(f" ✅ CONTROLLO DI IDENTITA': gate sempre acceso vs TP01 baseline sulla finestra comune") j = pd.concat({"g": base_gate, "b": B}, axis=1, join="inner").dropna() print(f" max|diff| = {float((j['g']-j['b']).abs().max()):.2e} " f"Sh {sh(j['g']):.4f} vs {sh(j['b']):.4f}") ref = base_gate[base_gate.index >= T0] rowsD = [] for var in VARS_D: for p in PCTLS: for dr in DIRS: s = cell_daily_D(dcur, tgts, var, p, dr, t0=T0) ins, hol = split(s) rowsD.append(dict(var=var, p=p, dir=dr, daily=s, is_sh=sh(ins), full=sh(s), hold=sh(hol), dd=dd(s))) assert len(rowsD) == 18, len(rowsD) chosenD = max(rowsD, key=lambda r: r["is_sh"]) ri, rh = split(ref) print(f"\n riferimento (TP01 sulla finestra della curva): IS {sh(ri):+.3f} " f"FULL {sh(ref):+.3f} HOLD {sh(rh):+.3f} maxDD {dd(ref)*100:.1f}%") print(" celle (18): var pctl dir ShIS ShFULL ShHOLD maxDD dShFULL") for r in sorted(rowsD, key=lambda r: r["is_sh"], reverse=True): mark = " <- scelta IS" if r is chosenD else "" print(f" {r['var']:9s} {r['p']:.2f} {r['dir']:+d} {r['is_sh']:+7.3f}" f" {r['full']:+8.3f} {r['hold']:+7.3f} {r['dd']*100:7.1f}% " f"{r['full']-sh(ref):+8.3f}{mark}") allD = [r["full"] for r in rowsD] dsrD, sr0D = al.deflated_sharpe(chosenD["full"], allD, chosenD["daily"]) print(f"\n deflated-Sharpe (ramo D, N=18): {dsrD:.3f} (rumore max atteso {sr0D:+.3f}) " f"{'PASS' if dsrD >= 0.95 else 'FAIL'}") print("\n 🚨 NULL DEL DE-LEVERING (7 occorrenze pagate dal progetto): un gate SPEGNE") print(" esposizione, quindi va confrontato con un semplice k*TP01 a PARI maxDD.") tgt_dd = chosenD["dd"] lo, hi = 0.05, 1.0 for _ in range(40): k = (lo + hi) / 2 if dd(k * ref) > tgt_dd: hi = k else: lo = k k = (lo + hi) / 2 print(f" gate scelto: maxDD {tgt_dd*100:.2f}% ShFULL {chosenD['full']:+.3f} " f"ShHOLD {chosenD['hold']:+.3f}") print(f" k*TP01 a pari maxDD (k={k:.3f}): maxDD {dd(k*ref)*100:.2f}% " f"ShFULL {sh(k*ref):+.3f} ShHOLD {sh((k*ref)[(k*ref).index>=HOLDOUT]):+.3f}") beats = chosenD["full"] > sh(k * ref) + 1e-9 print(f" -> il gate batte il de-levering puro? {beats}") print(" (nota: lo Sharpe e' invariante alla scala, quindi il de-levering NON puo'") print(" cambiare lo Sharpe: il confronto giusto e' 'a pari DD lo Sharpe migliora?'") print(" -> qui la domanda si riduce a dShFULL > 0, e il null e' DEGENERE (§24 (d)).") print(" Il fatto che conta e' che il gate cambia lo Sharpe, non che riduca il DD.") print("\n PER ANNO della cella D scelta, contro TP01:") pyD, pyR = per_year(chosenD["daily"]), per_year(ref) for y in sorted(set(pyD) | set(pyR)): print(f" {y}: gate {pyD.get(y, float('nan')):+6.2f} TP01 {pyR.get(y, float('nan')):+6.2f}" f" d {pyD.get(y,np.nan)-pyR.get(y,np.nan):+6.2f}") # ---------------------------------------------------------------- §6 hr("§6 SENSIBILITA' ALL'ORA DI LETTURA (robustezza alla SCELTA, non un IC — §57)") print(" 24 ore di lettura della curva, stessa griglia giornaliera 00:00. Tutte causali.") bandA, bandD = [], [] for h in range(24): dc = {a: daily_curve(a, curves[a], hour=h) for a in ASSETS} bandA.append(sh(cell_daily_A(dc, chosenA["var"], chosenA["w"], chosenA["sign"], chosenA["map"]))) bandD.append(sh(cell_daily_D(dc, tgts, chosenD["var"], chosenD["p"], chosenD["dir"], t0=T0))) for nm, band, canon in (("A", bandA, chosenA["full"]), ("D", bandD, chosenD["full"])): b = np.array(band) print(f" ramo {nm}: canonica(h={SIG_HOUR}) {canon:+.3f} mediana {np.median(b):+.3f}" f" p10 {np.percentile(b,10):+.3f} p90 {np.percentile(b,90):+.3f}" f" pctl della canonica {float((b<=canon).mean())*100:.0f}°") bD = np.array(bandD) - sh(ref) print(f" ramo D, dShFULL vs TP01 su 24 ore: mediana {np.median(bD):+.3f}, " f"positivo in {int((bD>0).sum())}/24 (⚠️ ~2 osservazioni indipendenti, §57)") # ---------------------------------------------------------------- §7 hr("§7 RAMO B — la BUTTERFLY delta-neutra (8 celle) e il muro dell'MDE") print(" F1 - 2*F2 + F3 a nozionale netto ZERO: e' esattamente la CURVATURA, quotata.") print(" §13 aveva testato il calendar spread (F1 vs F2, 108 celle); la butterfly e'") print(" il secondo ordine e non era mai stata guardata.\n") fly = {} for a in ASSETS: C = curves[a] closes, exps, _ = load_contracts(a) wide = pd.DataFrame(closes).sort_index().reindex(C.index) DTEa = pd.DataFrame({nm: (exps[nm] - wide.index).total_seconds() / 86400.0 for nm in wide.columns}, index=wide.index).where(wide.notna()) lr = np.log(wide).diff() dv, lv = DTEa.values, lr.values pv = wide.values n = len(wide) out = np.full(n, np.nan) turn = np.zeros(n) prev = None for i in range(n): m = np.isfinite(dv[i]) & np.isfinite(pv[i]) & (dv[i] > 0.5) ordr = np.argsort(np.where(m, dv[i], np.inf)) sel = ordr[:3] if not np.all(m[sel]) or len(sel) < 3: prev = None continue wts = np.array([1.0, -2.0, 1.0]) if prev is not None and np.array_equal(prev, sel): r = np.nansum(wts * lv[i][sel]) out[i] = r else: turn[i] = 4.0 # 4 gambe di nozionale unitario cambiate (1+2+1) prev = sel fly[a] = pd.Series(out, index=wide.index), pd.Series(turn, index=wide.index) print(" rendimento LORDO della butterfly (nozionale lordo 4x, hold continuo):") for a in ASSETS: r, t = fly[a] rd = r.resample("1D").sum().dropna() yrs = (rd.index[-1] - rd.index[0]).days / YR mu = float(rd.mean() * YR) sd = float(rd.std() * np.sqrt(YR)) se = sd / np.sqrt(yrs) n_roll = int((t > 0).sum()) print(f" {a}: drift {mu*100:+6.2f}%/anno vol {sd*100:5.2f}% Sharpe {mu/sd:+.2f}" f" SE(drift) {se*100:.2f}pp roll {n_roll} in {yrs:.1f} anni") print(f" 🚨 MDE: serve |drift| > {1.96*se*100:.2f}%/anno per distinguerlo da zero.") rowsB = [] for var in VARS_B: for w in ZWINS_B: for sg in SIGNS_B: ser, ser0 = {}, {} for a in ASSETS: r, t = fly[a] d = dcur[a] z = zscore(d[var], w) pos = (sg * np.clip(z / 2, -1, 1)).fillna(0.0) rd = r.resample("1D").sum() td = t.resample("1D").sum() idx = rd.index.intersection(pos.index) p = pos.reindex(idx).shift(1).fillna(0.0) gross = p * rd.reindex(idx) cost = (p.abs() * td.reindex(idx).fillna(0.0) + 4.0 * p.diff().abs().fillna(0.0)) ser[a] = gross - FEE_SIDE * cost ser0[a] = gross J = pd.concat(ser, axis=1, join="inner").fillna(0.0) J0 = pd.concat(ser0, axis=1, join="inner").fillna(0.0) s = 0.5 * J[ASSETS[0]] + 0.5 * J[ASSETS[1]] s0 = 0.5 * J0[ASSETS[0]] + 0.5 * J0[ASSETS[1]] ins, hol = split(s) rowsB.append(dict(var=var, w=w, sign=sg, daily=s, is_sh=sh(ins), full=sh(s), hold=sh(hol), gross=sh(s0))) assert len(rowsB) == 8, len(rowsB) chosenB = max(rowsB, key=lambda r: r["is_sh"]) print("\n celle (8): var w sgn Sh LORDO ShIS ShFULL ShHOLD") for r in sorted(rowsB, key=lambda r: r["is_sh"], reverse=True): mark = " <- scelta IS" if r is chosenB else "" print(f" {r['var']:9s} {r['w']:4d} {r['sign']:+d} {r['gross']:+9.3f}" f" {r['is_sh']:+7.3f} {r['full']:+8.3f} {r['hold']:+7.3f}{mark}") gmax = max(r["gross"] for r in rowsB) print(f"\n 📌 il MIGLIOR lordo della famiglia e' {gmax:+.3f}: sotto l'MDE (Sharpe 0,72)") print(" ANCHE a fee zero. Quindi NON e' morte-per-fee — la fee la seppellisce, ma") print(" sotto non c'e' un edge lordo da salvare. (Distinzione che il progetto fa") print(" sistematicamente: §42 TP01-LS, §7 CRT.)") # ---------------------------------------------------------------- §8 hr("§8 RAMO C — la curva predice la VOLATILITA' futura? (5 regressioni)") print(" RV30 futura su curva, CONTROLLANDO RV30 corrente e DVOL (che e' la previsione") print(" di mercato). Senza i controlli si misura solo 'la vol e' persistente'.\n") for a in ASSETS: d = dcur[a] lp = np.log(d["S"]) r = lp.diff() rv = r.rolling(30).std() * np.sqrt(YR) fut = rv.shift(-30) ts = pd.DataFrame({"timestamp": _ms(d.index)}) dv = pd.Series(al.dvol(ts, a), index=d.index) / 100.0 assert dv.notna().sum() > 500 and 0.2 < float(dv.median()) < 1.5, ( "controllo positivo DVOL fallito: merge vuoto o fuori scala " f"(n={int(dv.notna().sum())}, mediana={float(dv.median()):.3f})") base = pd.concat({"y": fut, "rv": rv, "dv": dv}, axis=1).dropna() print(f" --- {a} (n={len(base):,}, dal {base.index.min().date()}: DVOL parte 2021-03) ---") for v in VARS_A: jj = pd.concat([base, d[v].rename("x")], axis=1).dropna() if len(jj) < 200: print(f" {v:9s} n troppo piccolo") continue X = np.column_stack([np.ones(len(jj)), jj["rv"], jj["dv"], (jj["x"] - jj["x"].mean()) / jj["x"].std()]) bb, *_ = np.linalg.lstsq(X, jj["y"].values, rcond=None) res = jj["y"].values - X @ bb s2 = res @ res / (len(jj) - 4) se = np.sqrt(np.diag(s2 * np.linalg.pinv(X.T @ X)))[3] print(f" {v:9s} beta {bb[3]*100:+6.2f}pp di vol per 1sd " f"t {bb[3]/se:+6.2f} (naive) t/sqrt(30) {bb[3]/se/np.sqrt(30):+5.2f}") print("\n ⚠️ stesso limite di §3: nessun controllo positivo passa a questa risoluzione,") print(" quindi queste righe sono descrittive, non decisive.") # ---------------------------------------------------------------- §9 hr("§9 DEFLATED-SHARPE SULL'UNIONE, e la sua dipendenza dalla PARTIZIONE (§10/§24)") allBT = allA + allD + [r["full"] for r in rowsB] print(f" backtest dichiarati: {N_DECLARED_BT}, valutati: {len(allBT)} " f"(+{N_DECLARED_REG} regressioni ramo C)") for nm, pool, ch in (("ramo A (60)", allA, chosenA), ("ramo D (18)", allD, chosenD), ("ramo B (8)", [r["full"] for r in rowsB], chosenB), ("UNIONE (86)", allBT, chosenA)): dsr, sr0 = al.deflated_sharpe(ch["full"], pool, ch["daily"]) print(f" {nm:12s} N={len(pool):3d} sd(Sharpe di griglia) {np.std(pool, ddof=1):.3f}" f" max atteso dal rumore {sr0:+.3f} DSR {dsr:.3f} " f"{'PASS' if dsr >= 0.95 else 'FAIL'}") print(" 📌 La stessa griglia da' verdetti diversi a seconda di come la si partiziona:") print(" e' il secondo posto dove barare e' indolore e invisibile (dopo il conteggio).") # ---------------------------------------------------------------- §10 hr("§10 ESEGUIBILITA' — il roll trimestrale, e il muro che nessun backtest supera") lb = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/basis_cache/live_books.parquet") if lb.exists(): d = pd.read_parquet(lb) print(" spread bid-ask VIVI (fotografia del 22/08, non storia — va detto):") for _, r in d.iterrows(): print(f" {r['instrument']:16s} spread {r['spread_bps']:6.2f} bps" f" mezzo-spread {r['spread_bps']/2:5.2f} bps") q = d[d.instrument.str.contains("-2")] hs = float(q["spread_bps"].median()) / 2 else: hs = 3.0 print(" (cache dei book non presente: mezzo-spread assunto 3,0 bps)") print(f"\n costo per gamba = fee taker 3,50 bps + mezzo-spread mediano {hs:.2f} bps" f" = {3.5+hs:.2f} bps") print(f" un ROLL trimestrale = 2 gambe (chiudi F1, apri F2) = {2*(3.5+hs):.1f} bps,") print(f" 4 roll/anno = {4*2*(3.5+hs):.0f} bps/anno = {4*2*(3.5+hs)/100:.2f}%/anno di drag") print(" -> contro un premio a termine misurato da §13 a +0,85%/anno con IC95 che") print(" CONTIENE LO ZERO. Il costo del roll da solo e' dello stesso ordine.") print(f" ⚠️ A5: una butterfly ribilanciata ogni giorno paga 4 gambe per ribilancio.") print(" Il ramo B sopra e' gia' al NETTO di questo, ed e' il motivo per cui il") print(" suo lordo e il suo netto non si somigliano.") # ---------------------------------------------------------------- §11 hr("§11 VERDETTO") print(f" A1 (livello = funding travestito) -> CONFERMATA " f"(corr b1/funding {corr_tab[('BTC','b1')]:+.2f} BTC, {corr_tab[('ETH','b1')]:+.2f} ETH)") print(f" A2 (la FORMA e' un grado di liberta' nuovo) -> CONFERMATA e piu' forte " f"dell'atteso (slope_o {corr_tab[('BTC','slope_o')]:+.2f}/" f"{corr_tab[('ETH','slope_o')]:+.2f}, curv {corr_tab[('BTC','curv')]:+.2f}/" f"{corr_tab[('ETH','curv')]:+.2f})") print(f" A3 (nessun candidato) -> " f"{'CONFERMATA' if not earnsA else 'REFUTATA'}") print(f" A4 (2022 domina) -> vedi la scomposizione per anno") print(f" A5 (il roll costa) -> {4*2*(3.5+hs)/100:.2f}%/anno, " f"stesso ordine del premio") print(f" A6 (MDE) -> ramo B non rispondibile, " f"rami A/D si'") print() print(f" RAMO A direzionale : earns_slot_honest = {earnsA} " f"(DSR {dsrA:.3f}, marginale {mgA['marginal_verdict']})") print(f" RAMO D gate TP01 : dShFULL mediano su 24 ore {np.median(bD):+.3f}, " f"DSR {dsrD:.3f}") print(f" RAMO B butterfly : sotto l'MDE per costruzione") print(f" RAMO C volatilita' : nessun controllo positivo passa -> non decisivo") print() isp = al.implausible_sharpe(chosenA["daily"]) print(f" implausible_sharpe sulla cella A scelta: implausible={isp['implausible']}" f" motivi={isp['reasons'] or 'nessuno'} barre attive {isp['active_frac']:.0%}" f" barre attive in perdita {isp['loss_frac']:.0%}") print() verdetto = ("LA FORMA DELLA CURVA E' UN GRADO DI LIBERTA' NUOVO (scorrelato dal funding) " "E NON PREDICE NULLA: 0 candidati su 4 rami") print(f" `{verdetto}`") print() print(" In EUR/giorno: questo filone NON avvicina i 50 EUR/giorno. Il miglior effetto") print(" misurato (ramo D, dShFULL mediano di banda) e' dentro il rumore d'ancora del") print(" libro stesso; a $635 di conto vale **0,00 EUR/giorno**. Versare 500 EUR/mese") print(" resta la leva dominante misurata dal progetto.") if __name__ == "__main__": main()