Files
PythagorasGoal/scripts/research/r0726_capwall_refresh.py
T
Adriano Dal Pastro 326229188a research: rischio di VENUE — mai prezzato, e non e' diversificabile dagli sleeve
Domanda dell'operatore: "tutto su Deribit?". Ha scoperto un buco, non un dettaglio.

IL BUCO. Il progetto ha prezzato con ossessione fee, slippage, min-order, pavimento IB,
haircut small-cap, fortuna d'ancora, degrado d'esecuzione, look-ahead, backfill, split
non aggiustati — e MAI la probabilita' che l'exchange sparisca col saldo dentro. E
TP01+SKH01+VRP01 stanno tutti sullo stesso conto: tre sleeve quasi-ortogonali sui
ritorni, PERFETTAMENTE CORRELATI sul fallimento del venue. La matrice di correlazione
del book non lo vede per costruzione.

LIMITE DEI MURI DEL 25-26/07 (dichiarato): book_series gira a alloc=$600 col book a 2
sleeve, quindi portarlo a $272k assumeva gia' "tutto su Deribit" senza dirlo — e
assumeva anche di girare il book da $600 a $272k (falso: a $3k GTAA01, $5k XSR01, $20k
XS01 -> il muro vero e' piu' basso).

LA MISURA (accumulo da $600, 250 EUR/m, 20a, bersaglio $272k, x0.89, jump di venue;
CONC = 100% Deribit vs SPLIT = Deribit 65 / HL 15 / IB 20; bersaglio identico ->
conservativo CONTRO lo split):

  p annua    P(arrivare) CONC / SPLIT    P(perso TUTTO) CONC / SPLIT
  0.5%              87% / 90%                    10% / 0%
  1.0%              81% / 83%                    18% / 0%
  2.0%              69% / 71%                    34% / 4%
  5.0%              42% / 45%                    64% / 27%

Capitale mediano CONC a 20a: $544k (p=0) -> $0 (p=5%).

LA COLONNA CHE CONTA NON E' LA PRIMA. Sulla probabilita' di ARRIVARE la concentrazione
costa 1-3pp; sulla ROVINA fino a 64pp. Motivo strutturale: con un conto solo "almeno un
fallimento" COINCIDE con "perso tutto". Lo SPLIT viene colpito 2.5x piu' spesso ed e'
molto piu' sicuro -> "quante volte vieni colpito" non e' una misura di rischio.

