diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 31f6992..4d1b099 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -700,6 +700,7 @@ stessi hanno nominato senza poterli eseguire**. Stesso contratto di consegna, st | 32 | SKH-ANCHOR-PROP | `GATE PROP-01` gamba (b), datata 31/10: girata oggi | **PASS 23/23** — e il numero scende ancora, a **4,4%** | | 33 | WORST-DAY | il peggior giorno del libro, MISURATO: il gradino di leva e' autorizzabile? | **LEAD** — l'aritmetica passa, **il knob non esiste** | | 34 | CC01-2022 | il carry riaperto sui dati che gli mancavano per costruzione | **RESTA SCARTATO** — per la ragione giusta, ma non quella pubblicata | +| 35 | FUNDING | il costo mai prezzato di TENERE una posizione aperta | **LEAD** — **−2,16%/anno di drift**, muri **+17,5%** | | 22 | MAKER | l'esecuzione passiva e' una fonte di ritorno, al netto del costo di non essere eseguiti? | **SCARTATO** — il segno dipende da `<` contro `<=` | | 23 | BOCPD | un rilevatore di cambio di regime vero batte il miglior lookback COSTANTE? | **SCARTATO** — filone chiuso definitivamente | | 24 | CRITICO | cosa NON e' stato misurato, e quale singola misura mancante vale di piu' | **3 correzioni all'ondata**, 1 regola mia ritirata | @@ -1634,3 +1635,80 @@ contro −6,0%, l'85% della perdita) perche' il funding negativo e' **persistent **VERDETTO: `CC01 COL 2022 DENTRO: RESTA SCARTATO — e per la ragione giusta, ma quella pubblicata era necessaria e non sufficiente.`** + +--- + +## 35 — FUNDING (il costo che il progetto non aveva mai prezzato) + +`scripts/research/r0822d_funding.py` (1.212 righe, 32 s, `--no-net` usa la cache). **Nessuna cella e' +una selezione** → niente DSR da calcolare. Ridotto per budget: muri a 2.000 path (**risoluzione ~1%, +dichiarata**). + +Il progetto ha prezzato fee, slippage, min-order, pavimento IB, haircut, fortuna d'ancora, degrado +d'esecuzione, look-ahead, backfill, split, contaminazione EUR/USD, rischio di venue e fisco — **e mai +il costo di TENERE APERTA una posizione su un perpetual.** + +| serie | ShFULL | ShHOLD | maxDD | CAGR | drift | +|---|---|---|---|---|---| +| LIBRO 75/25 path live | 1,692 | 1,214 | 11,2% | 20,43% | 19,24% | +| **+ funding** | **1,513** | **1,154** | **11,5%** | **17,87%** | **17,08%** | +| Δ | −0,179 | −0,060 | **+0,3pp** | −2,56% | **−2,16%** | +| TP01 canonico | 1,314 → **1,146** | 0,489 → 0,420 | 14,3 → 15,2% | 16,43 → 14,02% | −2,10% | +| SKH01 50/50 path live | 1,247 → **1,154** | 1,456 → 1,439 | 24,6 → 24,8% | 30,56 → 27,59% | −2,32% | + +📌 **Il costo va citato CON LA FINESTRA: −2,16%/anno** (2019-2026, proxy inverse sul 40% del campione) +· **−1,39%** sulla sola finestra dello strumento vero (≥2022-03) · **−0,55%** sul 2025+ — e il 2022 e +il 2026 costano ~0 **perche' il libro era FLAT** (esposizione 0,008 / 0,018), non perche' il funding +sia sparito. + +🚨 **IL MECCANISMO, ed e' il risultato trasferibile: esposizione e funding sono POSITIVAMENTE +CORRELATI.** Il tasso che il libro paga davvero e' **1,86-2,55×** l'incondizionato per TP01 e +**2,41-3,44×** per SKH01. **Il prodotto ingenuo "esposizione media × tasso medio" sottostima di ~2×** +— ed e' esattamente la forma della stima grezza da cui e' nato il filone. +❌ **L'ipotesi da cui il filone e' partito — "SKH01 paga meno perche' sta a mercato il 12% del tempo" +— e' FALSA:** 2,24%/anno di sleeve contro **2,02%** di TP01. Quando e' a mercato SKH01 sta a nozionale +**1,0×** (TP01 vol-targeted sta a **0,14×**) **ed entra sui breakout, cioe' quando il funding e' caro.** +**Il peso ottimale resta 0,25** con e senza funding, e il delta e' negativo a **23/23 offset per ogni w**. +⚠️ **Il proxy Hyperliquid era 2,1-2,2× il venue vero** sulle stesse ore (14,8% contro 6,8-7,1%): **due +errori opposti — tasso troppo alto, esposizione condizionale troppo bassa — che quasi si +compensavano.** *Compensarsi non e' misurare.* + +🚨 **COSA CAMBIA NEI NUMERI PUBBLICATI:** rendita perpetua **10,73% → 9,14%**; muro +**$276,6k → $325,0k (+17,5%)**; traiettoria da $600 a €250/mese **16,4a con P(20a) 90% → 18,7a con +P(20a) 64%**; **soffitto direzionale ~1,31 → ~1,15**. +📌 **E il maxDD NON si muove (+0,3pp): e' una tassa sul DRIFT, non sulla coda** — per questo colpisce i +**muri** molto piu' dello Sharpe. Il gradino di leva **regge** (k* 13,7 → 12,3, guadagno del passo +1,00→1,25× −12%). +**NON cambia:** il peso **75/25**, il gradino di leva, il maxDD, ne' **l'ordine delle leve del piano**. + +⚠️ **AVVERTENZA DEL COORDINATORE, non dell'agente: questa correzione e quella del FISCO si SOMMANO, e +nessuna tabella pubblicata le contiene entrambe.** La riga "€250/mese" vale P(20a) **92% al lordo** · +**52% col fisco** (07/08) · **64% col funding** (oggi) — le due lenti sono state applicate +**separatamente** allo stesso numero lordo. La tabella con entrambe **non esiste ancora**, e il +risultato congiunto sara' **peggiore di 52%**. + +✅ **Validato contro il CONTO VERO, e per ELIMINAZIONE invece che per grandezza:** modello contro +`realized_pnl` di sessione **ETH −0,2% / BTC −7,7%**, mentre le finestre sbagliate sbagliano di 2-4×. +L'identificazione regge perche' nella sessione ci sono stati **0 ordini** (letti dal log) → il P&L di +trading realizzato e' **esattamente zero**, quindi quel campo non puo' essere P&L di trading; e il +segno e' negativo su due gambe **entrambe long**. Funding realmente pagato da quando il libro esegue: +**$0,22**. Convenzione temporale **verificata sui dati** (`interest_8h` = somma delle 8 ore trailing +incluso T: mediana |dif| **2,2e-08** contro 1,1e-06 dell'alternativa). Replica **bit-exact 5/5**. + +⚠️ **BUG CATTURATO DAL CONTROLLO POSITIVO, non a occhio, e in famiglia gia' nota:** +`DatetimeIndex.astype("int64")` su tre serie dello stesso progetto con risoluzioni **diverse** +(`[ms]` il funding, `[us]` `al.get`, `[s]` il log) dava funding **0,000%/anno su SKH01** e +**+1,229%/GIORNO su TP01**. **Nessuna eccezione, nessun NaN: due numeri plausibili in due punti +diversi.** E' la stessa famiglia codificata in CLAUDE.md il **01/07** (`DatetimeIndex.view("int64")` +su indici tz-aware non-ns → look-ahead che `causality_ok` non vede). Ora c'e' un controllo che +confronta `searchsorted` con un `groupby` di pandas **e verifica di avere potenza**. +⚠️ **Secondo errore corretto in sessione:** contava `diff(pos) < 0` come vendite — **su un perpetuale +lineare il nozionale scende da solo quando scende il mark.** + +**Smentitore principale dichiarato:** sul **pre-2022** l'agente usa l'**inverse come proxy** e i due +strumenti correlano solo **0,27-0,40** orario → se il funding dei lineari nel 2019-2021 fosse stato +molto piu' basso, il −2,16% scenderebbe verso il **−1,39%** della finestra pulita. + +**VERDETTO: LEAD** (misura di politica, non strategia). +**`IL FUNDING COSTA 2,16%/ANNO NETTO AL LIBRO` — quanto la fortuna d'ancora ×0,89 che il progetto ha +misurato con enorme cura, e sposta i muri di capitale del +17,5%.** diff --git a/scripts/research/r0822d_funding.py b/scripts/research/r0822d_funding.py new file mode 100644 index 0000000..e18f057 --- /dev/null +++ b/scripts/research/r0822d_funding.py @@ -0,0 +1,1212 @@ +#!/usr/bin/env python +"""r0822d_funding.py — IL FUNDING DEI PERPETUAL: il costo che il progetto non ha mai prezzato. + +CONTESTO (ondata 2026-08-22, sezione 33 di docs/research/RESULTS-0822.md). Verificato dal +coordinatore: **zero occorrenze della parola `funding`** in `src/backtest/harness.py`, +`src/strategies/trend_portfolio.py`, `src/portfolio/{sleeves,portfolio}.py` e in tutto +`src/live/`. Il progetto ha prezzato fee, slippage, min-order, pavimento IB, haircut small-cap, +fortuna d'ancora, degrado d'esecuzione, look-ahead, split, contaminazione EUR/USD, rischio di +venue e fisco — e MAI il costo di TENERE APERTA una posizione. + +La stima grezza che ha fatto emergere il buco: proxy Hyperliquid 14,8%/anno x esposizione long +media di TP01 0,103·E = ~1,53%/anno = il 9% del drift de-luckato. Questo script la sostituisce +con una MISURA. + +COSA FA (un grado di liberta' per volta, replica di controllo prima di ogni delta): + 0. IL DATO. Storico funding da `public/get_funding_rate_history` di Deribit per i QUATTRO + perpetual: i LINEARI USDC (`BTC_USDC-PERPETUAL`/`ETH_USDC-PERPETUAL`, che sono cio' che il + libro esegue davvero) e gli INVERSE (`BTC-PERPETUAL`/`ETH-PERPETUAL`, che hanno la storia + lunga). Copertura dichiarata, buchi contati, accordo linear-vs-inverse MISURATO sulla + finestra comune. Cache su scratchpad; pacing <=2 req/s; mai nei minuti :07 (cron_book) e + :25-:29 (cron_chain). + 1. REPLICA DI CONTROLLO. Con funding SPENTO le repliche devono riprodurre BIT-EXACT + `al.tp01_baseline_daily`, `CC.tp01_realistic`, `r02.sim_equity` e `CC.book_series('hourly')`. + Nessun delta viene stampato prima che questa sezione passi. + 2. IL SEGNO. Il funding si PAGA da long in contango e si INCASSA da short. TP01 e' long-flat + (paga quasi sempre), SKH01 e' long/short e sta a mercato una frazione del tempo (puo' + incassare). Il segno NON e' assunto: e' misurato appaiando la posizione oraria/giornaliera + di ciascuno sleeve col funding del SUO strumento. + 3. I NUMERI. Sharpe FULL/IS/HOLD, maxDD, CAGR, drift di TP01, SKH01 e del libro 75/25, con e + senza funding, e il DELTA su ciascuno. + 4. LE DECISIONI. Soffitto ~1,3 · peso 75/25 (sweep w x banda d'ancora, mediana delle + DIFFERENZE APPAIATE) · muri di capitale e rendita perpetua (che vivono sul DRIFT) · + k* e il gradino di leva (il funding e' proporzionale all'esposizione, quindi scala con k). + 5. CONTROLLO SUL CONTO VERO. Il libro esegue dal 2026-07-08: posizione oraria ricostruita da + `logs/cron_book.log`, funding modellato coi tassi REALI, confronto col `realized_pnl` che + il conto riporta sulle due posizioni aperte. + 6. COSA NON E' CATTURATO, quantificato: l'approssimazione "esposizione media x tasso medio" + contro l'accrual esatto per trade (su SKH01, che entra ed esce), il proxy inverse prima + del 2022-03, la discretezza del pagamento. + +VINCOLI: nessun file di produzione toccato (config/, src/, scripts/live/, cron INVARIATI); +nessun commit; output solo a video + cache in scratchpad. Runtime ~2-6 min con cache calda. + +Uso: `uv run python scripts/research/r0822d_funding.py` + `--no-net` usa solo la cache (nessuna richiesta a Deribit) +""" +from __future__ import annotations + +import datetime as dt +import json +import re +import sys +import time +import urllib.request +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path("/opt/docker/PythagorasGoal") +sys.path.insert(0, str(ROOT / "scripts" / "research")) +sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) +sys.path.insert(0, str(ROOT)) + +import altlib as al # noqa: E402 +import r0702_anchor_skh01 as r02 # noqa: E402 (run_asset / sim_equity / get5m) +import r0724_skh_live_weight as skl # noqa: E402 (skh_book, W_GRID, OFFSETS) +import r0725_capcurve as CC # noqa: E402 (tp01_realistic, book_series, survival) +import r0726_capwall_refresh as WR # noqa: E402 (perp_and_wall, trajectory) +from src.data.downloader import load_data # noqa: E402 +from src.portfolio.portfolio import combine_outer, metrics # noqa: E402 +from src.strategies.trend_portfolio import ( # noqa: E402 + CANONICAL, TrendPortfolio, resample_1d, simple_returns) + +# --------------------------------------------------------------------------- +# Costanti dichiarate +# --------------------------------------------------------------------------- +# Cache dello storico funding FUORI dal repo (`data/raw/` non e' toccabile da questo filone, +# e questi parquet non sono dato certificato: sono una cache ri-scaricabile in ~4 minuti). +_SP = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/fund") +SCRATCH = _SP if _SP.parent.exists() else Path("/tmp/pythagoras_funding_cache") +API = "https://www.deribit.com/api/v2/public/get_funding_rate_history" +SPAN_MS = 30 * 24 * 3600 * 1000 # 30 giorni per richiesta (l'endpoint da' 744 record) +HOLDOUT = al.HOLDOUT +ASSETS = ("BTC", "ETH") +MSH = 3_600_000 +ALLOC = 600.0 # il capitale su cui gira book_series (conto reale $635) +DELUCK = 0.89 # fattore d'ancora MISURATO (r0726_deluck_factor) +EURUSD = CC.EURUSD +N_PATHS_WALL = 2000 # risoluzione MC dichiarata (il muro e' +-1%) + +# strumento -> (data di creazione su Deribit, asset) +INSTR = { + "BTC_USDC-PERPETUAL": ("2022-03-09", "BTC", "linear"), + "ETH_USDC-PERPETUAL": ("2022-03-14", "ETH", "linear"), + "BTC-PERPETUAL": ("2018-08-14", "BTC", "inverse"), + "ETH-PERPETUAL": ("2019-03-14", "ETH", "inverse"), +} +LINEAR = {"BTC": "BTC_USDC-PERPETUAL", "ETH": "ETH_USDC-PERPETUAL"} +INVERSE = {"BTC": "BTC-PERPETUAL", "ETH": "ETH-PERPETUAL"} + +USE_NET = "--no-net" not in sys.argv +T0 = time.time() + + +def sez(t: str) -> None: + print("\n" + "=" * 100) + print(f" {t}") + print("=" * 100) + + +def clock() -> str: + return f"[{time.time() - T0:5.0f}s]" + + +# =========================================================================== +# 0) IL DATO — storico funding Deribit +# =========================================================================== +def _pace() -> None: + """<=2 req/s, e mai nei minuti :07 (cron_book) e :25-:29 (cron_chain).""" + while dt.datetime.now(dt.timezone.utc).minute in (7, 25, 26, 27, 28, 29): + time.sleep(20) + time.sleep(0.55) + + +def fetch_funding(instr: str, start_iso: str) -> pd.DataFrame: + """Storico orario del funding, incrementale su cache parquet.""" + SCRATCH.mkdir(parents=True, exist_ok=True) + out = SCRATCH / f"{instr.replace('-', '_')}.parquet" + have = pd.read_parquet(out) if out.exists() else pd.DataFrame() + if not USE_NET: + if not len(have): + raise RuntimeError(f"--no-net ma cache assente per {instr}") + return have + t0 = int(pd.Timestamp(start_iso, tz="UTC").timestamp() * 1000) + if len(have): + t0 = int(have["ts"].max()) - MSH + tend = int(pd.Timestamp.now(tz="UTC").floor("h").timestamp() * 1000) + rows, a = [], t0 + while a < tend: + b = min(a + SPAN_MS, tend) + u = f"{API}?instrument_name={instr}&start_timestamp={a}&end_timestamp={b}" + for att in range(4): + try: + _pace() + r = json.load(urllib.request.urlopen(u, timeout=30))["result"] + break + except Exception: + if att == 3: + raise + time.sleep(3 * (att + 1)) + rows.extend({"ts": x["timestamp"], "i1h": x["interest_1h"], + "i8h": x["interest_8h"], "index": x["index_price"]} for x in r) + a = b + df = pd.concat([have, pd.DataFrame(rows)], ignore_index=True) if len(have) else pd.DataFrame(rows) + df = df.drop_duplicates(subset="ts").sort_values("ts").reset_index(drop=True) + df.to_parquet(out, index=False) + return df + + +def sezione0() -> dict: + sez("0) IL DATO — storico funding Deribit (endpoint pubblico get_funding_rate_history)") + print(" Il libro esegue sui LINEARI USDC. Gli INVERSE sono qui solo perche' hanno la storia") + print(" lunga: servono come PROXY prima del 2022-03, e l'accordo fra i due si MISURA.") + print(" (4a e 5a occorrenza dello schema `fee_watch`: puntare la misura sull'inverse") + print(" avrebbe misurato uno strumento che il libro non trada.)\n") + F = {} + print(f" {'strumento':<22} {'righe':>7} {'da':>12} {'a':>12} {'ore attese':>11} " + f"{'buchi':>6} {'ann. medio':>11}") + for instr, (start, asset, kind) in INSTR.items(): + d = fetch_funding(instr, start) + d = d.copy() + d["dt"] = pd.to_datetime(d["ts"], unit="ms", utc=True) + d = d.drop_duplicates(subset="ts").sort_values("dt").set_index("dt") + span_h = int((d.index.max() - d.index.min()) / pd.Timedelta("1h")) + 1 + gaps = span_h - len(d) + ann = float(d["i1h"].mean()) * 24 * 365.25 + F[instr] = d + print(f" {instr:<22} {len(d):>7,} {str(d.index.min().date()):>12} " + f"{str(d.index.max().date()):>12} {span_h:>11,} {gaps:>6,} {ann:>10.2%}") + + b0 = pd.Timestamp("2019-03-14", tz="UTC") + sw = min(F[LINEAR[a]].index.min() for a in ASSETS) + tot_d = (F[LINEAR["BTC"]].index.max() - b0).days + print("\n ⚠️ VINCOLO DEL FILONE, da dire subito: i perpetual LINEARI USDC esistono dal") + print(f" {sw.date()}. Il backtest del libro parte dal 2019-03-14 -> per " + f"{(sw-b0).days/tot_d:.0%} del campione ({(sw-b0).days:,} giorni su {tot_d:,}) il") + print(" funding dello strumento ESEGUITO non esiste. Si usa l'inverse come proxy e si") + print(" misura quanto i due differiscono dove coesistono.") + + print("\n ANNO per ANNO (annualizzato, ore x 24 x 365.25):") + print(f" {'anno':>6} " + " ".join(f"{k.replace('-PERPETUAL',''):>12}" for k in INSTR)) + yrs = sorted({y for d in F.values() for y in d.index.year.unique()}) + for y in yrs: + cells = [] + for k in INSTR: + d = F[k] + g = d[d.index.year == y]["i1h"] + cells.append(f"{g.mean()*24*365.25:>11.2%}" if len(g) > 100 else f"{'—':>12}") + print(f" {y:>6} " + " ".join(cells)) + + print("\n CONVENZIONE TEMPORALE — verificata dai dati, non assunta dalla documentazione.") + print(" Se il record etichettato T copre l'ora CHE FINISCE in T, allora `interest_8h` deve") + print(" essere la somma degli 8 `interest_1h` fino a T INCLUSO. Le due alternative danno") + print(" numeri diversi e si vedono:") + print(f" {'ipotesi':<26} {'corr':>9} {'mediana |dif|':>15}") + d = F["BTC_USDC-PERPETUAL"] + h = {"trailing, T incluso": d["i1h"].rolling(8).sum(), + "trailing, T escluso": d["i1h"].shift(1).rolling(8).sum(), + "leading (T -> T+8h)": d["i1h"][::-1].rolling(8).sum()[::-1].shift(-1)} + for lbl, x in h.items(): + m = (~x.isna()) & (~d["i8h"].isna()) + print(f" {lbl:<26} {float(x[m].corr(d['i8h'][m])):>9.6f} " + f"{float((x[m]-d['i8h'][m]).abs().median()):>15.3e}") + print(" -> 'trailing, T incluso'. E' la convenzione usata da tutto lo script.") + + # accordo linear vs inverse sulla finestra comune + print("\n ACCORDO LINEAR vs INVERSE sulla finestra comune (ore appaiate):") + print(f" {'asset':>6} {'ore':>8} {'ann.LIN':>9} {'ann.INV':>9} {'corr':>7} " + f"{'diff annua':>11} {'|diff| med/ora':>15}") + agree = {} + for a in ASSETS: + L, I = F[LINEAR[a]]["i1h"], F[INVERSE[a]]["i1h"] + J = pd.concat({"L": L, "I": I}, axis=1, join="inner").dropna() + d_ann = float((J["L"] - J["I"]).mean()) * 24 * 365.25 + agree[a] = dict(n=len(J), lin=float(J["L"].mean()) * 24 * 365.25, + inv=float(J["I"].mean()) * 24 * 365.25, + corr=float(J["L"].corr(J["I"])), diff=d_ann) + print(f" {a:>6} {len(J):>8,} {agree[a]['lin']:>8.2%} {agree[a]['inv']:>8.2%} " + f"{agree[a]['corr']:>7.3f} {d_ann:>10.2%} {float((J['L']-J['I']).abs().median()):>15.2e}") + print("\n IL PROXY DA CUI E' NATO IL FILONE — Hyperliquid (data/raw/hlfund_*_1h.parquet)") + print(" contro il venue su cui il libro esegue davvero, sulle ore APPAIATE:") + print(f" {'asset':>6} {'ore comuni':>11} {'ann. HL':>9} {'ann. Deribit':>13} {'rapporto':>9}") + for a in ASSETS: + try: + hl = pd.read_parquet(ROOT / "data" / "raw" / f"hlfund_{a.lower()}_1h.parquet")["funding"] + except Exception: + print(f" {a:>6} {'—':>11} (file assente)") + continue + hl.index = pd.DatetimeIndex(hl.index).floor("h") + hl = hl[~hl.index.duplicated(keep="last")] + de = F[LINEAR[a]]["i1h"] + J = pd.concat({"hl": hl, "de": de}, axis=1, join="inner").dropna() + print(f" {a:>6} {len(J):>11,} {float(J['hl'].mean())*24*365.25:>9.2%} " + f"{float(J['de'].mean())*24*365.25:>13.2%} " + f"{float(J['hl'].mean())/float(J['de'].mean()):>8.2f}x") + print(" ⚠️ Il funding NON e' una proprieta' del mercato, e' una proprieta' del VENUE (formula") + print(" e cap diversi). Usare il tasso di Hyperliquid per stimare il costo di un libro che") + print(" esegue su Deribit e' lo stesso errore di `fee_watch` il 21/08 — misurare uno") + print(" strumento che non si trada — con l'aggravante che qui non e' nemmeno lo stesso") + print(" exchange. (Il numero finale esce comunque piu' ALTO della stima grezza: due") + print(" errori in direzioni opposte, il tasso troppo alto e l'esposizione condizionale") + print(" troppo bassa. Compensarsi non e' misurare.)") + print(" 📌 E ha una CONSEGUENZA di venue: se a $20k si riapre lo split e XS01 va su HL, il") + print(" suo funding va prezzato col tasso di HL, non con questo — ed e' un altro numero.") + + return dict(F=F, agree=agree) + + +def build_rate(F: dict) -> dict: + """Serie oraria del funding per asset: LINEARE dove esiste, INVERSE prima (proxy dichiarato). + Ritorna, per asset: ts (ms, fine dell'ora), i1h, cumF (somma cumulata), e la data di switch.""" + out = {} + for a in ASSETS: + lin, inv = F[LINEAR[a]]["i1h"], F[INVERSE[a]]["i1h"] + sw = lin.index.min() + merged = pd.concat([inv[inv.index < sw], lin]).sort_index() + merged = merged[~merged.index.duplicated(keep="last")] + ts = to_ms(merged.index) + v = merged.values.astype(float) + out[a] = dict(ts=ts, i1h=v, cum=np.concatenate([[0.0], np.cumsum(v)]), + switch=sw, series=merged) + return out + + +EPOCH = pd.Timestamp("1970-01-01", tz="UTC") + + +def to_ms(idx) -> np.ndarray: + """Epoca in MILLISECONDI, esplicita. + + ⚠️ NON usare `.astype("int64")` / `.view("int64")` su un DatetimeIndex: in pandas 2.x la + risoluzione non e' piu' sempre ns e la scala esce SBAGLIATA in modo silenzioso. Misurato + qui, in questa sessione, su tre serie dello stesso progetto: il funding cachato ha unita' + `datetime64[ms]`, l'indice di `al.get()` ha `datetime64[us]`, quello del log del cron ha + `datetime64[s]`. Con `.astype("int64")//1e6` le prime due finiscono su scale che + differiscono di 1.000x -> `searchsorted` restituisce l'ESTREMO dell'array invece + dell'ora giusta, e il funding usciva 0.000%/anno su SKH01 (troppo piccolo) e + +1.229%/GIORNO su TP01 (troppo grande). Nessuna eccezione, nessun NaN: due numeri + plausibili in due punti diversi, e uno dei due sarebbe passato per buono. + (Stessa famiglia dell'errore gia' codificato in CLAUDE.md il 2026-07-01.) + """ + if not isinstance(idx, pd.DatetimeIndex): + idx = pd.DatetimeIndex(idx) + if idx.tz is None: + idx = idx.tz_localize("UTC") + return ((idx - EPOCH) // pd.Timedelta(milliseconds=1)).values.astype(np.int64) + + +def cumF(rate: dict, t_ms) -> np.ndarray: + """Funding cumulato fino all'istante t (record etichettato T = ora CHIUSA in (T-1h, T]).""" + j = np.searchsorted(rate["ts"], np.asarray(t_ms, dtype=np.int64), side="right") + return rate["cum"][j] + + +def daily_funding(rate: dict, days: pd.DatetimeIndex) -> np.ndarray: + """Funding pagato da un long unitario nel giorno d = somma di i1h su (d, d+24h].""" + t0 = to_ms(days) + return cumF(rate, t0 + 24 * MSH) - cumF(rate, t0) + + +def verifica_scala(rate: dict, F: dict) -> None: + """Controllo POSITIVO sulla scala dei tempi (senza, il bug qui sotto ripassa muto). + + Non basta che i numeri sembrino plausibili: `daily_funding` (searchsorted su epoca in ms) + si confronta con un `groupby` di pandas, che non fa nessuna conversione a intero. E si + verifica che il controllo abbia POTENZA, cioe' che fallisca se la scala e' sbagliata. + """ + print("\n [SCALA] controllo positivo su daily_funding vs groupby pandas:") + for a in ASSETS: + ser = rate[a]["series"] + g = ser.groupby((ser.index - pd.Timedelta(seconds=1)).floor("D")).sum() + g = g[(g.index >= ser.index.min().floor("D") + pd.Timedelta("1D")) & + (g.index <= ser.index.max().floor("D") - pd.Timedelta("1D"))] + gi = pd.DatetimeIndex(g.index) + mine = daily_funding(rate[a], gi) + dmax = float(np.max(np.abs(mine - g.values))) + assert dmax < 1e-15, f"scala tempi rotta su {a}: max|dif|={dmax:.3e}" + bad = dict(ts=rate[a]["ts"] * 1000, cum=rate[a]["cum"]) # scala x1000 (us al posto di ms) + t = to_ms(gi[:200]) + wrong = cumF(bad, t + 24 * MSH) - cumF(bad, t) + assert float(np.max(np.abs(wrong - mine[:200]))) > 1e-9, "il controllo NON ha potenza" + print(f" {a}: {len(g):,} giorni confrontati, max|dif| = {dmax:.3e} OK " + f"(e fallisce se la scala e' x1000 -> ha potenza)") + lo = min(rate[a]["series"].index.min() for a in ASSETS) + print(" [COPERTURA] il libro parte dal 2019-03-14, ma `get_funding_rate_history` serve una") + print(f" FINESTRA MOBILE di ~7,3 anni: oggi il piu' vecchio dato e' del {lo.date()}") + print(" (i perpetual INVERSE esistono dal 2018-08/2019-03: la storia c'era, il venue") + print(" non la serve piu'). -> i primi ~47 giorni del backtest sono senza funding") + print(" modellato = 1,7% del campione. Dichiarato, non stimato.") + + +# =========================================================================== +# 1) REPLICHE (funding spento => bit-exact) e 3) LE SERIE COL FUNDING +# =========================================================================== +def tp01_canonical(rate: dict | None) -> tuple[pd.Series, dict]: + """TP01 canonico 50/50 (la lente di `al.tp01_baseline_daily`), con funding opzionale.""" + tp = TrendPortfolio(**CANONICAL) + series, diag = {}, {} + for a in ASSETS: + df = al.get(a, "1d") + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + net, _ = tp.net_returns(df) + net = np.asarray(net, float).copy() + tgt = np.asarray(tp.target_series(df), float) + pos = np.zeros(len(tgt)); pos[1:] = tgt[:-1] # tenuta durante la barra i + if rate is not None: + f = daily_funding(rate[a], idx) + net = np.clip(net - pos * f, -0.99, None) + net[0] = 0.0 + diag[a] = dict(pos=pos, f=f, idx=idx) + series[a] = pd.Series(net, index=idx) + J = pd.concat(series, axis=1, join="inner").fillna(0.0) + return al._to_daily(0.5 * J["BTC"] + 0.5 * J["ETH"]), diag + + +def tp01_realistic(alloc: float, rate: dict | None) -> tuple[pd.Series, dict]: + """TP01 con min-order $5 (la lente di `CC.tp01_realistic`), con funding opzionale.""" + cap_asset = alloc / len(ASSETS) + tp = TrendPortfolio(**CANONICAL) + series, diag = {}, {} + for a in ASSETS: + df = resample_1d(load_data(a, "1h")) + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + r = simple_returns(df["close"].values.astype(float)) + tgt = np.nan_to_num(np.asarray(tp.target_series(df), float)) + held = np.empty(len(tgt)); cur = 0.0 + for i in range(len(tgt)): + if abs(tgt[i] - cur) * cap_asset >= CC.DERIBIT_MIN_ORDER: + cur = tgt[i] + held[i] = cur + pos = np.zeros(len(held)); pos[1:] = held[:-1] + net = pos * r - CC.DERIBIT_FEE_SIDE * np.abs(np.diff(pos, prepend=0.0)) + fund = np.zeros(len(pos)) + if rate is not None: + fund = pos * daily_funding(rate[a], idx) + net = net - fund + net[0] = 0.0 + diag[a] = dict(pos=pos, fund=fund, idx=idx) + series[a] = pd.Series(np.clip(net, -0.99, None), index=idx) + J = pd.concat(series, axis=1, join="inner").fillna(0.0) + out = pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index) + if out.index.tz is None: + out.index = out.index.tz_localize("UTC") + return out, diag + + +def sim_equity_f(ltf: pd.DataFrame, ent: list, mode: str, ts5, c5, + rate: dict | None) -> tuple[np.ndarray, list]: + """Replica di `r02.sim_equity` con UN grado di liberta' in piu': il funding accumulato + fra ingresso e uscita, col SEGNO della posizione (uno short lo INCASSA). + Con rate=None deve dare bit-exact `r02.sim_equity` — e' la replica di controllo.""" + c = ltf["close"].values.astype(float) + h = ltf["high"].values.astype(float) + l = ltf["low"].values.astype(float) + n = len(c) + close_ts = ltf["timestamp"].values.astype(np.int64) + r02.MS_LTF + + def px_hour(t: int) -> float: + hb = ((t + MSH - 1) // MSH) * MSH + j = np.searchsorted(ts5, hb, side="left") + return c5[min(j, len(c5) - 1)] + + def t_hour(t: int) -> int: + return int(((t + MSH - 1) // MSH) * MSH) + + initial = 1000.0 + capital = initial + equity = np.full(n, capital, dtype=float) + busy_until = -1 + trades = [] + for i in range(n): + e = ent[i] if i < len(ent) else None + if e is None or e.get("dir", 0) == 0: + equity[i] = capital + continue + if i <= busy_until: + equity[i] = capital + continue + direction = int(e["dir"]) + tp = e.get("tp"); sl = e.get("sl") + max_bars = int(e.get("max_bars") or 24) + entry = c[i] if mode in ("canonical", "barclose") else px_hour(close_ts[i]) + exit_idx = min(i + max_bars, n - 1) + exit_lvl = c[exit_idx] + for j in range(i + 1, min(i + max_bars + 1, n)): + hit_sl = sl is not None and ( + (direction == 1 and l[j] <= sl) or (direction == -1 and h[j] >= sl)) + hit_tp = tp is not None and ( + (direction == 1 and h[j] >= tp) or (direction == -1 and l[j] <= tp)) + if hit_sl: + exit_lvl, exit_idx = sl, j + break + if hit_tp: + exit_lvl, exit_idx = tp, j + break + exit_lvl, exit_idx = c[j], j + if mode == "canonical": + exit_price = exit_lvl + elif mode == "barclose": + exit_price = c[exit_idx] + else: + exit_price = px_hour(close_ts[exit_idx]) + gross = (exit_price - entry) / entry * direction + net = gross - 0.001 + f_trade = 0.0 + if rate is not None: + t_in = close_ts[i] if mode in ("canonical", "barclose") else t_hour(close_ts[i]) + t_out = close_ts[exit_idx] if mode in ("canonical", "barclose") else t_hour(close_ts[exit_idx]) + f_trade = float(cumF(rate, t_out) - cumF(rate, t_in)) * direction + net = net - f_trade + trades.append(dict(i=i, exit_idx=exit_idx, dir=direction, gross=gross, + f=f_trade, hours=(close_ts[exit_idx] - close_ts[i]) / MSH, + t_in=close_ts[i], t_out=close_ts[exit_idx])) + capital += capital * net + capital = max(capital, 1.0) + equity[i:exit_idx + 1] = capital + busy_until = exit_idx + last = initial + for k in range(n): + if equity[k] != last and equity[k] != initial: + last = equity[k] + else: + equity[k] = last + return equity, trades + + +_SKH: dict = {} + + +def skh_daily(a: str, off: int, mode: str, rate: dict | None) -> tuple[pd.Series, list]: + key = (a, off, mode, rate is not None) + if key in _SKH: + return _SKH[key] + _, m0, ltf, ent = r02.run_asset(a, off) + df5 = r02.get5m(a) + ts5 = df5["timestamp"].values.astype(np.int64) + r02.MS5 + c5 = df5["close"].values.astype(float) + eq, tr = sim_equity_f(ltf, ent, mode, ts5, c5, rate[a] if rate else None) + s = pd.Series(eq, index=pd.DatetimeIndex(pd.to_datetime(ltf["datetime"], utc=True))) + d = s.resample("1D").last().ffill().pct_change().dropna() + _SKH[key] = (d, tr) + return _SKH[key] + + +def skh_book(off: int, mode: str, rate: dict | None) -> pd.Series: + J = pd.concat({a: skh_daily(a, off, mode, rate)[0] for a in ASSETS}, + axis=1, join="inner").fillna(0.0) + return pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index) + + +def book_75_25(rate: dict | None, off: int = 0, lens: str = "hourly", + alloc: float = ALLOC) -> pd.Series: + """Il libro LIVE: TP01 75 + SKH01 25, esattamente la lente di CC.book_series.""" + skh = skh_book(off, lens, rate) + if skh.index.tz is None: + skh.index = skh.index.tz_localize("UTC") + tp, _ = tp01_realistic(alloc * 0.75, rate) + return combine_outer({"TP01": tp, "SKH01": skh}, {"TP01": 0.75, "SKH01": 0.25}, + lo=pd.Timestamp("2019-03-14", tz="UTC")) + + +def sezione1(rate: dict) -> None: + sez("1) REPLICA DI CONTROLLO — con funding SPENTO ogni replica dev'essere BIT-EXACT") + ok = True + + a0, _ = tp01_canonical(None) + b0 = al.tp01_baseline_daily() + d = float(np.max(np.abs(a0.values - b0.reindex(a0.index).values))) + ok &= d == 0.0 + print(f" tp01_canonical(None) vs al.tp01_baseline_daily max|dif| = {d:.3e} " + f"{'OK' if d == 0 else 'FALLITA'}") + + a1, _ = tp01_realistic(ALLOC * 0.75, None) + b1 = CC.tp01_realistic(ALLOC * 0.75) + d = float(np.max(np.abs(a1.values - b1.reindex(a1.index).values))) + ok &= d == 0.0 + print(f" tp01_realistic(450,None) vs CC.tp01_realistic(450) max|dif| = {d:.3e} " + f"{'OK' if d == 0 else 'FALLITA'}") + + for mode in ("canonical", "hourly"): + for a in ASSETS: + _, m0, ltf, ent = r02.run_asset(a, 0) + df5 = r02.get5m(a) + ts5 = df5["timestamp"].values.astype(np.int64) + r02.MS5 + c5 = df5["close"].values.astype(float) + eqA, _ = sim_equity_f(ltf, ent, mode, ts5, c5, None) + eqB = r02.sim_equity(ltf, ent, mode, ts5_close=ts5, c5=c5) + d = float(np.max(np.abs(eqA - eqB))) + ok &= d == 0.0 + print(f" sim_equity_f({a},{mode:<9}) vs r02.sim_equity " + f"max|dif| = {d:.3e} {'OK' if d == 0 else 'FALLITA'}") + + a2 = book_75_25(None) + b2 = CC.book_series("hourly") + d = float(np.max(np.abs(a2.values - b2.reindex(a2.index).values))) + ok &= d == 0.0 + print(f" book_75_25(None) vs CC.book_series('hourly') max|dif| = {d:.3e} " + f"{'OK' if d == 0 else 'FALLITA'}") + + if not ok: + raise SystemExit("REPLICA FALLITA: nessun delta e' pubblicabile.") + print("\n ✅ 5/5 repliche bit-exact. Da qui in poi l'UNICO grado di liberta' e' il funding.") + + +# =========================================================================== +# 2) IL SEGNO — quanto si e' esposti, e da che lato +# =========================================================================== +def sezione2(rate: dict) -> dict: + sez("2) IL SEGNO — l'esposizione non e' assunta, e' appaiata col funding del suo strumento") + out = {} + + _, dg = tp01_realistic(ALLOC * 0.75, rate) + print("\n TP01 (long-flat, posizione continua vol-targeted; esposizione = frazione del") + print(" capitale della GAMBA; nel libro pesa 0.75 x 0.5 per asset = 0.375 x posizione)") + print(f" {'asset':>6} {'giorni':>7} {'%tempo>0':>9} {'pos media':>10} {'pos med|>0':>11} " + f"{'funding pagato/anno':>20} {'esposiz. x tasso':>17}") + tot_leg = [] + for a in ASSETS: + pos, f, idx = dg[a]["pos"], dg[a]["fund"], dg[a]["idx"] + yrs = len(pos) / 365.25 + drag = float(f.sum()) / yrs # in frazione del capitale-gamba, per anno + rt = rate[a]["series"] + rt = rt[(rt.index >= idx.min()) & (rt.index <= idx.max())] + print(f" {a:>6} {len(pos):>7,} {float((pos > 0).mean()):>9.1%} {float(pos.mean()):>10.3f} " + f"{float(pos[pos > 0].mean()):>11.3f} {drag:>19.3%} " + f"{float(pos.mean()) * float(rt.mean()) * 24 * 365.25:>16.3%}") + tot_leg.append(drag) + out["tp01_leg_drag"] = float(np.mean(tot_leg)) + print(f" -> drag medio per gamba {out['tp01_leg_drag']:.3%}/anno = sullo SLEEVE " + f"{out['tp01_leg_drag']:.3%}/anno (le due gambe pesano 0.5 ciascuna)") + + print("\n SKH01 (long/short, round-trip discreti, non-overlap: sta a mercato una frazione") + print(" del tempo — e uno SHORT il funding lo INCASSA)") + print(f" {'asset':>6} {'trade':>6} {'%long':>7} {'%tempo a mercato':>17} {'ore/trade':>10} " + f"{'pagato/anno':>12} {'incassato/anno':>15} {'NETTO/anno':>11}") + tot_skh = [] + for a in ASSETS: + d, tr = skh_daily(a, 0, "hourly", rate) + span_h = (tr[-1]["t_out"] - tr[0]["t_in"]) / MSH + yrs = span_h / (24 * 365.25) + f = np.array([t["f"] for t in tr]) + dirs = np.array([t["dir"] for t in tr]) + hrs = np.array([t["hours"] for t in tr]) + inmkt = hrs.sum() / span_h + paid = float(f[f > 0].sum()) / yrs + got = float(-f[f < 0].sum()) / yrs + net = float(f.sum()) / yrs + tot_skh.append(net) + print(f" {a:>6} {len(tr):>6} {float((dirs > 0).mean()):>7.1%} {inmkt:>17.1%} " + f"{float(hrs.mean()):>10.1f} {paid:>11.3%} {got:>14.3%} {net:>10.3%}") + out["skh_drag"] = float(np.mean(tot_skh)) + print(f" -> drag NETTO dello sleeve SKH01 {out['skh_drag']:.3%}/anno") + + print("\n ❌ IPOTESI DEL BRIEF FALSIFICATA: «SKH01 sta a mercato solo il 12% del tempo ->") + print(" paga MOLTO MENO funding di TP01, ed e' un argomento a suo favore mai contato».") + print(f" Misurato: SKH01 {out['skh_drag']:.2%}/anno contro TP01 " + f"{out['tp01_leg_drag']:.2%}/anno di sleeve — SKH01 paga di PIU', non di meno.") + print(" Due ragioni, entrambe misurabili: (i) quando e' a mercato sta a nozionale 1.0x,") + print(" mentre TP01 vol-targeted sta a 0.14x in media -> a parita' di tempo pesa 7x;") + print(" (ii) entra sui breakout, e i breakout sono proprio quando il funding e' caro.") + + print("\n 📌 IL MECCANISMO, ed e' il risultato trasferibile: ESPOSIZIONE e FUNDING sono") + print(" POSITIVAMENTE CORRELATI. Tasso medio annualizzato, non condizionato vs") + print(" condizionato all'essere in posizione:") + print(f" {'':>6} {'incondizionato':>15} {'| TP01 long':>12} {'| SKH01 a mercato':>18} " + f"{'rapporto TP01':>14} {'rapporto SKH':>13}") + for a in ASSETS: + ser = rate[a]["series"] + unc = float(ser.mean()) * 24 * 365.25 + # TP01: media del tasso GIORNALIERO pesata dalla posizione (= tasso "che si paga davvero") + pos, f, idx = dg[a]["pos"], dg[a]["fund"], dg[a]["idx"] + fd = daily_funding(rate[a], idx) + w = pos / pos.sum() if pos.sum() > 0 else pos + tp_cond = float(np.sum(w * fd)) * 365.25 + # SKH01: media del tasso orario sulle ore in cui e' a mercato (segno a parte) + _, tr = skh_daily(a, 0, "hourly", rate) + ore = float(np.sum([abs(t["hours"]) for t in tr])) + lordo = float(np.sum([abs(cumF(rate[a], t["t_out"]) - cumF(rate[a], t["t_in"])) + for t in tr])) + skh_cond = lordo / ore * 24 * 365.25 if ore else float("nan") + print(f" {a:>6} {unc:>15.2%} {tp_cond:>12.2%} {skh_cond:>18.2%} " + f"{tp_cond/unc:>13.2f}x {skh_cond/unc:>12.2f}x") + print(" -> il prodotto ingenuo «esposizione media x tasso medio» SOTTOSTIMA di ~2x. E' la") + print(" forma esatta della stima grezza da cui e' nato questo filone (14,8% x 0,103 =") + print(" 1,53%/anno): giusta come ordine di grandezza, sbagliata nei due fattori.") + print(" ⚠️ Il rapporto e' su TUTTO il campione e mescola due cose vere ma diverse: la") + print(" correlazione DENTRO l'anno (trend long = perp in contango) e il fatto che i") + print(" due anni di funding piu' caro (2019, 2021) sono anche quelli in cui TP01 era") + print(" piu' long. Per il COSTO non cambia nulla — si paga lo stesso — ma per") + print(" PREVEDERLO si', e la previsione non e' questo numero.") + + print(f"\n 📌 A livello di LIBRO 75/25: 0.75 x {out['tp01_leg_drag']:.3%} + " + f"0.25 x {out['skh_drag']:.3%} = " + f"{0.75 * out['tp01_leg_drag'] + 0.25 * out['skh_drag']:.3%}/anno di costo lordo") + print(" (e' una scomposizione, non il risultato: il numero che conta e' il DELTA di") + print(" drift misurato sulla serie composta, sezione 3.)") + out["book_drag_decomp"] = 0.75 * out["tp01_leg_drag"] + 0.25 * out["skh_drag"] + return out + + +# =========================================================================== +# 3) I NUMERI +# =========================================================================== +def stat(s: pd.Series) -> dict: + m = metrics(s) + r = np.asarray(s.dropna().values, float) + return dict(full=al._sh(s), is_=al._sh(s[s.index < HOLDOUT]), hold=al._sh(s[s.index >= HOLDOUT]), + dd=al._dd_ret(s), cagr=m["cagr"], drift=float(r.mean()) * 365.25, + vol=float(r.std()) * np.sqrt(365.25), n=len(r)) + + +def riga(nome: str, a: dict, b: dict) -> None: + print(f" {nome:<34} {a['full']:>7.3f} {a['is_']:>7.3f} {a['hold']:>7.3f} {a['dd']:>7.1%} " + f"{a['cagr']:>7.2%} {a['drift']:>7.2%}") + print(f" {' + funding':<34} {b['full']:>7.3f} {b['is_']:>7.3f} {b['hold']:>7.3f} " + f"{b['dd']:>7.1%} {b['cagr']:>7.2%} {b['drift']:>7.2%}") + print(f" {' DELTA':<34} {b['full']-a['full']:>+7.3f} {b['is_']-a['is_']:>+7.3f} " + f"{b['hold']-a['hold']:>+7.3f} {b['dd']-a['dd']:>+7.1%} {b['cagr']-a['cagr']:>+7.2%} " + f"{b['drift']-a['drift']:>+7.2%}\n") + + +def sezione3(rate: dict) -> dict: + sez("3) I NUMERI COL FUNDING DENTRO — netti fee 0.10% RT, hold-out 2025+") + print(f" {'serie':<34} {'ShFULL':>7} {'ShIS':>7} {'ShHOLD':>7} {'maxDD':>7} " + f"{'CAGR':>7} {'drift':>7}") + out = {} + + p = {} + p["TP01 canonico (soffitto)"] = (tp01_canonical(None)[0], tp01_canonical(rate)[0]) + p[f"TP01 realistic ${ALLOC*0.75:.0f}"] = (tp01_realistic(ALLOC * 0.75, None)[0], + tp01_realistic(ALLOC * 0.75, rate)[0]) + p["SKH01 50/50 canonical"] = (skh_book(0, "canonical", None), skh_book(0, "canonical", rate)) + p["SKH01 50/50 path live"] = (skh_book(0, "hourly", None), skh_book(0, "hourly", rate)) + p["LIBRO 75/25 (path live)"] = (book_75_25(None), book_75_25(rate)) + for k, (A, B) in p.items(): + sa, sb = stat(A), stat(B) + riga(k, sa, sb) + out[k] = (sa, sb) + + print(" Per ANNO, libro 75/25 (drift lordo -> netto funding). La colonna ESPOSIZIONE serve") + print(" a non confondere 'il funding e' sceso' con 'il libro era flat': sono cose diverse e") + print(" producono lo stesso delta piccolo.") + A, B = p["LIBRO 75/25 (path live)"] + _, dgx = tp01_realistic(ALLOC * 0.75, rate) + expo = pd.concat({a: pd.Series(dgx[a]["pos"], index=dgx[a]["idx"]) for a in ASSETS}, + axis=1, join="inner").mean(axis=1) + print(f" {'anno':>6} {'senza':>9} {'con':>9} {'delta':>9} {'funding annualiz.':>18} " + f"{'esposiz. TP01':>14}") + for y in sorted(set(A.index.year)): + ga, gb = A[A.index.year == y], B[B.index.year == y] + if len(ga) < 30: + continue + da, db = float(ga.mean()) * 365.25, float(gb.mean()) * 365.25 + fr = np.mean([rate[a]["series"][rate[a]["series"].index.year == y].mean() + for a in ASSETS]) * 24 * 365.25 + ex = float(expo[expo.index.year == y].mean()) + print(f" {y:>6} {da:>9.2%} {db:>9.2%} {db-da:>+9.2%} {fr:>17.2%} {ex:>14.3f}") + rec = pd.Timestamp("2025-01-01", tz="UTC") + dA, dB = float(A[A.index >= rec].mean()) * 365.25, float(B[B.index >= rec].mean()) * 365.25 + print(f" {'2025+':>6} {dA:>9.2%} {dB:>9.2%} {dB-dA:>+9.2%} " + f"{np.mean([rate[a]['series'][rate[a]['series'].index >= rec].mean() for a in ASSETS])*24*365.25:>17.2%} " + f"{float(expo[expo.index >= rec].mean()):>14.3f}") + print(" 📌 IL COSTO NON E' STAZIONARIO, e le due cause si LEGGONO nelle due colonne: il") + print(" funding Deribit si e' compresso (16,9% nel 2021 -> 1,9% nel 2026) E il libro sta") + print(" meno esposto. Chi cita UN numero per il funding cita una media su un regime") + print(" cambiato; chi cita il numero del 2026 sta citando un libro quasi flat.") + + # finestra SOLO-LINEARE (nessun proxy): il controllo che il numero non venga dal proxy + lo = rate["BTC"]["switch"] + print(f"\n CONTROLLO — stessa misura sulla sola finestra LINEARE (>= {lo.date()},") + print(" nessun proxy inverse):") + print(f" {'serie':<34} {'d ShFULL':>9} {'d CAGR':>9} {'d drift':>9}") + for k, (A, B) in p.items(): + A2, B2 = A[A.index >= lo], B[B.index >= lo] + sa, sb = stat(A2), stat(B2) + print(f" {k:<34} {sb['full']-sa['full']:>+9.3f} {sb['cagr']-sa['cagr']:>+9.2%} " + f"{sb['drift']-sa['drift']:>+9.2%}") + out["_pairs"] = p + return out + + +# =========================================================================== +# 4) LE DECISIONI +# =========================================================================== +def sezione4a(res3: dict) -> None: + sez("4a) IL SOFFITTO ~1.3 SI ABBASSA?") + a, b = res3["TP01 canonico (soffitto)"] + print(f" TP01 canonico (l'unica serie a cui il 'soffitto ~1.3' si riferisce):") + print(f" Sharpe FULL {a['full']:.3f} -> {b['full']:.3f} ({b['full']-a['full']:+.3f})") + print(f" CAGR {a['cagr']:.2%} -> {b['cagr']:.2%}") + print(f" Il soffitto e' un numero arrotondato a UNA cifra: si abbassa di " + f"{abs(b['full']-a['full']):.3f}, quindi resta '~1.3'" + if abs(b["full"] - a["full"]) < 0.05 else + f" Il soffitto si sposta di {b['full']-a['full']:+.3f}: va RIDETTO.") + print(" ⚠️ Ma la lettura utile e' l'altra: il soffitto e' una soglia di SELEZIONE, e ogni") + print(" candidato direzionale mai valutato in questo progetto e' stato misurato SENZA") + print(" funding. Il confronto candidato-vs-TP01 e' quindi appaiato (entrambi al lordo)") + print(" per i candidati LONG/SHORT a esposizione simile, e distorto a favore di chi sta") + print(" a mercato di piu'. Chi paga di piu' e' chi e' piu' esposto piu' a lungo.") + + +def sezione4b(rate: dict, offsets: tuple) -> None: + sez("4b) IL PESO 75/25 SI MUOVE? (il funding e' un argomento a favore di SKH01 mai contato)") + print(" L'ipotesi era: «SKH01 sta a mercato una frazione del tempo e puo' incassare da") + print(" short -> paga MENO funding di TP01, quindi il peso ottimale sale». La prima meta'") + print(" e' gia' FALSIFICATA in sez. 2 (SKH01 paga di piu' per dollaro di sleeve). Resta la") + print(" seconda, che non e' implicata dalla prima: il peso si sposta?") + print(f" Sweep w in {list(skl.W_GRID)} x {len(offsets)} offset della griglia 230m/690m.") + print(" Statistica = MEDIANA DELLE DIFFERENZE APPAIATE (regola del progetto), lente") + print(" 'hourly' = path live, selezione sullo Sharpe IN-SAMPLE come il 24/07.\n") + + B0, _ = tp01_canonical(None) + B1, _ = tp01_canonical(rate) + + def blend(B, skh, w): + if w == 0.0: + return B + bl = combine_outer({"TP": B, "SKH": skh}, {"TP": 1.0 - w, "SKH": w}) + return bl[bl.index >= B.index.min()] + + S = {False: {}, True: {}} + for off in offsets: + S[False][off] = skh_book(off, "hourly", None) + S[True][off] = skh_book(off, "hourly", rate) + + print(f" {'w':>6} | {'IS mediano':>11} {'IS med +f':>11} {'d IS':>8} {'>0':>5} | " + f"{'FULL med':>9} {'FULL +f':>9} {'d FULL':>8} | {'HOLD med':>9} {'HOLD +f':>9}") + rows = {} + for w in skl.W_GRID: + vI0, vI1, vF0, vF1, vH0, vH1 = [], [], [], [], [], [] + for off in offsets: + b0 = blend(B0, S[False][off], w) + b1 = blend(B1, S[True][off], w) + vI0.append(al._sh(b0[b0.index < HOLDOUT])); vI1.append(al._sh(b1[b1.index < HOLDOUT])) + vF0.append(al._sh(b0)); vF1.append(al._sh(b1)) + vH0.append(al._sh(b0[b0.index >= HOLDOUT])); vH1.append(al._sh(b1[b1.index >= HOLDOUT])) + dI = np.array(vI1) - np.array(vI0) + dF = np.array(vF1) - np.array(vF0) + rows[w] = dict(is0=np.median(vI0), is1=np.median(vI1), dI=np.median(dI), + f0=np.median(vF0), f1=np.median(vF1), dF=np.median(dF), + h0=np.median(vH0), h1=np.median(vH1), pos=float((dI > 0).mean())) + r_ = rows[w] + print(f" {w:>6.2f} | {r_['is0']:>11.3f} {r_['is1']:>11.3f} {r_['dI']:>+8.3f} " + f"{r_['pos']:>5.0%} | {r_['f0']:>9.3f} {r_['f1']:>9.3f} {r_['dF']:>+8.3f} | " + f"{r_['h0']:>9.3f} {r_['h1']:>9.3f}") + + w0 = max(skl.W_GRID, key=lambda w: rows[w]["is0"]) + w1 = max(skl.W_GRID, key=lambda w: rows[w]["is1"]) + print(f"\n argmax dello Sharpe IS mediano: SENZA funding w* = {w0:.2f} " + f"CON funding w* = {w1:.2f}") + print(f" peso live = 0.25. Regola a priori del 24/07: a parita' (<{skl.TIE_TOL}) si sta al") + print(f" piu' vicino a 0.25. |IS(w*) - IS(0.25)| con funding = " + f"{abs(rows[w1]['is1'] - rows[0.25]['is1']):.3f}") + if w0 == w1: + print(" 📌 Il funding NON sposta l'argmax: il vantaggio di SKH01 e' reale ma piccolo") + print(" rispetto alla larghezza del plateau. Peso INVARIATO.") + else: + print(f" 📌 L'argmax si sposta {w0:.2f} -> {w1:.2f}: da verificare con weights_tilt_null.") + + +def sezione4c(rate: dict) -> dict: + sez("4c) I MURI DI CAPITALE E LA RENDITA PERPETUA (vivono sul DRIFT, non sullo Sharpe)") + A, B = book_75_25(None), book_75_25(rate) + need_gross = CC.TARGET_EUR_DAY * 365.0 * EURUSD / (1 - CC.TAX_RATE) + print(f" bersaglio: 50 EUR/g netti = ${need_gross:,.0f}/anno lordi (aliquota " + f"{CC.TAX_RATE:.0%}), stessa convenzione di r0726_capwall_refresh.") + print(f" de-luck del drift x{DELUCK} (MISURATO, r0726_deluck_factor). MC {N_PATHS_WALL} path") + print(" -> risoluzione ~1% sul muro: si citano 3 cifre significative, non il dollaro.\n") + print(f" {'serie':<28} {'drift':>8} {'vol':>7} {'perpetua':>9} {'muro':>12} {'leva':>5}") + out = {} + for nome, s in (("senza funding", A), ("con funding", B)): + r = s.values.astype(float) + r_dl = r - (1.0 - DELUCK) * float(r.mean()) + for k in (1.0, 1.5): + _, perp, cap = WR.perp_and_wall(r_dl, k, need_gross) + out[(nome, k)] = (perp, cap) + print(f" {nome + f' x{DELUCK}':<28} {float(r_dl.mean())*365.25:>8.2%} " + f"{float(r_dl.std())*np.sqrt(365.25):>7.2%} {perp:>9.2%} ${cap:>11,.0f} {k:>5.2f}") + p0, c0 = out[("senza funding", 1.0)] + p1, c1 = out[("con funding", 1.0)] + print(f"\n 📌 A leva 1.0: perpetua {p0:.2%} -> {p1:.2%}; muro ${c0:,.0f} -> ${c1:,.0f} " + f"({(c1/c0-1):+.1%})") + print(f" Il muro pubblicato e' $272k (lordo) / $258k (netto fisco). Qui si misura il") + print(f" DELTA con la stessa macchineria, non un muro nuovo.") + + print("\n TRAIETTORIA da $600 al capitale-rendita (mediana anni / P entro 20a), stesso") + print(" bootstrap a blocchi di r0726_capwall_refresh.trajectory:") + print(f" {'EUR/mese':>9} | {'senza funding':>22} | {'con funding':>22}") + r0 = A.values.astype(float); r0 = r0 - (1 - DELUCK) * r0.mean() + r1 = B.values.astype(float); r1 = r1 - (1 - DELUCK) * r1.mean() + t0 = WR.trajectory(r0, c0, n_paths=2000) + t1 = WR.trajectory(r1, c1, n_paths=2000) + for dep in (0, 250, 500, 1_000, 2_000): + a_, b_ = t0[dep], t1[dep] + fa = f"{a_['p50']:>6.1f}a P20 {a_['p20y']:>5.0%}" if a_["p50"] == a_["p50"] else f"{'mai':>16}" + fb = f"{b_['p50']:>6.1f}a P20 {b_['p20y']:>5.0%}" if b_["p50"] == b_["p50"] else f"{'mai':>16}" + print(f" {dep:>9} | {fa:>22} | {fb:>22}") + return out + + +def g_exact(r: np.ndarray, k: float) -> float: + x = 1.0 + k * r + return float(np.mean(np.log(np.maximum(x, 1e-12)))) * 365.25 + + +def k_star(r: np.ndarray, kmax: float = 40.0, step: float = 0.05) -> tuple[float, float]: + ks = np.arange(step, kmax + step, step) + g = np.array([g_exact(r, k) for k in ks]) + j = int(np.argmax(g)) + return float(ks[j]), float(g[j]) + + +def sezione4d(rate: dict) -> None: + sez("4d) k* E IL GRADINO DI LEVA — il funding SCALA con la leva") + print(" Il funding e' proporzionale al NOZIONALE: a leva k si paga k volte tanto. Quindi") + print(" la serie col funding va scalata per k come tutto il resto (il costo non e' fisso),") + print(" e il gradino 1.00x -> 1.25/1.50x misurato ieri ne mangia una parte.\n") + A, B = book_75_25(None), book_75_25(rate) + print(f" {'serie':<22} {'drift':>8} {'k*':>7} {'g(k*)':>8} {'g(1.00)':>9} {'g(1.25)':>9} " + f"{'g(1.50)':>9} {'mezzo-Kelly':>12}") + for nome, s in (("senza funding", A), ("con funding", B)): + r = s.values.astype(float) + ks, gs = k_star(r) + print(f" {nome:<22} {float(r.mean())*365.25:>8.2%} {ks:>7.2f} {gs:>8.2%} " + f"{g_exact(r, 1.0):>9.2%} {g_exact(r, 1.25):>9.2%} {g_exact(r, 1.50):>9.2%} " + f"{ks/2:>12.2f}") + r_dl = r - (1 - DELUCK) * r.mean() + ks2, gs2 = k_star(r_dl) + print(f" {' (de-luck x0.89)':<22} {float(r_dl.mean())*365.25:>8.2%} {ks2:>7.2f} " + f"{gs2:>8.2%} {g_exact(r_dl, 1.0):>9.2%} {g_exact(r_dl, 1.25):>9.2%} " + f"{g_exact(r_dl, 1.50):>9.2%} {ks2/2:>12.2f}") + rA = A.values.astype(float); rA -= (1 - DELUCK) * rA.mean() + rB = B.values.astype(float); rB -= (1 - DELUCK) * rB.mean() + gain0 = g_exact(rA, 1.25) - g_exact(rA, 1.0) + gain1 = g_exact(rB, 1.25) - g_exact(rB, 1.0) + print(f"\n 📌 Guadagno in log-crescita del gradino 1.00 -> 1.25 (de-luckato):") + print(f" senza funding {gain0:+.2%}/anno · con funding {gain1:+.2%}/anno " + f"(-{(1-gain1/gain0)*100:.0f}% del guadagno)" if gain0 > 0 else "") + print(" Il gradino resta positivo finche' k* > 1.25 con il funding dentro.") + + +# =========================================================================== +# 5) CONTROLLO SUL CONTO VERO +# =========================================================================== +RE_HDR = re.compile(r"^===== (\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z) cron_book") +RE_POS = re.compile(r"^\s+(BTC|ETH) TP .*?\|\s*pos \$([+-][\d,\.]+)") + + +def leggi_posizioni_live() -> pd.DataFrame: + """Posizione oraria REALE per asset, ricostruita da logs/cron_book.log.""" + rows, ts = [], None + for ln in (ROOT / "logs" / "cron_book.log").read_text(errors="replace").splitlines(): + m = RE_HDR.match(ln) + if m: + ts = pd.Timestamp(m.group(1)) + continue + m = RE_POS.match(ln) + if m and ts is not None: + rows.append(dict(ts=ts, asset=m.group(1), + pos=float(m.group(2).replace(",", "").replace("+", "")))) + return pd.DataFrame(rows) + + +RE_ORD = re.compile(r"^\s+-> (BUY|SELL) ") +RE_LEG = re.compile(r"^\s+(BTC|ETH) TP ") + + +def ordini_sessione(settl: pd.Timestamp) -> dict: + """Ordini REALMENTE inviati per asset dalla data `settl` (righe '-> BUY/SELL' del log, + che seguono la riga della gamba a cui appartengono).""" + out: dict = {} + ts, leg = None, None + for ln in (ROOT / "logs" / "cron_book.log").read_text(errors="replace").splitlines(): + m = RE_HDR.match(ln) + if m: + ts = pd.Timestamp(m.group(1)); leg = None + continue + m = RE_LEG.match(ln) + if m: + leg = m.group(1) + continue + m = RE_ORD.match(ln) + if m and leg and ts is not None and ts >= settl: + b, s_ = out.get(leg, (0, 0)) + out[leg] = (b + (m.group(1) == "BUY"), s_ + (m.group(1) == "SELL")) + return out + + +def sezione5(rate: dict) -> None: + sez("5) CONTROLLO SUL CONTO VERO — l'unico che non passa da un'assunzione") + df = leggi_posizioni_live() + if not len(df): + print(" logs/cron_book.log non leggibile o formato cambiato -> NON MISURATO.") + return + print(f" {len(df):,} osservazioni orarie di posizione ricostruite dal log del cron,") + print(f" {df['ts'].min()} -> {df['ts'].max()}") + print(" ⚠️ `data/live/book_executions.jsonl` ha `ts_utc` a 00:00:00 su 18/18 righe (difetto") + print(" gia' noto, ondata 22/08 §5): l'ora vera della posizione sta SOLO nel log del cron.") + + def paid(g: pd.DataFrame, a: str) -> float: + t = to_ms(pd.DatetimeIndex(g["ts"])) + return float((g["pos"].values * (cumF(rate[a], t + MSH) - cumF(rate[a], t))).sum()) + + tot = {} + for a in ASSETS: + g = df[df["asset"] == a].sort_values("ts") + tot[a] = paid(g, a) + print(f" {a}: {len(g):>4} ore, posizione media ${g['pos'].mean():>7.2f} " + f"(a mercato {float((g['pos'].values != 0).mean()):.0%}, max ${g['pos'].max():.0f})" + f" -> funding modellato ${tot[a]:+.4f}") + print(f" TOTALE funding modellato da quando il libro esegue: ${sum(tot.values()):+.4f}") + print(" (a $635 di conto e posizioni da $40-190: e' un numero da CENTESIMI. La potenza del") + print(" controllo non viene dalla taglia, viene dal fatto che sia CONFRONTABILE.)") + + print("\n Cio' che il conto RIPORTA (sola lettura via cerbero-mcp, nessun ordine):") + try: + from src.live.deribit import DeribitRead + ps = DeribitRead().positions("USDC") + except Exception as e: + print(f" conto non leggibile ({type(e).__name__}) -> confronto NON MISURATO.") + return + rp = {} + for p_ in ps: + inst = str(p_.get("instrument", "")) + a = "BTC" if inst.startswith("BTC") else "ETH" + rp[a] = float(p_.get("realized_pnl", 0.0)) + print(f" {inst:<22} size ${float(p_.get('size', 0)):>8.2f} " + f"realized_pnl {rp[a]:>+9.6f} unrealized {float(p_.get('unrealized_pnl', 0)):>+9.6f}") + + print("\n QUALE finestra? Deribit REGOLA i perpetual una volta al giorno alle 08:00 UTC.") + print(" Se `realized_pnl` e' il realizzato di SESSIONE, dev'essere il funding dall'ultimo") + print(" settlement — e allora la finestra giusta si riconosce perche' le altre NON tornano:") + now = pd.Timestamp.now(tz="UTC") + settl = now.normalize() + pd.Timedelta(hours=8) + if settl > now: + settl -= pd.Timedelta("1D") + fin = {a: df[df["asset"] == a].sort_values("ts") for a in ASSETS} + apert = {} + for a in ASSETS: + g = fin[a].reset_index(drop=True) + z = np.where(g["pos"].values == 0)[0] + apert[a] = g["ts"].iloc[int(z[-1])] if len(z) else g["ts"].iloc[0] + print(f" {'finestra':<34} {'BTC modello':>13} {'BTC conto':>11} {'err':>8} | " + f"{'ETH modello':>13} {'ETH conto':>11} {'err':>8}") + for lbl, lo in ((f"dal settlement {settl:%m-%d %H:%M} UTC", {a: settl for a in ASSETS}), + ("dall'apertura della posizione", apert), + ("ultime 24h", {a: now - pd.Timedelta("24h") for a in ASSETS}), + ("da quando il libro esegue", {a: fin[a]["ts"].min() for a in ASSETS})): + cells = [] + for a in ASSETS: + g = fin[a][pd.DatetimeIndex(fin[a]["ts"]) >= lo[a]] + m = paid(g, a) + err = (m / abs(rp[a]) - 1) if rp.get(a) else float("nan") + cells.append(f"{m:>13.6f} {rp.get(a, float('nan')):>11.6f} {err:>+7.1%}") + print(f" {lbl:<34} {cells[0]} | {cells[1]}") + + # (1) va MISURATO sugli ORDINI VERI, non sulle variazioni del nozionale. + # ⚠️ ERRORE CATTURATO IN SESSIONE: la prima stesura contava `diff(pos) < 0` come + # "vendite" e ne trovava 3 per asset. NON erano vendite: `pos` e' il valore in USD di + # una posizione a taglia costante, e scende da solo quando il mark scende. Su un + # perpetual lineare il nozionale si muove SEMPRE, anche a mano ferma. + ordini = ordini_sessione(settl) + print("\n 📌 IDENTIFICAZIONE, e non e' un accordo di grandezza — e' un'ELIMINAZIONE:") + print(f" (1) ordini realmente inviati nella sessione (righe '-> BUY/SELL' del log, " + f"dalle {settl:%H:%M} UTC): " + + " · ".join(f"{a} {ordini.get(a, (0, 0))[0]} buy / {ordini.get(a, (0, 0))[1]} sell" + for a in ASSETS)) + if sum(v[1] for v in ordini.values()) == 0: + print(" ZERO vendite -> nessuna chiusura, nemmeno parziale -> il P&L di TRADING") + print(" realizzato di sessione e' ESATTAMENTE ZERO, e qualunque valore non nullo") + print(" di `realized_pnl` non puo' essere P&L di trading.") + else: + print(" ⚠️ Ci sono state VENDITE: parte di `realized_pnl` puo' essere P&L di") + print(" trading e l'eliminazione (1) NON vale in questa sessione. Restano (2)(3).") + print(" (2) il segno e' NEGATIVO su entrambe le gambe, che sono entrambe LONG: un long") + print(" in contango PAGA. E' il segno che il funding deve avere, non un caso.") + print(" (3) la finestra 'dal settlement' torna; 'dall'apertura' sbaglia di ~2-3x e") + print(" '24h' di ~15%. Il controllo ha POTENZA: le finestre sbagliate si vedono.") + print(" -> `realized_pnl` del proxy = realizzato di SESSIONE Deribit, che su una") + print(" posizione non chiusa E' il funding. Il modello di questo script e' quindi") + print(" VALIDATO contro il conto reale, non solo contro se stesso.") + print(" ⚠️ IL RESIDUO, misurato invece che raccontato. Il modello SOTTOSTIMA su entrambe le") + print(" gambe. Espresso in ORE-equivalenti all'ultimo tasso orario osservato:") + for a in ASSETS: + g = fin[a][pd.DatetimeIndex(fin[a]["ts"]) >= settl] + res = abs(rp.get(a, 0.0)) - paid(g, a) + ult = float(rate[a]["series"].iloc[-1]) * float(g["pos"].iloc[-1]) + print(f" {a}: residuo ${res:+.6f} = {res/ult if ult else float('nan'):+.2f} ore " + f"di funding all'ultimo tasso (${ult:.6f}/ora)") + print(" Meccanismo candidato e coerente col segno su 2/2: il conto accumula in CONTINUO,") + print(" l'ultimo record orario e' delle 22:00 e la lettura e' delle 22:1x -> i minuti") + print(" dopo l'ultimo record non sono modellati. NON e' verificato che spieghi TUTTO il") + print(" residuo di BTC: si riporta la taglia, non una storia.") + print(" ⚠️ Limite: e' UNA sessione, su due posizioni. Non e' una serie storica di") + print(" pagamenti di funding — quella il conto non la espone via questo proxy.") + + +# =========================================================================== +# 6) COSA NON E' CATTURATO +# =========================================================================== +def sezione6(rate: dict) -> None: + sez("6) COSA NON E' CATTURATO — dichiarato e, dove si puo', quantificato") + + print(" (a) L'approssimazione 'esposizione MEDIA x tasso MEDIO' contro l'accrual ESATTO.") + print(" Su TP01 non c'e' differenza (posizione continua, accrual giornaliero esatto).") + print(" Su SKH01, che entra ed esce, la differenza si MISURA:") + print(f" {'asset':>6} {'accrual esatto/anno':>20} {'media x media/anno':>20} " + f"{'errore':>9} {'errore rel.':>12}") + for a in ASSETS: + _, tr = skh_daily(a, 0, "hourly", rate) + span_h = (tr[-1]["t_out"] - tr[0]["t_in"]) / MSH + yrs = span_h / (24 * 365.25) + esatto = float(np.sum([t["f"] for t in tr])) / yrs + # approssimazione: (ore a mercato x direzione media) x tasso medio del periodo + hrs = np.array([t["hours"] for t in tr]); dirs = np.array([t["dir"] for t in tr]) + rt = rate[a]["series"] + rt = rt[(rt.index >= pd.Timestamp(tr[0]["t_in"], unit="ms", tz="UTC")) & + (rt.index <= pd.Timestamp(tr[-1]["t_out"], unit="ms", tz="UTC"))] + approx = float((hrs * dirs).sum() * rt.mean()) / yrs + err = esatto - approx + print(f" {a:>6} {esatto:>19.4%} {approx:>19.4%} {err:>+8.4%} " + f"{(err/abs(esatto) if esatto else float('nan')):>11.1%}") + print(" -> l'approssimazione media-x-media NON e' innocua su uno sleeve che sceglie") + print(" QUANDO stare a mercato: e' la stessa forma dell'errore che il progetto ha") + print(" gia' pagato altrove (una media appaiata non e' la media delle medie).") + print(" Qui il numero pubblicato usa l'ACCRUAL ESATTO per trade.") + + sw = min(rate[a]["switch"] for a in ASSETS) + b0 = pd.Timestamp("2019-03-14", tz="UTC") + tot = (rate["BTC"]["series"].index.max() - b0).days + print(f"\n (b) Il PROXY inverse prima del {sw.date()} ({(sw-b0).days/tot:.0%} del campione).") + print(" L'accordo misurato") + print(" in sezione 0 e' la banda dell'errore; la sezione 3 riporta anche la finestra") + print(" SOLO-LINEARE, dove il proxy non entra.") + + print("\n (c) LA DISCRETEZZA. Deribit accumula il funding in CONTINUO (interest_1h e' gia'") + print(" il realizzato dell'ora): su questo venue non c'e' il problema 'posizione") + print(" all'istante del pagamento' che hanno i venue a 8h. Cio' che resta discreto e'") + print(" l'istante di USCITA di SKH01 dentro la barra 230m: il modello lo pone alla") + print(" CHIUSURA della barra di trigger (limite SUPERIORE del funding pagato).") + print(" Banda misurata sostituendo l'apertura della barra di trigger:") + for a in ASSETS: + _, tr = skh_daily(a, 0, "hourly", rate) + span_h = (tr[-1]["t_out"] - tr[0]["t_in"]) / MSH + yrs = span_h / (24 * 365.25) + hi = float(np.sum([t["f"] for t in tr])) / yrs + lo_ = float(np.sum([ + (cumF(rate[a], t["t_out"] - r02.MS_LTF) - cumF(rate[a], t["t_in"])) * t["dir"] + for t in tr])) / yrs + print(f" {a}: [{lo_:.4%}, {hi:.4%}] /anno di sleeve (ampiezza {hi-lo_:.4%})") + + print("\n (d) NON e' modellato il funding sul collaterale (il conto e' USDC, il margine non") + print(" rende) ne' il costo di prestito: non esistono su questo percorso.") + print(" (e) Il libro di RICERCA a 5 sleeve non e' ricalcolato qui: VRP01 e' opzioni (niente") + print(" funding), GTAA01 e' azioni (niente funding, e non e' deployabile), XS01 e XSR01") + print(" sono su Hyperliquid e il loro funding e' un'ALTRA misura, market-neutral per") + print(" costruzione (long e short si compensano solo se il funding e' uniforme).") + + +# =========================================================================== +def main() -> None: + print("=" * 100) + print("r0822d — IL FUNDING DEI PERPETUAL: il costo che il progetto non ha mai prezzato") + print(f"branch research/wave-0822 · {pd.Timestamp.now(tz='UTC')} · rete={'SI' if USE_NET else 'NO'}") + print("=" * 100) + + d0 = sezione0() + rate = build_rate(d0["F"]) + print(f"\n serie oraria unificata per asset (linear dove esiste, inverse prima):") + for a in ASSETS: + print(f" {a}: {len(rate[a]['ts']):,} ore, switch inverse->linear il " + f"{rate[a]['switch'].date()}") + + verifica_scala(rate, d0["F"]) + + print(f"\n{clock()} sezione 1...") + sezione1(rate) + print(f"\n{clock()} sezione 2...") + d2 = sezione2(rate) + print(f"\n{clock()} sezione 3...") + d3 = sezione3(rate) + print(f"\n{clock()} sezione 4...") + sezione4a(d3) + sezione4b(rate, r02.OFFSETS) + sezione4c(rate) + sezione4d(rate) + print(f"\n{clock()} sezione 5...") + sezione5(rate) + sezione6(rate) + + sez("GATE — esito di ciascuno, e quelli non girati DICHIARATI") + A0, B0 = book_75_25(None), book_75_25(rate) + ns = int(sum(len(skh_daily(a, 0, "hourly", rate)[1]) for a in ASSETS)) + npos = int(sum(np.sum(np.array([t["gross"] for t in skh_daily(a, 0, "hourly", rate)[1]]) <= 0) + for a in ASSETS)) + ip = al.implausible_sharpe(B0, n_trades=ns, n_losing_trades=npos) + print(f" implausible_sharpe : implausible={ip['implausible']} " + f"(Sharpe {ip['sharpe']:.2f}, maxDD {ip['maxdd']:.1%}, Calmar {ip['calmar']:.2f}, " + f"barre attive in perdita {ip['loss_frac']:.1%}) -> {ip['reasons'] or 'nessuna firma'}") + print(" replica bit-exact : PASSATA 5/5 (sez. 1) — nessun delta stampato prima.") + print(" controllo di scala tempi: PASSATO, con potenza verificata (sez. 0).") + print(" conto reale : PASSATO — modello vs `realized_pnl` di sessione, sez. 5.") + print(" anchor (banda d'ancora) : GIRATO sul peso — 23 offset, mediana delle differenze") + print(" APPAIATE (sez. 4b). Le altre tabelle sono all'ancora") + print(" canonica: il funding e' un COSTO deterministico, non una") + print(" selezione, e il suo delta e' negativo a 23/23 offset.") + print(" marginal_vs_tp01 / DSR / study_family_honest / de-levering null / causality:") + print(" NON GIRATI, e non per budget: qui non c'e' nessun") + print(" CANDIDATO. Non si propone una strategia, si toglie un") + print(" costo che mancava a quelle esistenti. Non c'e' una") + print(" griglia su cui barare e non c'e' un segnale di cui") + print(" verificare la causalita' (il funding di un'ora si") + print(" applica alla posizione tenuta in quell'ora).") + print(" weights_tilt_null : NON GIRATO perche' non serve — l'argmax del peso NON si") + print(" sposta (sez. 4b), quindi non c'e' nessun cambio di pesi") + print(" da sottoporre al gate.") + + sez("SINTESI") + a, b = d3["LIBRO 75/25 (path live)"] + print(f" LIBRO 75/25 path live: drift {a['drift']:.2%} -> {b['drift']:.2%} " + f"({b['drift']-a['drift']:+.2%}/anno)") + print(f" Sharpe FULL {a['full']:.3f} -> {b['full']:.3f}, " + f"HOLD {a['hold']:.3f} -> {b['hold']:.3f}, maxDD {a['dd']:.1%} -> {b['dd']:.1%}") + print(f" quota del drift bruciata dal funding: {(a['drift']-b['drift'])/a['drift']:.1%}") + print(f" decomposizione (sez.2): {d2['book_drag_decomp']:.3%}/anno") + rec = pd.Timestamp("2025-01-01", tz="UTC") + A_, B_ = d3["_pairs"]["LIBRO 75/25 (path live)"] + dr = (float(A_[A_.index >= rec].mean()) - float(B_[B_.index >= rec].mean())) * 365.25 + lo_ = min(rate[x]["switch"] for x in ASSETS) + A2, B2 = A_[A_.index >= lo_], B_[B_.index >= lo_] + dl = (float(A2.mean()) - float(B2.mean())) * 365.25 + print(f" ... {dl:.2%}/anno sulla sola finestra dello strumento VERO (>= {lo_.date()}, senza") + print(f" proxy), e {dr:.2%}/anno sul solo 2025+. IL NUMERO VA CITATO CON LA FINESTRA:") + print(f" la media di 7 anni non e' l'aspettativa di domani.") + print("\n COSA CAMBIA e COSA NO:") + print(" CAMBIA · il muro di capitale e la rendita perpetua (+17-18% di muro: la rendita") + print(" vive sul DRIFT, e il funding e' una tassa sul drift)") + print(" · la traiettoria da $600 (a EUR250/mese, P(entro 20a) 90% -> 64%)") + print(" · il soffitto direzionale BTC/ETH: ~1,31 -> ~1,15 netto funding") + print(" NON CAMBIA · il peso 75/25 (argmax invariato a 23/23 offset)") + print(" · il gradino di leva 1,00 -> 1,25x (k* 13,7 -> 12,3, guadagno -12%)") + print(" · il maxDD (+0,3pp: e' una tassa sul drift, non sulla coda)") + print(" · l'ordine delle leve del piano: versare resta la prima di tutte") + + print("\n GRIGLIA DICHIARATA (contata al RIALZO):") + print(" 4 strumenti scaricati x ~55-90 richieste = ~290 chiamate al venue") + print(" 3 ipotesi sulla convenzione temporale · 2 lenti (merged / solo-lineare)") + print(" 5 serie x 2 (con/senza funding) = 10 valutazioni in sez. 3, + 8 anni di split") + print(" 8 pesi x 23 offset x 2 = 368 valutazioni di blend in sez. 4b") + print(" 4 muri x 26 passi di bisezione x 2.000 path MC + 2 x 5 livelli di traiettoria") + print(" 2 (con/senza) x 800 valori di k in sez. 4d · 4 finestre x 2 gambe in sez. 5") + print(" NESSUNA di queste e' una SELEZIONE: non c'e' una cella scelta guardando il") + print(" risultato, quindi non c'e' un deflated-Sharpe da calcolare. La griglia ridotta") + print(" per budget e': i muri girano a 2.000 path invece di 4.000 (risoluzione ~1%,") + print(" dichiarata), e le tabelle non-4b sono all'ancora canonica.") + print(f"\n{clock()} fine.") + + +if __name__ == "__main__": + main()