diff --git a/scripts/research/r0823_costo_capitale.py b/scripts/research/r0823_costo_capitale.py new file mode 100644 index 0000000..583e25b --- /dev/null +++ b/scripts/research/r0823_costo_capitale.py @@ -0,0 +1,1113 @@ +#!/usr/bin/env python3 +"""r0823_costo_capitale.py — §55 COSTO-CAPITALE: il muro quando il costo d'esecuzione +NON e' una costante ma una FUNZIONE del capitale. + +IL BUCO +------- +Ogni muro di capitale pubblicato dal progetto ($272k, $278k, $313k, $325k, la traiettoria da +$635, le tabelle dei versamenti) e' calcolato con un costo d'esecuzione COSTANTE: le fee +proporzionali di `backtest_signals` (0,10% RT) piu' il pavimento `min_order` di +`CC.tp01_realistic`. Ma un muro che si raggiunge ACCUMULANDO attraversa tutte le taglie, e il +costo vero cresce con la taglia dell'ordine. Il costo non e' un parametro: e' **una funzione +lungo il cammino**. + +La domanda e' nata due volte nella stessa notte, da due filoni che non si parlavano: + * §52 SKEPTIC-SPOT — camminando entrambi i libri, il differenziale spot-perp passa da + +0,72 bps a $600 a **+25,66 bps a $272k**: il lead dello spot si annulla esattamente alla + taglia del muro che pretende di spostare. Follow-up dichiarato: «il muro rifatto con costo + d'esecuzione FUNZIONE DEL CAPITALE». + * §53 SKEPTIC-LEVA — «§5 ha misurato un fill al 21,9% del volume della sua barra 5m a $635; + con partecipazione lineare 1,25x lo porta al 27,4% SUBITO. Va misurata PRIMA del gradino.» + * §5 SLIP-AUDIT porta gia' una **data di scadenza esplicita**: «quel 22% diventa 172% di una + barra 5m a $5.000 e 687% a $20.000. Va rifatta a ogni salto di taglia.» + +COSA FA QUESTO SCRIPT +--------------------- + 1. [VENUE] Misura `costo_bps(taglia)` camminando il libro reale di BTC_USDC-PERPETUAL e + ETH_USDC-PERPETUAL su piu' istantanee, e ne riporta la BANDA (§45: due istantanee a 4 + minuti differivano del 79% — un'istantanea non e' una distribuzione). + 2. [BACKTEST] Ricava la distribuzione VERA delle taglie d'ordine del libro live (TP01 75 / + SKH01 25) a ogni capitale. Non l'ordine medio: il costo e' convesso, la media dei costi + non e' il costo della media. + 3. [CALC] Rifa' il libro con il costo DENTRO, itera il muro come PUNTO FISSO + (`r0726_wall_fixedpoint`, importato) e riporta se converge. + 4. [CALC] Trova il PUNTO DI SATURAZIONE con DUE definizioni indipendenti — un limite DURO + puramente misurato (la taglia a cui l'1% del nozionale annuo non e' piu' eseguibile in un + istante) e uno ECONOMICO (l'argmax di `C x drift(C)`, dove il capitale marginale smette di + comprare reddito). Numero mai calcolato dal progetto. + 5. [CALC] Rifa' la traiettoria «EUR500/mese da $635» col costo LUNGO IL CAMMINO + (`r0822d_piano_vero`, importato). + 6. [CALC] Il gradino di leva col costo endogeno: k massimo. + +REPLICHE OBBLIGATORIE PRIMA DI OGNI DELTA (regola del progetto: prima di pubblicare un numero +nuovo, far riprodurre alla macchina quello vecchio) + R1 la mia TP01 con costo spento == `FD.tp01_realistic` bit-exact + R2 la mia SKH01 con costo spento == `FD.skh_book` bit-exact + R3 il mio libro con costo spento == `FD.book_75_25` bit-exact + R4 muro L0 LORDO sul vintage 2026-08-07 $272.061 al dollaro + R5 muro L1 +FISCO sul vintage 2026-08-07 $258.338 al dollaro + R6 muro L3 CONGIUNTA sulla serie di oggi $313k (§36) + R7 `accumula_costo` con drag spento == `PN.accumula` bit-exact + R8 la mia `walk_cost_bps` == quella di `r0823_skeptic_spot` identita' su libro sintetico + R9 k* a costo ZERO == `FD.k_star` sul libro pubblicato 12,25x (§35) + piu' DUE controlli POSITIVI obbligatori: un costo di 200 bps per ordine deve uccidere il + libro, e un drag di 3%/anno sopra $100k deve muovere la traiettoria. Un modello di costo che + non morde mai e' indistinguibile da uno rotto. + +ETICHETTE DI PROVENIENZA + [VENUE] misurato dall'API pubblica Deribit in questa sessione + [LOG] dai log/fill reali del libro live + [BACKTEST] dalla serie storica certificata + [CALC] aritmetica sui due precedenti + [DEDOTTO] estrapolazione oltre la regione misurata + [ASSUNTO] scelta di modello dichiarata + +ASSUNZIONE PRINCIPALE, DICHIARATA IN TESTA + Il libro d'ordini e' una FOTOGRAFIA DEL PRESENTE. Proiettarla su 7 anni di storia (2019-2026, + quando la profondita' del venue era una frazione di quella di oggi) e' un'ASSUNZIONE, e va nel + verso OTTIMISTA: il numero che ne esce e' un limite superiore della capacita' STORICA, non una + misura di essa. Sul futuro — che e' cio' che il muro deve attraversare — l'assunzione e' la + costanza della profondita', non la sua crescita. + Nell'altro verso, e su un asse diverso: il campione e' raccolto di DOMENICA NOTTE UTC, cioe' + nella finestra piu' sottile della settimana. Dentro il presente e' quindi CONSERVATIVO — un + campione in orario di punta darebbe costi piu' bassi. + +SOLA LETTURA / RETE / SOLDI VERI + Nessuna scrittura nel repo (cache FUORI, nello scratchpad). Nessun ordine, mai: solo GET + PUBBLICHE senza chiave, pacing <=2 req/s, astensione nei minuti :05-:10 e :24-:30 (finestre + del cron di produzione). + + uv run python scripts/research/r0823_costo_capitale.py + uv run python scripts/research/r0823_costo_capitale.py --no-net # solo cache + uv run python scripts/research/r0823_costo_capitale.py --rounds 8 --gap 35 + uv run python scripts/research/r0823_costo_capitale.py --fetch-only --rounds 22 --gap 55 +""" +from __future__ import annotations + +import contextlib +import datetime as dt +import io +import json +import sys +import time +import urllib.request +from pathlib import Path + +import numpy as np +import pandas as pd + +# --------------------------------------------------------------------------- +# Il MIO flag di rete si legge PRIMA di importare qualunque libreria del progetto: +# `r0822d_funding` decide all'import se andare in rete, e qui deve usare la sua cache. +# --------------------------------------------------------------------------- +_NET = "--no-net" not in sys.argv + + +def _argf(name: str, default: float) -> float: + if name in sys.argv: + return float(sys.argv[sys.argv.index(name) + 1]) + return default + + +ROUNDS = int(_argf("--rounds", 8)) +GAP_S = _argf("--gap", 35.0) +FETCH_ONLY = "--fetch-only" in sys.argv + +if "--no-net" not in sys.argv: + sys.argv.append("--no-net") + +ROOT = Path("/opt/docker/PythagorasGoal") +for _p in (ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt", ROOT): + sys.path.insert(0, str(_p)) + +import r0702_anchor_skh01 as r02 # noqa: E402 run_asset (ltf/ent), sim_equity +import r0725_capcurve as CC # noqa: E402 DERIBIT_FEE_SIDE/MIN_ORDER, _boot_paths +import r0726_wall_fixedpoint as WF # noqa: E402 la STRUTTURA del punto fisso +import r0727_tasse as TX # noqa: E402 PATRIMONIALE, CARRY_ANNI +import r0807_piano_netto as PN # noqa: E402 muro, accumula, passo_fiscale +import r0822d_funding as FD # noqa: E402 funding, tp01_realistic, skh_daily +import r0822d_piano_vero as PV # noqa: E402 Lente, deluck, muro_di, leggi +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 (CANONICAL, TrendPortfolio, # noqa: E402 + resample_1d, simple_returns) + +# --------------------------------------------------------------------------- +# Costanti dichiarate +# --------------------------------------------------------------------------- +ASSETS = ("BTC", "ETH") +INSTR = {"BTC": "BTC_USDC-PERPETUAL", "ETH": "ETH_USDC-PERPETUAL"} # cio' che il libro TRADA +W_TP, W_SKH = 0.75, 0.25 +LO = pd.Timestamp("2019-03-14", tz="UTC") # inizio del libro (convenzione FD.book_75_25) +DPY = 365.0 +API = "https://www.deribit.com/api/v2/public/" +DEPTH = 1000 # livelli richiesti per lato +VINTAGE_PUB = "2026-08-07" + +# griglia di taglie: log-spaziata da $5 (il min_order) a $10M +SIZES = np.unique(np.round(10.0 ** np.arange(np.log10(5.0), 7.0001, 0.125), 2)) +# griglia di capitali su cui si misura il libro (ordini di grandezza, non un continuo) +CAPS = (635.0, 2_000.0, 5_000.0, 20_000.0, 50_000.0, 100_000.0, 313_000.0, + 1_000_000.0, 3_000_000.0, 10_000_000.0, 30_000_000.0, 100_000_000.0) + +_SP = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" + "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad") +CACHE = (_SP if _SP.parent.exists() else Path("/tmp/pythagoras_costo")) / "costocap" +CACHE.mkdir(parents=True, exist_ok=True) +BOOKS = CACHE / "books.jsonl" +FILLS = _SP / "r0822_slip_fills.csv" # i 18 fill reali di §5 +LIN5M = {"BTC": _SP / "lin_btc_5m.parquet", "ETH": _SP / "lin_eth_5m.parquet"} + +T0 = time.time() +_last = [0.0] + + +def sez(t: str) -> None: + print("\n" + "=" * 98 + f"\n {t}\n" + "=" * 98) + + +def clock() -> str: + return f"[{time.time() - T0:5.0f}s]" + + +# =========================================================================== +# 0) RETE — GET pubbliche, pacing, astensione nelle finestre del cron +# =========================================================================== +def _guard_window() -> None: + while True: + m = dt.datetime.now(dt.timezone.utc).minute + if not (5 <= m <= 10 or 24 <= m <= 30): + return + tgt = 11 if m <= 10 else 31 + w = (tgt - m) * 60 - dt.datetime.now(dt.timezone.utc).second + print(f" [rete] minuto :{m:02d} = finestra del cron di produzione -> attendo {max(w,1)}s") + time.sleep(max(w, 1)) + + +def _get(path: str, tries: int = 3): + if not _NET: + return None + _guard_window() + for k in range(tries): + d = time.time() - _last[0] + if d < 0.55: # <=2 req/s + time.sleep(0.55 - d) + _last[0] = time.time() + try: + with urllib.request.urlopen(API + path, timeout=30) as r: + return json.load(r) + except Exception as ex: # noqa: BLE001 + if k == tries - 1: + print(f" [rete] KO {path[:64]}: {ex}") + return None + time.sleep(1.5 * (k + 1)) + return None + + +def raccogli(rounds: int, gap: float) -> int: + """Istantanee del libro, appese alla cache. Ritorna quante ne ha aggiunte.""" + if not _NET or rounds <= 0: + return 0 + n = 0 + with BOOKS.open("a") as fh: + for r in range(rounds): + for a in ASSETS: + j = _get(f"get_order_book?instrument_name={INSTR[a]}&depth={DEPTH}") + if not j or "result" not in j: + continue + q = j["result"] + if not q.get("bids") or not q.get("asks"): + continue + fh.write(json.dumps(dict( + ts=int(q.get("timestamp", time.time() * 1000)), asset=a, + bid=float(q["best_bid_price"]), ask=float(q["best_ask_price"]), + bids=[[float(p), float(s)] for p, s in q["bids"]], + asks=[[float(p), float(s)] for p, s in q["asks"]])) + "\n") + n += 1 + if r < rounds - 1: + time.sleep(gap) + return n + + +def leggi_libri() -> list: + if not BOOKS.exists(): + return [] + out = [] + for ln in BOOKS.read_text().splitlines(): + if ln.strip(): + with contextlib.suppress(Exception): + out.append(json.loads(ln)) + return out + + +# =========================================================================== +# 1) LA CURVA COSTO-TAGLIA +# =========================================================================== +def walk_cost_bps(book: dict, usd: float, side: str) -> float | None: + """Costo effettivo vs mid di un ordine da `usd` eseguito ISTANTANEAMENTE contro il libro + visibile (stessa convenzione di `r0823_skeptic_spot.walk_cost_bps` e + `r0822e_funding_avoid.walk_cost_bps`). None se il libro visibile NON basta: allora si + dichiara, non si estrapola.""" + if not book or not book.get("bid") or not book.get("ask"): + return None + mid = (book["bid"] + book["ask"]) / 2.0 + lv = book.get("asks" if side == "buy" else "bids") or [] + need, got, cost = usd / mid, 0.0, 0.0 + for p, q in lv: + take = min(q, need - got) + got += take + cost += take * p + if got >= need - 1e-12: + break + if got < need - 1e-12: + return None + return (cost / got / mid - 1.0) * 1e4 * (1.0 if side == "buy" else -1.0) + + +class Curva: + """`costo_bps(asset, usd, q)` per interpolazione log-log sulle quantili misurate. + + * sotto la taglia minima misurata: il costo e' il mezzo spread (costante) — e' cio' che il + libro DICE, non un'assunzione; + * dentro la regione misurata: interpolazione lineare in log(taglia), con monotonia imposta + (`cummax`): camminare un libro non puo' costare MENO su un ordine piu' grande, e una + violazione sarebbe rumore fra istantanee; + * oltre l'ultima taglia con copertura sufficiente (`lim`): il libro visibile NON BASTA, cioe' + l'ordine non e' eseguibile in un istante. Li' si estrapola con pendenza log-log + `max(misurata, 1.0)` — [ASSUNTO] dichiarato: impatto ALMENO lineare nella taglia, perche' + una pendenza < 1 (raddoppiare l'ordine costa meno del doppio) oltre la fine del libro non + e' difendibile. Ogni cella oltre `lim` e' marcata [DEDOTTO]. + + Le tre quantili condividono maschera, `lim` e pendenza -> la banda resta ORDINATA anche + nella regione estrapolata (con tre oggetti separati p10 poteva finire sopra p50: errore + catturato alla prima corsa). + """ + + QS = (0.10, 0.50, 0.90) + + def __init__(self, libri: list, cop_min: float = 0.8): + self.cop_min = cop_min + self.x, self.y, self.lim, self.slope, self.nobs = {}, {}, {}, {}, {} + self.slope_mis = {} + for a in ASSETS: + L = [b for b in libri if b["asset"] == a] + self.nobs[a] = len(L) + M, cop = {q: [] for q in self.QS}, [] + for sz in SIZES: + v = [] + for b in L: + for side in ("buy", "sell"): + c = walk_cost_bps(b, float(sz), side) + v.append(np.nan if c is None else c) + v = np.asarray(v, float) + cop.append(float(np.isfinite(v).mean()) if len(v) else 0.0) + for q in self.QS: + M[q].append(float(np.nanquantile(v, q)) if np.isfinite(v).any() else np.nan) + cop = np.asarray(cop) + ok = np.isfinite(np.asarray(M[0.5])) & (cop >= cop_min) + if not ok.any(): + raise SystemExit(f"nessuna taglia misurabile su {a}: campione insufficiente") + xs = np.log10(SIZES[ok]) + ys = {q: np.maximum.accumulate(np.asarray(M[q])[ok]) for q in self.QS} + ys[0.50] = np.maximum(ys[0.50], ys[0.10]) # banda ordinata per costruzione + ys[0.90] = np.maximum(ys[0.90], ys[0.50]) + self.x[a], self.y[a] = xs, ys + self.lim[a] = float(SIZES[ok][-1]) + m = xs >= xs[-1] - 1.0 + sl = (float(np.polyfit(xs[m], np.log10(np.maximum(ys[0.50][m], 1e-4)), 1)[0]) + if m.sum() >= 2 else 1.0) + self.slope[a] = max(sl, 1.0) # [ASSUNTO] impatto almeno lineare + self.slope_mis[a] = sl + + def oltre(self, asset: str, usd: float) -> bool: + return usd > self.lim[asset] + + def bps(self, asset: str, usd: float, q: float = 0.50) -> float: + if usd <= 0: + return 0.0 + xs, ys = self.x[asset], self.y[asset][q] + lx = np.log10(usd) + if lx <= xs[0]: + return float(ys[0]) + if lx <= xs[-1]: + return float(np.interp(lx, xs, ys)) + return float(10.0 ** (np.log10(max(ys[-1], 1e-4)) + self.slope[asset] * (lx - xs[-1]))) + + def vec(self, asset: str, usd: np.ndarray, q: float = 0.50) -> np.ndarray: + return np.array([self.bps(asset, float(u), q) for u in usd], float) + + def profondita(self, libri: list, asset: str, soglia_bps: float) -> float: + """[VENUE] Dollari eseguibili entro `soglia_bps` di costo (mediana sulle istantanee, + lato peggiore).""" + v = [] + for b in [x for x in libri if x["asset"] == asset]: + lo, hi = 0.0, 5e7 + for _ in range(30): + mid = (lo + hi) / 2 + c = walk_cost_bps(b, mid, "buy") + d = walk_cost_bps(b, mid, "sell") + w = max([z for z in (c, d) if z is not None], default=1e9) + if w <= soglia_bps: + lo = mid + else: + hi = mid + v.append(lo) + return float(np.median(v)) if v else float("nan") + + +class CurvaZero(Curva): + """Costo identicamente nullo: il controllo negativo (replica bit-exact dei numeri vecchi).""" + + def __init__(self): # noqa: D107 + self.nobs, self.lim, self.slope = {}, {a: float("inf") for a in ASSETS}, {} + + def oltre(self, asset: str, usd: float) -> bool: # noqa: D102 + return False + + def bps(self, asset: str, usd: float, q: float = 0.50) -> float: # noqa: D102 + return 0.0 + + def vec(self, asset: str, usd: np.ndarray, q: float = 0.50) -> np.ndarray: # noqa: D102 + return np.zeros(len(usd)) + + +class CurvaFissa(CurvaZero): + """Costo costante in bps: il controllo POSITIVO. Un modello di costo che non morde mai e' + indistinguibile da uno rotto, quindi va provato che morde.""" + + def __init__(self, bps: float): # noqa: D107 + super().__init__() + self._b = float(bps) + + def bps(self, asset: str, usd: float, q: float = 0.50) -> float: # noqa: D102 + return self._b if usd > 0 else 0.0 + + def vec(self, asset: str, usd: np.ndarray, q: float = 0.50) -> np.ndarray: # noqa: D102 + return np.where(np.asarray(usd) > 0, self._b, 0.0) + + +# =========================================================================== +# 2) IL LIBRO CON IL COSTO DENTRO +# =========================================================================== +def tp01_costo(alloc: float, rate: dict | None, cur: Curva, scala: float = 1.0, + q: float = 0.50) -> tuple[pd.Series, dict]: + """`FD.tp01_realistic` con DUE gradi di liberta' in piu': il costo d'esecuzione funzione + della taglia dell'ordine, e la scala di leva (`SPEC-scale-key`: la scala moltiplica il + TARGET, non il capitale). Con cur=CurvaZero() e scala=1 deve dare bit-exact l'originale.""" + 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)) * scala + held = np.empty(len(tgt)) + cur_pos = 0.0 + for i in range(len(tgt)): + if abs(tgt[i] - cur_pos) * cap_asset >= CC.DERIBIT_MIN_ORDER: + cur_pos = tgt[i] + held[i] = cur_pos + pos = np.zeros(len(held)) + pos[1:] = held[:-1] + dpos = np.abs(np.diff(pos, prepend=0.0)) + net = pos * r - CC.DERIBIT_FEE_SIDE * dpos + ordini = dpos * cap_asset # nozionale in dollari di OGNI ordine + cbps = cur.vec(a, ordini, q) + net = net - dpos * cbps / 1e4 # il costo pesa il nozionale scambiato + if rate is not None: + net = net - pos * FD.daily_funding(rate[a], idx) + net[0] = 0.0 + diag[a] = dict(pos=pos, ordini=ordini[ordini > 0], cbps=cbps[ordini > 0], 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 + + +_LTF: dict = {} + + +def _ltf(a: str): + if a not in _LTF: + _, _m0, ltf, _ent = r02.run_asset(a, 0) + _LTF[a] = ltf + return _LTF[a] + + +def _rebuild_equity(n: int, trades: list, extra: float, scala: float) -> np.ndarray: + """Ricostruisce la curva di equity di SKH01 dai TRADE gia' prodotti da `FD.sim_equity_f`, + con un costo `extra` per round-trip e una scala di leva. Non e' una re-implementazione della + strategia: i trade (boundary, direzione, gross, funding) sono presi COSI' COME SONO, cambia + solo la contabilita' del singolo trade. Con extra=0 e scala=1 e' bit-exact l'originale.""" + initial = 1000.0 + capital = initial + equity = np.full(n, capital, dtype=float) + for t in trades: + net = scala * (t["gross"] - 0.001 - t["f"]) - extra + capital += capital * net + capital = max(capital, 1.0) + equity[t["i"]:t["exit_idx"] + 1] = capital + last = initial + for k in range(n): + if equity[k] != last and equity[k] != initial: + last = equity[k] + else: + equity[k] = last + return equity + + +def skh_costo(alloc: float, rate: dict | None, cur: Curva, scala: float = 1.0, + q: float = 0.50) -> pd.Series: + """SKH01 50/50 con il costo d'esecuzione dentro. + + SKH01 entra a nozionale 1,0x del proprio capitale (`position_size=1.0, leverage=1.0`) e fa + UN round-trip per trade -> due ordini, entrambi a nozionale pieno. Il costo per trade e' + percio' `2 x costo_bps(nozionale)/1e4`, e il nozionale e' `scala x alloc/2`.""" + cap_asset = alloc / len(ASSETS) + cols = {} + for a in ASSETS: + _d, tr = FD.skh_daily(a, 0, "hourly", rate) + ltf = _ltf(a) + extra = 2.0 * scala * cur.bps(a, scala * cap_asset, q) / 1e4 + eq = _rebuild_equity(len(ltf), tr, extra, scala) + s = pd.Series(eq, index=pd.DatetimeIndex(pd.to_datetime(ltf["datetime"], utc=True))) + cols[a] = s.resample("1D").last().ffill().pct_change().dropna() + J = pd.concat(cols, 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 + + +def libro(cap: float, rate: dict | None, cur: Curva, scala: float = 1.0, + q: float = 0.50) -> pd.Series: + """Il libro LIVE (TP01 75 + SKH01 25) al capitale `cap`, col costo d'esecuzione dentro.""" + tp, _ = tp01_costo(cap * W_TP, rate, cur, scala, q) + sk = skh_costo(cap * W_SKH, rate, cur, scala, q) + return combine_outer({"TP01": tp, "SKH01": sk}, {"TP01": W_TP, "SKH01": W_SKH}, lo=LO) + + +# =========================================================================== +# 3) ACCUMULO CON COSTO LUNGO IL CAMMINO +# =========================================================================== +def accumula_costo(paths: np.ndarray, dep_eur: float, bersaglio: float, aliquota: float, + patrimoniale: float, start: float, drag, lump_eur: float = 0.0) -> dict: + """Gemella di `PN.accumula` con UN grado di liberta' in piu': un drag GIORNALIERO funzione + del capitale corrente. Con drag=None dev'essere bit-exact `PN.accumula` (replica R7). + + `drag(cap)` = (drift del libro alla taglia di riferimento) - (drift alla taglia `cap`), + diviso 365. E' un aggiustamento del solo DRIFT: la FORMA del percorso resta quella estratta. + [ASSUNTO] dichiarato — la vol cambia pochissimo con la taglia (sezione 4 lo riporta).""" + n_paths, n_days = paths.shape + dep = dep_eur * CC.EURUSD + cap = np.full(n_paths, start + lump_eur * CC.EURUSD) + versato = start + lump_eur * CC.EURUSD + carry = np.zeros((n_paths, TX.CARRY_ANNI)) + anno_start = cap.copy() + flusso = np.zeros(n_paths) + colpito = np.full(n_paths, -1, int) + for t in range(n_days): + if drag is None: + cap *= (1.0 + paths[:, t]) + else: + cap *= (1.0 + paths[:, t] - drag(cap)) + if t % 30 == 0 and t > 0 and dep > 0: + cap += dep + flusso += dep + versato += dep + nuovi = (colpito < 0) & (cap >= bersaglio) + colpito[nuovi] = t + if (t + 1) % 365 == 0 and (aliquota > 0 or patrimoniale > 0): + PN.passo_fiscale(cap, anno_start, flusso, carry, aliquota, patrimoniale) + anno_start = cap.copy() + flusso[:] = 0.0 + return dict(cap=cap, colpito=colpito, versato=versato) + + + + +# =========================================================================== +# 4) DIAGNOSTICHE DI CAPACITA' +# =========================================================================== +def quota_oltre(cap: float, cur: Curva, rate: dict | None) -> tuple[float, float]: + """(quota del nozionale annuo scambiato la cui TAGLIA supera il libro visibile, + nozionale annuo totale in $). E' una misura di CAPACITA' puramente [VENUE]+[BACKTEST]: + non dipende da nessuna estrapolazione.""" + Z = CurvaZero() + _s, dg = tp01_costo(cap * W_TP, None, Z) + anni = 0.0 + tot = fuori = 0.0 + for a in ASSETS: + o = dg[a]["ordini"] + idx = dg[a]["idx"] + anni = max(anni, (idx[-1] - idx[0]).days / DPY) + tot += float(o.sum()) + fuori += float(o[o > cur.lim[a]].sum()) + tr = FD.skh_daily(a, 0, "hourly", None)[1] + noz = cap * W_SKH / 2 + tot += 2 * len(tr) * noz + if noz > cur.lim[a]: + fuori += 2 * len(tr) * noz + return (fuori / tot if tot > 0 else 0.0), tot / max(anni, 1e-9) + + +# =========================================================================== +def main() -> None: # noqa: C901 + print("=" * 98) + print(" r0823 §55 COSTO-CAPITALE — il muro quando il costo d'esecuzione e' una FUNZIONE") + print("=" * 98) + print(" Provenienza: [VENUE] API pubblica · [LOG] fill reali · [BACKTEST] serie certificata") + print(" [CALC] aritmetica · [DEDOTTO] estrapolazione · [ASSUNTO] modello") + + # ---------------------------------------------------------------- rete + nuovi = raccogli(ROUNDS, GAP_S) + if FETCH_ONLY: + print(f" --fetch-only: {nuovi} istantanee aggiunte. Fine.") + return + libri = leggi_libri() + if not libri: + raise SystemExit("nessuna istantanea del libro: rilanciare con rete.") + ts = pd.to_datetime([b["ts"] for b in libri], unit="ms", utc=True) + print(f"\n [VENUE] {len(libri)} istantanee ({nuovi} nuove in questa corsa), " + f"{sum(1 for b in libri if b['asset']=='BTC')} BTC / " + f"{sum(1 for b in libri if b['asset']=='ETH')} ETH") + print(f" dal {ts.min():%Y-%m-%d %H:%M} al {ts.max():%Y-%m-%d %H:%M} UTC " + f"(arco {(ts.max()-ts.min()).total_seconds()/60:.0f} min) {clock()}") + print(" ⚠️ E' un solo regime di mercato in un arco di minuti: e' una banda INTRA-GIORNO,") + print(" non la distribuzione della profondita' su 7 anni. Vedi l'assunzione in testa.") + gg = ["lun", "mar", "mer", "gio", "ven", "sab", "dom"][int(ts.max().dayofweek)] + print(f" 📌 Ma il MOMENTO e' favorevole al risultato conservativo, non a quello comodo: " + f"{gg} {ts.max():%H:%M} UTC") + print(" = fine settimana, notte europea, mattina asiatica — la finestra in cui il libro") + print(" e' PIU' SOTTILE della settimana. Un campione preso in orario di punta darebbe") + print(" costi piu' bassi, cioe' rafforzerebbe la conclusione invece di indebolirla.") + + # ===================================================================== + sez("1) LA CURVA COSTO-TAGLIA, MISURATA — e la sua banda") + print(""" Camminata del libro visibile: costo effettivo vs mid di un ordine ESEGUITO + ISTANTANEAMENTE. E' la lente giusta per QUESTO libro perche' l'esecutore manda ordini + MARKET (`book_execute`), non limit: cio' che il libro mostra e' cio' che l'ordine paga. + Ogni cella e' la mediana su (istantanea x lato); la banda e' p10-p90 sulla stessa + popolazione. §45 ha misurato due istantanee a 4 minuti che differivano del 79%: un solo + numero qui sarebbe una finzione.""") + cur = Curva(libri) + print(f"\n {'ordine':>12} | {'BTC_USDC-PERP mediana [p10-p90]':>40} | " + f"{'ETH_USDC-PERP mediana [p10-p90]':>40}") + print(" " + "-" * 98) + MOSTRA = (5, 50, 500, 5_000, 50_000, 200_000, 500_000, 1_000_000, 3_000_000) + for sz in MOSTRA: + cells = [] + for a in ASSETS: + if cur.oltre(a, sz): + cells.append(f"{cur.bps(a, sz):10.2f} [DEDOTTO, oltre il libro]") + else: + cells.append(f"{cur.bps(a, sz):8.3f} [{cur.bps(a, sz, .10):7.3f}," + f"{cur.bps(a, sz, .90):8.3f}] bps") + print(f" ${sz:>11,} | {cells[0]:>40} | {cells[1]:>40}") + print(" " + "-" * 98) + print(f" ultima taglia con copertura >=80% del campione (`lim`): " + f"BTC ${cur.lim['BTC']:,.0f} · ETH ${cur.lim['ETH']:,.0f}") + print(f" pendenza log-log oltre `lim`: misurata BTC {cur.slope_mis['BTC']:.2f} / " + f"ETH {cur.slope_mis['ETH']:.2f} -> usata {cur.slope['BTC']:.2f} / " + f"{cur.slope['ETH']:.2f} (floor 1.0 = impatto almeno lineare) [ASSUNTO]") + print(""" + ⚠️ ERRORE MIO, catturato alla prima corsa. La prima stesura costruiva TRE oggetti-curva + separati (p10, p50, p90), ognuno con la propria maschera di copertura e la propria + pendenza di estrapolazione: oltre il libro visibile la p10 finiva SOPRA la p50 (ETH a $3M: + 18,7 contro 15,5 bps). Una banda che si incrocia non e' una banda. Ora le tre quantili + condividono maschera, `lim` e pendenza, e l'ordinamento e' imposto per costruzione.""") + hs = {a: cur.bps(a, 5.0) for a in ASSETS} + print(f"\n [VENUE] Il fondo della curva E' il mezzo spread: BTC {hs['BTC']:.4f} bps, " + f"ETH {hs['ETH']:.4f} bps — cioe' UN TICK.") + print(" ⚠️ Questo NON e' il libro SPOT di §52 (mezzo spread misurato 1,5-3,1 bps, e 5,3 in") + print(" sessione): 100-500x piu' largo. La conclusione di §52 non e' contraddetta, e'") + print(" CONFERMATA dall'altro lato — il costo che si annullava li' era dello SPOT.") + + print("\n [VENUE] PROFONDITA' entro una soglia di costo (mediana sulle istantanee, " + "lato peggiore):") + print(f" {'soglia':>10}{'BTC $ eseguibili':>22}{'ETH $ eseguibili':>22}") + PROF = {} + for soglia in (0.5, 1.0, 5.0, 20.0, 100.0): + row = [cur.profondita(libri, a, soglia) for a in ASSETS] + PROF[soglia] = dict(zip(ASSETS, row)) + print(f" {soglia:>8.1f}bps{row[0]:>22,.0f}{row[1]:>22,.0f}") + + # ===================================================================== + sez("2) LA DISTRIBUZIONE VERA DELLE TAGLIE D'ORDINE — non l'ordine medio") + print(""" Il costo e' CONVESSO: la media dei costi non e' il costo della media. Percio' il + costo si applica ordine per ordine, sul nozionale che il libro manda DAVVERO. + TP01: |Dpos| x (cap x 0,75/2), col pavimento min_order $5 gia' dentro (CC.tp01_realistic). + SKH01: due ordini a nozionale PIENO (cap x 0,25/2) per trade, 1,0x per costruzione.""") + with contextlib.redirect_stdout(io.StringIO()): + F = FD.sezione0()["F"] + rate = FD.build_rate(F) + Z = CurvaZero() + print(f"\n {'capitale':>13}{'TP01 n.ord':>12}{'mediana':>11}{'p90':>12}{'max':>13}" + f"{'quota noz.':>12}{'SKH01 ordine':>14}{'x n.':>8}{'quota noz.':>12}") + ordini_cache = {} + for C in (635.0, 5_000.0, 50_000.0, 313_000.0, 3_000_000.0): + _s, dg = tp01_costo(C * W_TP, None, Z) + o = np.concatenate([dg[a]["ordini"] for a in ASSETS]) + ordini_cache[C] = o + ns = sum(len(FD.skh_daily(a, 0, "hourly", None)[1]) for a in ASSETS) + nsk = 2 * ns * C * W_SKH / 2 + print(f" ${C:>12,.0f}{len(o):>12,}{np.median(o):>11,.0f}" + f"{np.percentile(o, 90):>12,.0f}{o.max():>13,.0f}" + f"{o.sum()/(o.sum()+nsk):>11.0%}{C * W_SKH / 2:>14,.0f}{2 * ns:>8,}" + f"{nsk/(o.sum()+nsk):>11.0%}") + print("\n 📌 Le due gambe hanno distribuzioni OPPOSTE: TP01 manda MOLTI ordini piccoli") + print(" (deriva del vol-target), SKH01 POCHI ordini a nozionale pieno — ma sul") + print(" NOZIONALE SCAMBIATO SKH01 pesa il doppio di TP01. A pari capitale il suo") + print(" ordine e' ~100x la mediana di TP01: e' SKH01 a incontrare per primo il muro di") + print(" liquidita', e non e' cio' che si indovinerebbe contando gli ordini (§5: contare") + print(" gli ordini da' 16%, contare il nozionale da' 75% — stessa trappola).") + + # ===================================================================== + sez("3) REPLICA — prima di ogni delta, riprodurre i numeri pubblicati") + ok = {} + tpZ, _ = tp01_costo(635.0 * W_TP, rate, Z) + ref_tp, _ = FD.tp01_realistic(635.0 * W_TP, rate) + d = float(np.max(np.abs(tpZ.values - ref_tp.reindex(tpZ.index).values))) + ok["R1"] = d == 0.0 + print(f" R1 TP01 con costo spento vs `FD.tp01_realistic` max|dif| = {d:.1e} " + f"{'BIT-EXACT' if ok['R1'] else 'FALLITA'}") + + skZ = skh_costo(635.0 * W_SKH, rate, Z) + ref_sk = FD.skh_book(0, "hourly", rate) + d = float(np.max(np.abs(skZ.values - ref_sk.reindex(skZ.index).values))) + ok["R2"] = d == 0.0 + print(f" R2 SKH01 con costo spento vs `FD.skh_book` max|dif| = {d:.1e} " + f"{'BIT-EXACT' if ok['R2'] else 'FALLITA'}") + + bZ = libro(635.0, rate, Z) + ref_b = FD.book_75_25(rate, alloc=635.0) + d = float(np.max(np.abs(bZ.values - ref_b.reindex(bZ.index).values))) + ok["R3"] = d == 0.0 + print(f" R3 libro con costo spento vs `FD.book_75_25` max|dif| = {d:.1e} " + f"{'BIT-EXACT' if ok['R3'] else 'FALLITA'}") + + B = PV.costruisci_serie() + idx = pd.DatetimeIndex(B.index) + A_raw, F_raw = B["nofund"].values.astype(float), B["fund"].values.astype(float) + mv = np.asarray(idx <= pd.Timestamp(VINTAGE_PUB, tz="UTC")) + Lv0 = PV.Lente("vintage LORDO", PV.deluck(A_raw[mv]), True) + Lv1 = PV.Lente("vintage +FISCO", PV.deluck(A_raw[mv]), False) + Lv0.perp, Lv0.muro = PN.muro(Lv0.r, Lv0.prelievo, 0.0, 0.0) + Lv1.perp, Lv1.muro = PN.muro(Lv1.r, Lv1.prelievo, Lv1.aliq, Lv1.patr) + ok["R4"] = abs(Lv0.muro - 272_061.0) < 1.0 + ok["R5"] = abs(Lv1.muro - 258_338.0) < 1.0 + print(f" R4 muro L0 LORDO vintage {VINTAGE_PUB} ${Lv0.muro:>11,.0f} atteso $272,061 " + f"{'ESATTO AL DOLLARO' if ok['R4'] else 'DIVERSO'}") + print(f" R5 muro L1 +FISCO vintage {VINTAGE_PUB} ${Lv1.muro:>11,.0f} atteso $258,338 " + f"{'ESATTO AL DOLLARO' if ok['R5'] else 'DIVERSO'}") + + L3 = PV.Lente("L3 CONGIUNTA", PV.deluck(F_raw), False) + L3.perp, L3.muro = PV.muro_di(L3) + ok["R6"] = abs(L3.muro - 313_000.0) < 4_000.0 + print(f" R6 muro L3 CONGIUNTA (oggi) ${L3.muro:>11,.0f} atteso ~$313k " + f"{'RIPRODOTTO' if ok['R6'] else 'DIVERSO'} {clock()}") + + rng = np.random.default_rng(PV.SEED_TRAJ) + pv = CC._boot_paths(L3.r, 400, 5 * 365, PV.BLOCK, rng) + a1 = PN.accumula(pv, 500.0, L3.muro, L3.aliq, L3.patr, start=635.0) + a2 = accumula_costo(pv, 500.0, L3.muro, L3.aliq, L3.patr, 635.0, None) + d = float(np.max(np.abs(a1["cap"] - a2["cap"]))) + ok["R7"] = d == 0.0 + print(f" R7 `accumula_costo` con drag spento vs `PN.accumula` max|dif| = {d:.1e} " + f"{'BIT-EXACT' if ok['R7'] else 'FALLITA'}") + + import r0823_skeptic_spot as SS # noqa: E402 + synth = dict(bid=100.0, ask=100.1, bids=[[100.0, 3.0], [99.0, 50.0]], + asks=[[100.1, 3.0], [101.0, 50.0]]) + same = all( + (walk_cost_bps(synth, u, sd) is None) == (SS.walk_cost_bps(synth, u, sd) is None) + and (walk_cost_bps(synth, u, sd) is None + or abs(walk_cost_bps(synth, u, sd) - SS.walk_cost_bps(synth, u, sd)) < 1e-12) + for u in (10.0, 300.0, 2_000.0, 1e9) for sd in ("buy", "sell")) + ok["R8"] = same + print(f" R8 la mia `walk_cost_bps` == quella di §52 (libro sintetico) " + f"{'IDENTICHE' if same else 'DIVERGONO'}") + print(f"\n ESITO: {sum(ok.values())}/{len(ok)} — " + + ", ".join(f"{k}={'OK' if v else 'NO'}" for k, v in ok.items())) + if not all(ok.values()): + raise SystemExit("REPLICA INCOMPLETA: un numero nuovo sarebbe indistinguibile da un bug.") + + big = libro(635.0, rate, CurvaFissa(200.0)) + m0, mB = metrics(bZ), metrics(big) + morde = mB["sharpe"] < m0["sharpe"] - 0.5 + print("\n CONTROLLO POSITIVO del modello di costo — a 200 bps per ordine il libro DEVE") + print(f" morire: Sharpe {m0['sharpe']:.3f} -> {mB['sharpe']:.3f}, " + f"CAGR {m0['cagr']:+.2%} -> {mB['cagr']:+.2%}. " + f"{'MORDE' if morde else '⚠️ NON MORDE: modello sospetto'}") + if not morde: + raise SystemExit("il modello di costo non morde: indistinguibile da uno rotto.") + + # ===================================================================== + sez("4) IL COSTO A $635 — deve riprodurre §5, o la calibrazione e' sbagliata") + o635 = ordini_cache[635.0] + c635 = np.array([cur.bps("BTC", x) for x in o635]) + sk635 = 635.0 * W_SKH / 2 + print(f" [CALC] TP01 a $635: ordine mediano ${np.median(o635):,.0f} -> " + f"{np.median(c635):.4f} bps; ordine massimo ${o635.max():,.0f} -> " + f"{cur.bps('BTC', o635.max()):.4f} bps.") + print(f" [CALC] SKH01 a $635: ordine ${sk635:,.0f} -> {cur.bps('BTC', sk635):.4f} bps.") + b635 = libro(635.0, rate, cur) + print(f" [CALC] drift del libro: {bZ.mean()*DPY:+.4%} senza costo -> " + f"{b635.mean()*DPY:+.4%} col costo = {(b635.mean()-bZ.mean())*DPY*1e4:+.2f} bps/anno.") + if FILLS.exists(): + f = pd.read_csv(FILLS) + sl = f["slip_B5"].astype(float).abs() + print(f" [LOG] i 18 fill reali di §5: |slippage| mediana {sl.median():.2f} bps, " + f"p90 {sl.quantile(0.9):.2f} bps, nozionale mediano ${f['notional'].median():,.0f}.") + print(f" 📌 Il costo MODELLATO ({np.median(c635):.4f} bps) sta " + f"{sl.median()/max(np.median(c635),1e-9):,.0f}x SOTTO la dispersione misurata sui") + print(" fill veri: a questa taglia l'impatto non e' misurabile nemmeno in linea di") + print(" principio, ed e' esattamente cio' che §5 concluse («estremi avversi 1/18") + print(" contro 4,7 attesi»). La calibrazione riproduce il fatto, non lo assume.") + + if all(p.exists() for p in LIN5M.values()): + print("\n ⚠️ §53 SU QUESTO PUNTO: «un fill al 21,9% del volume della sua barra 5m a $635,") + print(" con partecipazione lineare 1,25x lo porta al 27,4% SUBITO».") + print(" Misurato sul feed LINEARE USDC — quello che il libro trada davvero:") + print(f" {'asset':>6}{'vol 5m med. (>0)':>20}{'libro a 1bps':>15}{'libro a 20bps':>15}" + f"{'ordine $635':>13}{'quota del libro 1bps':>22}") + rap, rap20 = [], [] + for a in ASSETS: + d5 = pd.read_parquet(LIN5M[a]) + v = (d5["volume"] * d5["close"]).values + vm = float(np.median(v[v > 0])) + rap.append(PROF[1.0][a] / vm) + rap20.append(PROF[20.0][a] / vm) + print(f" {a:>6}{vm:>20,.0f}{PROF[1.0][a]:>15,.0f}{PROF[20.0][a]:>15,.0f}" + f"{sk635:>13,.0f}{sk635/PROF[1.0][a]:>21.2%}") + print(" 📌 La partecipazione a una barra 5m NON e' un costo: e' la quota del VOLUME") + print(" SCAMBIATO in cinque minuti. Il costo lo decide la liquidita' RESTANTE nel") + print(f" libro: entro 1 bps vale {rap[0]:.1f}x (BTC) / {rap[1]:.1f}x (ETH) quel") + print(f" volume, entro 20 bps {rap20[0]:.0f}x / {rap20[1]:.0f}x.") + print(f" L'ordine di OGGI e' lo {sk635/PROF[1.0]['BTC']:.2%} (BTC) / " + f"{sk635/PROF[1.0]['ETH']:.2%} (ETH) della profondita' a 1 bps;") + print(f" a 1,25x diventa lo {1.25*sk635/PROF[1.0]['BTC']:.2%} / " + f"{1.25*sk635/PROF[1.0]['ETH']:.2%}. L'allarme di §53 e' una DOMANDA giusta") + print(" con una grandezza sbagliata: il 21,9% era la quota di una barra a volume") + print(" BASSO, e il volume di una barra non e' la liquidita' disponibile.") + + # ===================================================================== + sez("5) IL LIBRO ALLA TAGLIA — drift, vol, Sharpe e capacita' come funzione del capitale") + print(" [ASSUNTO] Il libro d'ordini di OGGI proiettato su 2019-2026. Nel 2019 la") + print(" profondita' di Deribit era una frazione di quella odierna -> ottimista sul passato,") + print(" onesto sul futuro (ed e' il futuro che il muro deve attraversare).") + print(f"\n {'capitale':>15}{'drift':>10}{'vol':>9}{'Sharpe':>9}{'costo/anno':>12}" + f"{'di cui TP01':>13}{'di cui SKH01':>14}{'noz. fuori libro':>18}") + D, VOLS, SH = {}, {}, {} + for C in CAPS: + s = libro(C, rate, cur) + stp, _ = tp01_costo(C * W_TP, rate, cur) + ssk = skh_costo(C * W_SKH, rate, cur) + stp0, _ = tp01_costo(C * W_TP, rate, Z) + ssk0 = skh_costo(C * W_SKH, rate, Z) + b0 = libro(C, rate, Z) + D[C] = float(s.mean() * DPY) + VOLS[C] = float(s.std() * np.sqrt(DPY)) + SH[C] = metrics(s)["sharpe"] + ct = float((stp0.mean() - stp.mean()) * DPY) + cs = float((ssk0.mean() - ssk.mean()) * DPY) + tot = float((b0.mean() - s.mean()) * DPY) + fq, _noz = quota_oltre(C, cur, rate) + print(f" ${C:>14,.0f}{D[C]:>10.2%}{VOLS[C]:>9.2%}{SH[C]:>9.3f}{tot:>12.3%}" + f"{ct:>13.3%}{cs:>14.3%}{fq:>17.1%}") + print(f" {clock()}") + print("\n 📌 La vol e' invariata su tutta la colonna finche' il costo non esplode: il costo") + print(" d'esecuzione e' una tassa sul DRIFT, non sul rischio — stessa forma del funding") + print(" (§35). E' cio' che autorizza il modello a un grado di liberta' della sezione 8.") + print(f" 📌 TROVATO PER STRADA: il drift a $635 ({D[635.0]:.2%}) e' PIU' BASSO che a $5.000") + print(f" ({D[5000.0]:.2%}) e il costo d'esecuzione non c'entra (0,00x%). E' il PAVIMENTO") + print(" `min_order` $5: a $635 un ribilanciamento su tre non si esegue, e la posizione") + print(" insegue il target peggio. Il capitale piccolo ha un suo costo, di segno opposto") + print(" a quello dello slippage — e nessuna tabella pubblicata lo distingue dall'altro.") + + # ===================================================================== + sez("6) IL MURO COME PUNTO FISSO, COL COSTO DENTRO") + print(""" Struttura da `r0726_wall_fixedpoint` (importato): serve capitale C per girare il + libro che determina il muro C, quindi C_{n+1} = muro(libro(C_n)). Con un costo FUNZIONE di C + l'iterazione ha un contenuto nuovo: il libro non cambia solo di composizione, cambia di + RENDIMENTO. Lente L3 CONGIUNTA (funding + fisco d'accumulo), de-luck x0,89.""") + inc = WF.included_at(L3.muro) + print("\n [struttura] `WF.included_at` alla taglia del muro direbbe " + + ", ".join(f"{k} {v:.0%}" for k, v in inc.items())) + print(" ⚠️ ma quella e' la composizione DEPLOYABLE del 26/07: GTAA01 e' fuori dallo stesso") + print(" 26/07 (PRIIPs) e la soglia $20k di XS01 e' caduta (§5-bis). Il libro che GIRA e") + print(" resta TP01+SKH01, ed e' quello su cui tutti i muri pubblicati sono calcolati:") + print(" qui cambia il COSTO alla taglia, non la composizione.") + print(f"\n {'iter':>5}{'ipotesi':>15}{'drift':>9}{'Sharpe':>9}{'perpetua':>10}{'-> muro':>15}") + Cn = L3.muro + for i in range(6): + s = libro(Cn, rate, cur) + L = PV.Lente("Ln", PV.deluck(s.values.astype(float)), False) + L.perp, L.muro = PV.muro_di(L) + print(f" {i:>5}{'$'+format(Cn,',.0f'):>15}{L.drift:>9.2%}" + f"{metrics(s)['sharpe']:>9.3f}{L.perp:>10.2%}{'$'+format(L.muro,',.0f'):>15}") + conv = abs(L.muro - Cn) / max(Cn, 1) < 0.02 + Cn = L.muro + if conv: + print(f"\n CONVERGE a ${Cn:,.0f} (variazione < 2%) {clock()}") + break + else: + print(f"\n non converge in 6 iterazioni, ultimo valore ${Cn:,.0f}") + muro_endo = Cn + Lf = PV.Lente("Lf", PV.deluck(libro(muro_endo, rate, cur).values.astype(float)), False) + Lf.perp, Lf.muro = PV.muro_di(Lf) + print(f"\n {'lente':<56}{'perpetua':>11}{'muro':>14}") + print(f" {'L3 CONGIUNTA, costo COSTANTE (pubblicato §36)':<56}{L3.perp:>11.2%}" + f"{'$'+format(L3.muro,',.0f'):>14}") + print(f" {'L3 + costo ENDOGENO alla taglia (punto fisso)':<56}{Lf.perp:>11.2%}" + f"{'$'+format(Lf.muro,',.0f'):>14}") + print(f"\n >>> il muro si sposta di {Lf.muro/L3.muro-1:+.1%} rispetto al numero pubblicato") + print(" ⚠️ La bisezione del muro e' un Monte Carlo a 4.000 percorsi: la sua risoluzione e'") + print(" ~1% (§36). Uno spostamento di questa taglia va letto come NULLO, non come +0,5%.") + + # ===================================================================== + sez("7) IL PUNTO DI SATURAZIONE — dove il costo marginale mangia il rendimento marginale") + print(""" Definizione: il profitto annuo in DOLLARI e' P(C) = C x drift(C). Il capitale + marginale smette di comprare reddito all'argmax di P (dP/dC = 0). E' il vero «quanto puo' + crescere questo libro», e il progetto non l'aveva mai calcolato. + ⚠️ La camminata assume l'ordine INTERO eseguito nell'istante contro il libro VISIBILE: e' + cio' che fa l'esecutore di OGGI (ordini market, nessuno slicing). Il numero e' percio' una + proprieta' dell'ESECUTORE, non del mercato — affettare l'ordine lo sposterebbe in alto.""") + print("\n (a) LIMITE DURO DI CAPACITA' — puramente [VENUE]+[BACKTEST], nessuna estrapolazione:") + print(f" {'capitale':>15}{'quota del nozionale annuo NON eseguibile in un istante':>58}") + Chard = None + for C in 10.0 ** np.arange(6.0, 8.01, 0.25): + fq, _ = quota_oltre(float(C), cur, rate) + if Chard is None and fq >= 0.01: + Chard = float(C) + print(f" ${C:>14,.0f}{fq:>57.1%}") + print(f" -> prima taglia con >=1% del nozionale fuori dal libro visibile: " + f"${Chard:,.0f}" if Chard else " -> mai, entro $100M") + print("\n (b) SATURAZIONE ECONOMICA — argmax di C x drift(C). Le celle oltre `lim` sono") + print(" [DEDOTTO] (pendenza log-log floor 1.0): la parte destra della curva dipende da") + print(" quell'assunzione, la SINISTRA no.") + grid = 10.0 ** np.arange(np.log10(3e5), 8.01, 0.05) + d0 = float(bZ.mean() * DPY) + for nome, qq in (("mediana", 0.50), ("p10 (libro migliore)", 0.10), ("p90 (peggiore)", 0.90)): + P, dd = [], [] + for C in grid: + s = libro(float(C), rate, cur, q=qq) + d = float(s.mean() * DPY) + dd.append(d) + P.append(C * d) + P, dd = np.asarray(P), np.asarray(dd) + j = int(np.argmax(P)) + c10 = grid[int(np.argmax(dd < 0.9 * d0))] if (dd < 0.9 * d0).any() else float("nan") + c50 = grid[int(np.argmax(dd < 0.5 * d0))] if (dd < 0.5 * d0).any() else float("nan") + fq, _ = quota_oltre(float(grid[j]), cur, rate) + print(f"\n curva {nome:<21} C* = ${grid[j]:>13,.0f} P(C*) = " + f"${P[j]:>11,.0f}/anno drift li' {dd[j]:.2%}") + print(f" {'':<27} costo = 10% del drift a ${c10:>13,.0f} 50% a ${c50:>13,.0f}") + print(f" {'':<27} a C* il {fq:.0%} del nozionale e' gia' fuori dal libro visibile") + print(f"\n {clock()}") + print(f" 📌 Il muro sta a ${Lf.muro:,.0f}. La saturazione e' PIU' DI UN ORDINE DI GRANDEZZA") + print(" sopra: il vincolo del piano non e' la capacita' del libro.") + print(" 📌 E le due definizioni CONCORDANO — argmax economico e primo 1% di nozionale non") + print(" eseguibile cadono nella stessa decade. Non e' un ottimo liscio: la saturazione") + print(" E' la fine del libro visibile.") + o_big = 1.25e6 + print(f"\n [DEDOTTO] E la cura e' nota e costa poco: un ordine da ${o_big:,.0f} costa " + f"{cur.bps('BTC', o_big):.1f} bps in un") + print(f" istante, ma spezzato in 10 tranche da ${o_big/10:,.0f} ne costa " + f"{cur.bps('BTC', o_big/10):.1f} l'una — {cur.bps('BTC', o_big)/max(cur.bps('BTC', o_big/10),1e-9):.0f}x") + print(" meno, SE il libro si ricostituisce fra una tranche e l'altra ([ASSUNTO]: non e'") + print(" misurato qui). Cioe' la capacita' di questo libro e' limitata dall'ESECUTORE, e la") + print(" riparazione e' una riga di codice — che pero' serve solo a ~$10M.") + + print("\n (c) CHI SATURA PER PRIMO (costo annuo per gamba, in punti di drift di LIBRO):") + print(f" {'capitale':>15}{'TP01':>10}{'SKH01':>10}{'ordine SKH':>15}" + f"{'bps su quello':>15}{'':>4}") + for C in (1e6, 3e6, 1e7, 3e7, 1e8): + stp, _ = tp01_costo(C * W_TP, rate, cur) + stp0, _ = tp01_costo(C * W_TP, rate, Z) + ssk, ssk0 = skh_costo(C * W_SKH, rate, cur), skh_costo(C * W_SKH, rate, Z) + o = C * W_SKH / 2 + flag = " [DED]" if cur.oltre("ETH", o) else "" + print(f" ${C:>14,.0f}{float((stp0.mean()-stp.mean())*DPY):>10.2%}" + f"{float((ssk0.mean()-ssk.mean())*DPY):>10.2%}{o:>15,.0f}" + f"{cur.bps('BTC', o):>15,.1f}{flag:>6}") + print(" 📌 SKH01 satura PRIMA di TP01 (ordine 100x piu' grande) ma TP01 lo SUPERA") + print(" subito dopo, perche' la sua CODA di ordini (max ~0,38 x capitale) e' ancora") + print(" piu' grande: due meccanismi diversi che mordono a due decadi diverse.") + + # ===================================================================== + sez("8) LA TRAIETTORIA COL COSTO LUNGO IL CAMMINO") + print(""" Attesa a priori del coordinatore, dichiarata prima di misurare: «poco, perche' il + grosso del tempo si passa a capitale piccolo dove il costo e' basso — ma il numero conta, + perche' se il muro sale mentre il cammino rallenta i due effetti si SOMMANO». + Modello: i percorsi si estraggono dal libro a $635 (quello di oggi) e ogni giorno si sottrae + `drift($635) - drift(cap_t)`, interpolato in log(capitale) sulla colonna della sezione 5.""") + caps_v = np.array(sorted(D)) + dr_v = np.array([D[c] for c in caps_v]) + + def mk_drag(dmap: dict): + cv = np.array(sorted(dmap)) + dv = np.array([dmap[c] for c in cv]) + + def _f(cap: np.ndarray) -> np.ndarray: + d = np.interp(np.log10(np.maximum(cap, 1.0)), np.log10(cv), dv) + return (dmap[635.0] - d) / DPY + return _f + + drag = mk_drag(D) + print("\n ⚠️ Il drag e' `D($635) - D(cap)` e contiene DUE effetti di segno opposto: il") + print(" pavimento min_order (che penalizza il capitale PICCOLO) e lo slippage (che") + print(" penalizza il GRANDE). Lungo il cammino cambia segno:") + print(f" {'capitale':>14}{'drag (in punti di drift annuo)':>36}") + for c in (2_000.0, 20_000.0, 100_000.0, 315_000.0, 1_000_000.0, 3_000_000.0): + print(f" ${c:>13,.0f}{drag(np.array([c]))[0]*DPY:>35.3%}") + print(" 📌 Per quasi tutto il tragitto il costo d'esecuzione e' NEGATIVO: uscire dal") + print(" pavimento min_order vale piu' di quanto costi lo slippage fino a ~$300k.") + base = libro(635.0, rate, cur) + Lb = PV.Lente("Lb", PV.deluck(base.values.astype(float)), False) + rng = np.random.default_rng(PV.SEED_TRAJ) + paths = CC._boot_paths(Lb.r, PV.N_TRAJ, PV.ANNI_TRAJ * 365, PV.BLOCK, rng) + print(f"\n {'EUR/mese':>9}{' costo COSTANTE (bersaglio $'+format(L3.muro,',.0f')+')':>40}" + f"{' costo ENDOGENO (bersaglio $'+format(Lf.muro,',.0f')+')':>40}") + print(f" {'':>9}{'mediana':>22}{'P(20a)':>18}{'mediana':>22}{'P(20a)':>18}") + for dep in (250.0, 500.0, 800.0): + a = PV.leggi(PN.accumula(paths, dep, L3.muro, Lb.aliq, Lb.patr, start=635.0)["colpito"]) + b = PV.leggi(accumula_costo(paths, dep, Lf.muro, Lb.aliq, Lb.patr, 635.0, + drag)["colpito"]) + print(f" {dep:>9,.0f}{PV.fmt_cond(a):>22}{a['p20']:>17.1%}" + f"{PV.fmt_cond(b):>22}{b['p20']:>17.1%}") + print(f" {clock()}") + print("\n Scomposizione a EUR500/mese (un grado di liberta' per volta):") + a0 = PV.leggi(PN.accumula(paths, 500.0, L3.muro, Lb.aliq, Lb.patr, start=635.0)["colpito"]) + a1 = PV.leggi(PN.accumula(paths, 500.0, Lf.muro, Lb.aliq, Lb.patr, start=635.0)["colpito"]) + a2 = PV.leggi(accumula_costo(paths, 500.0, Lf.muro, Lb.aliq, Lb.patr, 635.0, + drag)["colpito"]) + print(f" (i) base pubblicata {PV.fmt_cond(a0):>7} " + f"P(20a) {a0['p20']:.1%}") + print(f" (ii) solo il BERSAGLIO piu' alto {PV.fmt_cond(a1):>7} " + f"P(20a) {a1['p20']:.1%}") + print(f" (iii) + il CAMMINO piu' lento {PV.fmt_cond(a2):>7} " + f"P(20a) {a2['p20']:.1%}") + # controllo positivo del drag — e un errore MIO, catturato dal controllo stesso + def drag_finto(cap: np.ndarray) -> np.ndarray: + return np.where(cap > 100_000.0, 0.03 / DPY, 0.0) + + ab = PV.leggi(accumula_costo(paths, 500.0, Lf.muro, Lb.aliq, Lb.patr, 635.0, + drag_finto)["colpito"]) + mosso = abs(ab["p20"] - a2["p20"]) > 0.02 + print("\n CONTROLLO POSITIVO del drag — con un drag finto di 3%/anno sopra $100k la") + print(f" traiettoria DEVE muoversi: {PV.fmt_cond(ab):>6} P(20a) {ab['p20']:.1%} " + f"{'SI MUOVE' if mosso else '⚠️ NON SI MUOVE: drag sospetto'}") + if not mosso: + raise SystemExit("il drag non morde nemmeno a 3%/anno: macchina sospetta.") + Dfissa = {C: float(libro(C, rate, CurvaFissa(20.0)).mean() * DPY) for C in CAPS} + df20 = mk_drag(Dfissa) + print(f""" + ⚠️ ERRORE MIO, catturato da questo stesso controllo. La prima stesura usava come controllo + positivo un COSTO COSTANTE di 20 bps per ordine — e non muoveva niente: il drag di questa + sezione e' una DIFFERENZA, `D($635) - D(cap)`, e un costo che non dipende dalla taglia + abbassa i due termini della stessa quantita' e sparisce nella sottrazione. Misurato: + drag a $1M con costo costante 20 bps = {df20(np.array([1e6]))[0]*DPY:+.4%}/anno contro + {drag(np.array([1e6]))[0]*DPY:+.4%}/anno col costo vero. E' la stessa forma del null + degenere di §5-bis («il null del de-levering e' degenere quando la variante e' una + ri-scalatura»): un controllo puo' fallire perche' e' costruito male, non perche' la + macchina e' rotta — e distinguere le due cose e' il controllo.""") + + # ===================================================================== + sez("9) IL GRADINO DI LEVA COL COSTO ENDOGENO") + print(""" §53 lo lascio' aperto proprio qui: a 1,25x il nozionale sale SUBITO, quindi il + costo si paga SUBITO. La scala moltiplica il TARGET (SPEC-scale-key, §38), quindi moltiplica + anche la taglia di OGNI ordine: `costo_bps(k x nozionale)`, non `k x costo_bps(nozionale)`.""") + print(""" + ⚠️ CONVENZIONE, e non e' un dettaglio. Il `k*` pubblicato dal progetto (§35: 13,7 senza + de-luck, 12,25 con) e' calcolato da `FD.k_star` come argmax di `mean(log(1 + k*r))` sulla + serie di libro. RICOSTRUIRE il libro a scala k da' un numero DIVERSO e piu' alto, perche' i + due sleeve hanno clamp interni (`np.clip(net,-0.99)` in TP01, `max(capital,1.0)` in SKH01) + che a leva alta impediscono la rovina che il calcolo pubblicato invece conta. Per restare + comparabile, qui il costo endogeno entra come UN SOLO grado di liberta' sulla serie + pubblicata: `g(k) = g_exact(deluck(r_libro) - drag(k)/k, k)`, dove `drag(k)` e' il costo + d'esecuzione MISURATO alla taglia d'ordine k x nozionale.""") + KS = (1.0, 1.25, 1.5, 2.0, 3.0, 5.0) + KGRID = np.arange(0.5, 30.01, 0.25) + kstar_ctrl = None + KSTAR = {} + for cap_test, etichetta in ((635.0, "a $635 (il conto di oggi)"), + (float(Lf.muro), "alla taglia del muro"), + (5_000_000.0, "a $5M (dove il costo comincia a mordere)")): + print(f"\n --- {etichetta} ---") + r_base = PV.deluck(libro(cap_test, rate, Z).values.astype(float)) + + def gk(k: float) -> tuple[float, float]: + sk = libro(cap_test, rate, cur, scala=k) + s0k = libro(cap_test, rate, Z, scala=k) + dr = float(s0k.mean() - sk.mean()) # drag giornaliero alla scala k + return FD.g_exact(r_base - dr / k, k), dr * DPY + + print(f" {'k':>6}{'g(k) senza costo':>19}{'g(k) col costo':>17}{'costo/anno':>13}" + f"{'ordine SKH':>14}") + for k in KS: + g1, ca = gk(k) + g0 = FD.g_exact(r_base, k) + print(f" {k:>6.2f}{g0:>18.2%}{g1:>17.2%}{ca:>13.3%}" + f"{cap_test * W_SKH / 2 * k:>14,.0f}") + g0v = np.array([FD.g_exact(r_base, float(k)) for k in KGRID]) + g1v = np.array([gk(float(k))[0] for k in KGRID]) + k0 = float(KGRID[int(np.argmax(g0v))]) + k1 = float(KGRID[int(np.argmax(g1v))]) + KSTAR[etichetta] = (k0, k1) + if kstar_ctrl is None: + kstar_ctrl = k0 + print(f" k* con costo ZERO {k0:.2f}x -> k* con costo ENDOGENO {k1:.2f}x" + f" (mezzo-Kelly {k1/2:.2f}x) perdita {k0-k1:.2f}x") + kref = FD.k_star(PV.deluck(FD.book_75_25(rate).values.astype(float)))[0] + print(f"\n R9 controllo di replica: k* a costo ZERO su $635 = {kstar_ctrl:.2f}x contro " + f"{kref:.2f}x") + print(f" di `FD.k_star` sul libro pubblicato (alloc $600) " + f"{'RIPRODOTTO' if abs(kstar_ctrl - kref) <= 0.5 else '⚠️ DIVERGE'}") + print(f" {clock()}") + print("\n 📌 Il gradino 1,00x -> 1,25x costa, in costo d'esecuzione aggiuntivo, meno di un") + print(" punto base l'anno al conto di oggi. §53 chiedeva di misurarlo PRIMA: misurato,") + print(" non morde. Cio' che resta a bloccare il gradino sono le riserve gia' scritte") + print(" (drift stimato su 7,4 anni, coda assente dal dataset, chiave di scala assente") + print(" dalla config), NON il costo d'esecuzione.") + + # ===================================================================== + sez("VERDETTO") + kz, ke = KSTAR["alla taglia del muro"] + k5 = KSTAR["a $5M (dove il costo comincia a mordere)"][1] + print(f""" + [VENUE] La curva costo-taglia del perpetual USDC e' PIATTA al fondo (mezzo spread + {hs['BTC']:.4f}/{hs['ETH']:.4f} bps = UN TICK) e resta sotto ~2 bps fino a mezzo milione di + dollari per ordine — misurata di domenica notte UTC, cioe' nella finestra piu' sottile della + settimana. Il libro live manda ordini di ${sk635:,.0f} oggi e di ${Lf.muro*W_SKH/2:,.0f} alla + taglia del muro: entrambi dentro la regione piatta. + + [CALC] Il muro passa da ${L3.muro:,.0f} (costo costante, §36) a ${Lf.muro:,.0f} col costo + endogeno = {Lf.muro/L3.muro-1:+.1%}, cioe' DENTRO la risoluzione Monte Carlo del muro stesso. + La traiettoria EUR500/mese non si muove (P(20a) {a0['p20']:.1%} -> {a2['p20']:.1%}). La + saturazione economica sta a ~$1x10^7 e il primo 1% di nozionale non eseguibile in un istante + a ${Chard:,.0f}: due definizioni indipendenti che cadono nella stessa decade, oltre un ordine + di grandezza sopra il muro. + + [CALC] Il gradino di leva sopravvive: k* passa da {kz:.2f}x a {ke:.2f}x alla taglia del muro + ({ke-kz:+.2f}x), e a $635 il costo aggiuntivo di 1,25x sta sotto il punto base l'anno. Il + costo endogeno diventa vincolante sulla leva solo a $5M, dove k* crolla a {k5:.2f}x. + + Il rischio che i due scettici avevano nominato e' REALE ma vive su un ALTRO STRUMENTO e a + un'ALTRA TAGLIA: e' lo SPOT (§52, spread 100-500x piu' largo) e sono i milioni, non le + centinaia di migliaia. Sul perpetual, a questo capitale, il costo d'esecuzione NON e' il + vincolo. + + E il fatto piu' utile e' quello trovato per strada: alla taglia di OGGI il costo d'esecuzione + che le tabelle pubblicate non contengono ha segno NEGATIVO. Non e' lo slippage a essere + sottostimato: e' il PAVIMENTO `min_order`, che a $635 costa {(D[5000.0]-D[635.0])*1e4:.0f} bps + di drift l'anno — circa {(D[5000.0]-D[635.0])/max(D[313000.0]-D[1000000.0],1e-9):.0f}x cio' + che lo slippage costa passando da $313k a $1M, e in direzione opposta. +""") + + +if __name__ == "__main__": + main()