ONESTA': p NON e' stimato (sensibilita', non previsione); i fallimenti sono assunti
indipendenti, ottimistico per Deribit-HL -> la parte solida dello split e' IB, altra
classe di rischio; a $600 lo split e' impossibile, la concentrazione e' forzata.

RISPOSTA: no, ma la domanda ha una DATA. Prima soglia vera ~$3k (GTAA01 su IB). E
converge col 25/07: "max 25% su IB, COSTA ~0.08 EUR/g" era detto su basi di solo
rendimento; sull'asse della rovina quello stesso 25% e' la mossa principale — non e' il
prezzo di un peggioramento, e' il premio di un'assicurazione.

Incluse le aggiunte a r0726_capwall_refresh: drill-down 250 EUR/m e solve_deposit
(10 anni @P=90% = 1.178 EUR/m, $155.923 versati su $272k -> il rendimento fa il 43%,
contro il 77% a 20 anni). Bug catturato: il contatore dei versamenti si congelava al
traguardo mentre il capitale continuava a riceverli -> rapporto gonfiato (46x vs 25x a
30 anni); test di regressione cablato.

Book, pesi, cron, config INVARIATI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 17:57:53 +00:00

335 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python
"""r0726_capwall_refresh.py — i MURI DI CAPITALE ricalcolati col fattore de-luck corretto.
Il 25/07 (`r0725_capcurve.py`) ha calcolato rendita perpetua, muro di capitale per 50 EUR/g e
traiettoria da $600 applicando un de-luck **×0.6 scelto a occhio** sul drift del book.
Il 26/07 quel fattore e' stato MISURATO e decomposto (`r0726_deluck_factor.py` +
`r0726_skh_live_book.py`):
* fortuna d'ancora sul drift, book live Deribit (TP01/SKH01 75/25): **×0.890**
(24 x 23 = 552 configurazioni, estrazioni congiunte);
* degradazione del path live: **non-negativa su tutto cio' che e' misurato**
(SKH01 uscite +0.081 Sharpe di book su 23/23 offset; SKH01 ingressi +0.73pp di drift,
positivo nel 100% delle estrazioni; TP01 barra parziale trascurabile su 24 ancore).
Il ×0.6 implicava quindi un residuo ×0.687 attribuito al live OLTRE l'ancora, che nessuna misura
sostiene: era il doppio conteggio gia' sospettato in CLAUDE.md, ora quantificato.
Questo script rifa' i numeri della DECISIONE ai tre fattori, sulla stessa macchineria del 25/07
(nessuna re-implementazione: `book_series`, `survival`, `_boot_paths` importati da li').
uv run python scripts/research/r0726_capwall_refresh.py
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "scripts" / "research"))
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
import r0725_capcurve as CC # noqa: E402
# fattori a confronto. 0.890 = fortuna d'ancora MISURATA sul book live Deribit; il vero fattore
# onesto e' >= 0.890 perche' la componente live e' non-negativa (non la conto: e' conservativo).
FACTORS = (
(0.60, "×0.60 (25/07, a occhio)"),
(0.89, "×0.89 (26/07, ancora MISURATA)"),
(1.00, "×1.00 (nessun haircut)"),
)
LEVERAGES = (1.0, 1.5)
N_PATHS = 2000
def perp_and_wall(r: np.ndarray, k: float, gross_needed: float) -> tuple[float, float, float]:
"""(SWR-20a, tasso di rendita PERPETUA, capitale necessario) — stessa ricerca binaria
del 25/07: perpetua = prelievo con P(cap a 20a >= cap iniziale) >= 90%."""
lo, hi = 0.0, 0.40
for _ in range(13):
mid = (lo + hi) / 2
s = CC.survival(r, 1e6, 1e6 * mid, 20, k, n_paths=N_PATHS)
if s["p10_end"] >= 1e6:
lo = mid
else:
hi = mid
perp = lo
lo2, hi2 = 0.0, 0.40
for _ in range(13):
mid = (lo2 + hi2) / 2
if CC.survival(r, 1e6, 1e6 * mid, 20, k, n_paths=N_PATHS)["p_survive"] >= 0.95:
lo2 = mid
else:
hi2 = mid
cap = gross_needed / perp if perp > 0.002 else float("inf")
return lo2, perp, cap
def trajectory(r: np.ndarray, cap_needed: float, n_paths: int = 3000,
years: int = 25, seed: int = 725) -> dict:
"""Da $600 al capitale-rendita, stocastico (block bootstrap): stessa logica di
r0725_capcurve.part_d, parametrizzata sul fattore e sul bersaglio."""
rng = np.random.default_rng(seed)
n_days = 365 * years
paths = CC._boot_paths(r, n_paths, n_days, 20, rng)
out = {}
for dep_eur in (0, 250, 500, 1_000, 2_000):
dep = dep_eur * CC.EURUSD
cap = np.full(n_paths, 600.0)
hit = np.full(n_paths, -1, int)
for t in range(n_days):
cap = cap * (1.0 + paths[:, t])
if t % 30 == 0 and t > 0:
cap += dep
newly = (hit < 0) & (cap >= cap_needed)
hit[newly] = t
yrs = np.where(hit >= 0, hit / 365.0, np.nan)
fin = yrs[~np.isnan(yrs)]
out[dep_eur] = dict(
p50=float(np.percentile(fin, 50)) if len(fin) > 100 else float("nan"),
p10=float(np.percentile(fin, 10)) if len(fin) > 100 else float("nan"),
p20y=float((yrs <= 20).sum() / len(yrs)))
return out
def focus(r: np.ndarray, cap_needed: float, dep_eur: float, n_paths: int = 5000,
years: int = 30, seed: int = 726) -> dict:
"""Drill-down su UN livello di deposito: percentili del tempo, probabilita' cumulate,
capitale raggiunto per anno, e quanto del capitale finale e' DEPOSITO vs rendimento."""
rng = np.random.default_rng(seed)
n_days = 365 * years
paths = CC._boot_paths(r, n_paths, n_days, 20, rng)
dep = dep_eur * CC.EURUSD
cap = np.full(n_paths, 600.0)
hit = np.full(n_paths, -1, int)
paid = np.full(n_paths, 600.0) # capitale immesso (iniziale + depositi)
paid_at_hit = np.full(n_paths, np.nan)
snaps, snap_paid = {}, {}
for t in range(n_days):
cap = cap * (1.0 + paths[:, t])
if t % 30 == 0 and t > 0:
cap += dep
# ⚠️ il contatore deve seguire il capitale: `cap` riceve il deposito SEMPRE, quindi
# anche `paid` accumula sempre. Congelarlo al traguardo (come faceva la prima
# stesura) rende il rapporto capitale/versato gonfiato dopo il traguardo, perche'
# il capitale conterrebbe depositi che il contatore non conta. Il valore AL
# traguardo si cattura a parte, con lo snapshot `paid_at_hit`.
paid = paid + dep
newly = (hit < 0) & (cap >= cap_needed)
hit[newly] = t
paid_at_hit[newly] = paid[newly]
if (t + 1) % 365 == 0:
snaps[(t + 1) // 365] = cap.copy()
snap_paid[(t + 1) // 365] = paid.copy()
yrs = np.where(hit >= 0, hit / 365.0, np.nan)
fin = yrs[~np.isnan(yrs)]
return dict(yrs=yrs, fin=fin, snaps=snaps, snap_paid=snap_paid,
paid_at_hit=paid_at_hit[~np.isnan(paid_at_hit)], n=n_paths)
def _p_hit(paths: np.ndarray, cap_needed: float, dep_usd: float, n_days: int) -> float:
"""P(il capitale tocca `cap_needed` entro n_days) partendo da $600 con deposito mensile."""
cap = np.full(paths.shape[0], 600.0)
hit = np.zeros(paths.shape[0], bool)
for t in range(n_days):
cap = cap * (1.0 + paths[:, t])
if t % 30 == 0 and t > 0:
cap += dep_usd
hit |= cap >= cap_needed
return float(hit.mean())
def solve_deposit(r: np.ndarray, cap_needed: float, years: int, p_target: float,
n_paths: int = 4000, seed: int = 726) -> tuple[float, float]:
"""Deposito mensile (EUR) perche' P(arrivare entro `years`) >= p_target.
Il bootstrap si estrae UNA VOLTA e si riusa a ogni passo della ricerca binaria: con
campioni diversi per passo la funzione non sarebbe monotona e la bisezione oscillerebbe.
"""
rng = np.random.default_rng(seed)
n_days = 365 * years
paths = CC._boot_paths(r, n_paths, n_days, 20, rng)
lo, hi = 0.0, 20_000.0
if _p_hit(paths, cap_needed, hi * CC.EURUSD, n_days) < p_target:
return float("nan"), float("nan")
for _ in range(16):
mid = (lo + hi) / 2
if _p_hit(paths, cap_needed, mid * CC.EURUSD, n_days) >= p_target:
hi = mid
else:
lo = mid
dep = hi
versato = 600.0 + dep * CC.EURUSD * (years * 365 // 30)
return dep, versato
def main() -> None:
print("=" * 100)
print(" r0726 — MURI DI CAPITALE col fattore de-luck MISURATO (era ×0.6 a occhio)")
print("=" * 100)
gross = CC.TARGET_EUR_DAY * 365 * CC.EURUSD / (1 - CC.TAX_RATE)
print(f" bersaglio: {CC.TARGET_EUR_DAY:.0f} EUR/g netti = EUR "
f"{CC.TARGET_EUR_DAY*365:,.0f}/anno netti = ${gross:,.0f}/anno LORDI "
f"(fisco {CC.TAX_RATE:.0%}, EURUSD {CC.EURUSD})")
print(" book: TP01+SKH01 75/25 su path live (lente 'hourly'), costi reali Deribit\n")
base = CC.book_series("hourly")
mu = float(base.values.mean())
print(f" {'fattore':<32}{'leva':>6}{'SWR-20a':>10}{'PERPETUA':>10}{'capitale':>16}")
print(" " + "-" * 74)
res = {}
for f, lab in FACTORS:
r = (base - (1.0 - f) * mu).values.astype(float)
for k in LEVERAGES:
swr, perp, cap = perp_and_wall(r, k, gross)
res[(f, k)] = (swr, perp, cap)
capf = f"${cap:,.0f}" if np.isfinite(cap) else "IRRAGGIUNGIBILE"
print(f" {lab:<32}{k:>6.2f}{swr:>9.2%}{perp:>10.2%}{capf:>16}")
print()
print("=" * 100)
print(" COSA CAMBIA NELLA DECISIONE")
print("=" * 100)
for k in LEVERAGES:
c_old = res[(0.60, k)][2]
c_new = res[(0.89, k)][2]
if np.isfinite(c_old) and np.isfinite(c_new):
print(f" leva {k:.2f}: muro ${c_old:,.0f} -> ${c_new:,.0f} "
f"({(c_new/c_old - 1)*100:+.0f}%)")
c_new = res[(0.89, 1.0)][2]
print(f"\n Il muro scende di ~45%, ma resta ${c_new:,.0f} = ~{c_new/600:,.0f}x il conto")
print(" di oggi ($600). La conclusione STRUTTURALE del 25/07 non cambia: i 600 euro")
print(" come CAPITALE restano refutati, come BIGLIETTO (prop) no. Cambia la taglia")
print(" dell'errore — e cambia che il numero ora e' misurato invece che scelto.")
print("\n ⚠️ Il ×0.89 e' la sola fortuna d'ancora: e' un LIMITE INFERIORE del fattore")
print(" onesto, perche' ogni componente misurata del path live e' non-negativa.")
print(" Il muro vero sta quindi fra la riga ×0.89 e la riga ×1.00.")
# rendita a $600 — il numero che il 25/07 dava a ~0.11 EUR/g
print("\n" + "=" * 100)
print(" RENDITA A $600 (lo stesso conto di oggi)")
print("=" * 100)
for f, lab in FACTORS:
r = (base - (1.0 - f) * mu).values.astype(float)
cagr = float((1 + r.mean()) ** 365.25 - 1)
eur = 600.0 * cagr * (1 - CC.TAX_RATE) / CC.EURUSD / 365.0
print(f" {lab:<32} CAGR {cagr*100:6.2f}% rendita EUR {eur:.2f}/giorno")
print("\n (a questa taglia la differenza fra ×0.6 e ×0.89 sono centesimi al giorno:")
print(" il fattore conta per il MURO e per le soglie prop, non per il conto attuale.)")
# ---------------------------------------------------------------- traiettoria
print("\n" + "=" * 100)
print(" TRAIETTORIA da $600 al capitale-rendita (block bootstrap, 3000 path, 25 anni)")
print("=" * 100)
print(" Il fattore agisce DUE VOLTE: alza il drift (si accumula piu' in fretta) E abbassa")
print(" il bersaglio (serve meno capitale). Le due colonne centrali separano gli effetti.")
cap_old = res[(0.60, 1.0)][2]
cap_new = res[(0.89, 1.0)][2]
scen = [
(0.60, cap_old, f"×0.60, muro ${cap_old:,.0f} (25/07)"),
(0.89, cap_old, f"×0.89, muro ${cap_old:,.0f} (solo drift)"),
(0.89, cap_new, f"×0.89, muro ${cap_new:,.0f} (26/07)"),
]
T = {}
for f, capn, lab in scen:
r = (base - (1.0 - f) * mu).values.astype(float)
T[lab] = trajectory(r, capn)
for dep in (0, 250, 500, 1_000, 2_000):
print(f"\n deposito {dep:,}/mese")
print(f" {'scenario':<34}{'p10':>8}{'MEDIANA':>10}{'P(entro 20a)':>14}")
for _f, _c, lab in scen:
d = T[lab][dep]
fm = (lambda v: f"{v:.1f}a" if np.isfinite(v) else " —")
print(f" {lab:<34}{fm(d['p10']):>8}{fm(d['p50']):>10}{d['p20y']:>13.0%}")
print("\n ⚠️ Restano vere le due cose che non dipendono dal fattore: senza depositi il")
print(" capitale-rendita non si raggiunge, e la mediana e' una MEDIANA (meta' dei")
print(" path arriva dopo, e una quota non arriva affatto).")
# ------------------------------------------------------- drill-down su un deposito
DEP = 250.0
print("\n" + "=" * 100)
print(f" DRILL-DOWN — EUR {DEP:,.0f}/mese (5000 path, 30 anni)")
print("=" * 100)
print(f" E' la soglia dove il verdetto si ribalta: P(entro 20a) 8% (×0.60) -> 91% (×0.89).")
r_new = (base - (1.0 - 0.89) * mu).values.astype(float)
r_old = (base - (1.0 - 0.60) * mu).values.astype(float)
F = focus(r_new, cap_new, DEP)
F_old = focus(r_old, cap_old, DEP)
print(f"\n A) QUANDO si tocca il capitale-rendita (${cap_new:,.0f})")
print(f" {'':<14}{'p10':>9}{'p25':>9}{'MEDIANA':>10}{'p75':>9}{'p90':>9}")
for lab, D in (("26/07 (×0.89)", F), ("25/07 (×0.60)", F_old)):
f_ = D["fin"]
if len(f_) < 100:
print(f" {lab:<14}{'—':>9}{'—':>9}{'—':>10}{'—':>9}{'—':>9}")
continue
q = np.percentile(f_, [10, 25, 50, 75, 90])
print(f" {lab:<14}" + "".join(f"{v:8.1f}a" for v in q[:2]) +
f"{q[2]:9.1f}a" + "".join(f"{v:8.1f}a" for v in q[3:]))
print(f"\n B) PROBABILITA' di averlo toccato entro N anni")
print(f" {'':<14}" + "".join(f"{y:>9}a" for y in (5, 10, 15, 20, 25, 30)))
for lab, D in (("26/07 (×0.89)", F), ("25/07 (×0.60)", F_old)):
y = D["yrs"]
print(f" {lab:<14}" + "".join(f"{float((y <= n).sum())/D['n']:9.0%}"
for n in (5, 10, 15, 20, 25, 30)))
print(f"\n C) CAPITALE lungo la strada (×0.89) — e quanto ne hai MESSO tu")
print(f" {'anno':>5}{'versato':>12}{'p10':>13}{'MEDIANA':>13}{'p90':>13}"
f"{' mediana/versato':>18}")
for y in (5, 10, 15, 20, 25, 30):
c = F["snaps"][y]
pd_ = float(np.median(F["snap_paid"][y]))
q = np.percentile(c, [10, 50, 90])
print(f" {y:>5}{'$'+format(pd_, ',.0f'):>12}"
+ "".join(f"{'$'+format(v, ',.0f'):>13}" for v in q)
+ f"{q[1]/pd_:>17.1f}x")
if len(F["paid_at_hit"]) > 100:
med_paid = float(np.median(F["paid_at_hit"]))
print(f"\n D) AL TRAGUARDO (mediana dei path che arrivano)")
print(f" versato di tasca: ${med_paid:,.0f} "
f"(= EUR {med_paid/CC.EURUSD:,.0f} in ~{np.median(F['fin']):.0f} anni)")
print(f" capitale: ${cap_new:,.0f}")
print(f" quota da RENDIMENTO: {(1 - med_paid/cap_new)*100:.0f}% "
f"(il resto sono i tuoi versamenti)")
print(f" rendita che paga: EUR {CC.TARGET_EUR_DAY:.0f}/giorno netti")
# -------------------------------------------- quanto versare per un ORIZZONTE dato
print("\n" + "=" * 100)
print(f" QUANTO VERSARE PER ARRIVARCI IN N ANNI (bersaglio ${cap_new:,.0f}, ×0.89)")
print("=" * 100)
print(" 'Arrivarci in N anni' non e' un numero: dipende da quanto vuoi esserne sicuro.")
print(" La colonna e' la PROBABILITA' di ORRIVARE entro l'orizzonte.".replace("ORRIVARE", "arrivare"))
print(f"\n {'orizzonte':<12}{'P=50%':>12}{'P=75%':>12}{'P=90%':>12}{'P=95%':>12}")
sol = {}
for years in (10, 15, 20):
row = f" {str(years) + ' anni':<12}"
for p in (0.50, 0.75, 0.90, 0.95):
dep, versato = solve_deposit(r_new, cap_new, years, p)
sol[(years, p)] = (dep, versato)
row += (f"{'EUR ' + format(dep, ',.0f'):>12}" if np.isfinite(dep)
else f"{'irragg.':>12}")
print(row)
print(f"\n {'':12}{'(totale versato di tasca su tutto l orizzonte)':<48}")
for years in (10, 15, 20):
row = f" {str(years) + ' anni':<12}"
for p in (0.50, 0.75, 0.90, 0.95):
_d, v = sol[(years, p)]
row += (f"{'$' + format(v, ',.0f'):>12}" if np.isfinite(v) else f"{'—':>12}")
print(row)
print("\n ⚠️ A orizzonti corti il versamento domina il rendimento: si sta comprando")
print(" il capitale, non generandolo. Il confronto onesto e' con la colonna")
print(" 'totale versato' — se si avvicina al bersaglio, il book non sta facendo")
print(" il lavoro, lo stanno facendo i bonifici.")
if __name__ == "__main__":
main()