Files
PythagorasGoal/scripts/research/r0823_costo_capitale.py
T
Adriano Dal Pastro 73207f79b9 research(costo-capitale): il costo d'esecuzione e' una funzione del capitale, e il muro non si muove — a $635 il vincolo e' il pavimento min_order, di segno opposto
§55 COSTO-CAPITALE. Tutti i muri pubblicati ($272k/$278k/$313k/$325k, traiettorie,
versamenti) sono calcolati con un costo d'esecuzione COSTANTE, mentre un muro che si
raggiunge accumulando attraversa tutte le taglie. Qui il costo diventa una funzione,
misurata camminando il libro Deribit vero.

[VENUE] 50 istantanee di BTC_USDC-PERPETUAL / ETH_USDC-PERPETUAL in 35 minuti (domenica
notte UTC = la finestra piu' sottile della settimana, quindi conservativa). Mezzo spread
0,0065 / 0,0207 bps = UN TICK; costo sotto ~2 bps fino a $500k per ordine; il libro
visibile finisce a $2,8M (BTC) / $1,2M (ETH).

[CALC] Muro L3 congiunta $313.143 -> $315.080 col costo endogeno (+0,6%, DENTRO la
risoluzione Monte Carlo del muro stesso, punto fisso convergente in una iterazione).
La traiettoria EUR500/mese non si muove (P(20a) 85,1% -> 84,7%). Saturazione a ~$9,5M
per argmax di C x drift(C) e a $10M per il primo 1% di nozionale non eseguibile in un
istante: due definizioni indipendenti nella stessa decade, oltre un ordine di grandezza
sopra il muro. Il gradino di leva sopravvive (k* 12,50x -> 12,25x alla taglia del muro;
morde solo a $5M, dove crolla a 1,75x).

Le due riserve degli scettici sono misurate e non mordono qui: il costo che si annullava
in §52 e' quello dello SPOT (spread 100-500x piu' largo), e la partecipazione del 21,9%
di §53 e' la quota di una barra 5m a volume basso — l'ordine di oggi e' lo 0,13% (BTC) /
0,43% (ETH) della profondita' a 1 bps.

Trovato per strada, ed e' il fatto piu' utile: alla taglia di OGGI il costo che le tabelle
non contengono ha segno NEGATIVO. Il drift a $635 e' 13 bps/anno PIU' BASSO che a $5.000
per il pavimento min_order, ~2x cio' che lo slippage costa fra $313k e $1M.

Repliche 8/8 (TP01/SKH01/libro bit-exact; $272.061 e $258.338 al dollaro; accumula
bit-exact; walk_cost_bps identica a §52) + R9 k* = 12,25x come FD.k_star, e due controlli
POSITIVI obbligatori. Due errori miei catturati e pubblicati: banda p10/p50/p90 che si
incrociava oltre il libro visibile (tre oggetti-curva separati), e un controllo positivo
DEGENERE per costruzione (un costo costante sparisce da un drag differenziale).

Sola lettura, nessun ordine, solo GET pubbliche. Libro, pesi, cron, config INVARIATI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M8Ncho6QV9FWLdyy4VyQf
2026-08-23 03:28:10 +00:00

1114 lines
59 KiB
Python

#!/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()