research(rendita): curva capitale->book eseguibile, muro della rendita, asimmetria funded

Ondata "rendita passiva" (2° filone del 2026-07-25). Book live, pesi, config e cron
INVARIATI: questa sessione non tocca nulla del path di esecuzione.

1) DIFETTO DI ESEGUIBILITA' su GTAA01 (5° sleeve del book attivo). gtaa.py modella 2bps
   proporzionali e lo sleeve e' documentato "switch mensile/basso turnover": entrambe false.
   Il vol-target ribilancia OGNI GIORNO su 6 gambe e IB ha un pavimento FISSO per ordine
   (min(max($0.35, $0.0035/az), 1% valore)) = ~$530/anno indipendenti dal capitale.
   A $600: CAGR -3.5% / Sharpe -0.55 (vs +5.5%/0.77 modellato); negativo fino a ~$3k.
   Con banda $50 + cadenza settimanale (plateau, non argmax) torna a Sh 0.52 / CAGR 3.0%.
   -> Sharpe onesto di GTAA01 ~0.55-0.64, non 0.77.
   REGOLA NUOVA: il costo di un venue va modellato nella sua FORMA (fisso vs proporzionale),
   non solo nel livello. Controprova su Deribit (proporzionale): TP01 realistico = modellato
   da ~$500 in su.

2) LA RENDITA NON E' capitale x CAGR. Il muro del 24/07 (EUR 122k) ignorava il rischio di
   sequenza. Prelievo perpetuo (P(cap a 20a >= cap iniziale) >= 90%) sul path live
   de-luckato: 5.98% -> $497k; banda onesta $232k-$497k (il de-luck x0.6 sopra il path live
   rischia di contare due volte la degradazione SKH01). Invariante di lente: la rendita
   perpetua vale ~45-55% del CAGR -> ogni muro calcolato come target/CAGR sbaglia di ~2x.

3) DIVERSIFICARE NON CREA REDDITO a pari nozionale (5.98% -> 6.35%); libera BUDGET DI
   RISCHIO: a iso-rischio (vol 15%) 7.51%/$395k -> 9.25%/$321k. L'ipotesi "piu' capitale ->
   piu' sleeve -> CAGR super-lineare" e' REFUTATA nella forma forte (curva Sharpe piatta da
   $600 a $200k).

4) ASIMMETRIA CAPITALE-PROPRIO vs FUNDED. Su conto funded il capitale e' $100k -> gli sleeve
   STAT-MODE per taglia (XS01) diventano eseguibili, e le regole prop passano sul DRAWDOWN,
   non sul CAGR. Aggiungendo XS01: P(pass) HYRO 44.4->54.7%, funded P(vivo 1a) 18.9->57.6%.

5) LA VIA: i 600 euro come BIGLIETTO. Scala di conti funded, 36 mesi. Problema mai studiato:
   N conti sullo STESSO book bustano INSIEME -> serve sleeve diversi su conti diversi.
   Vince il MISTO, non gli estremi; il book live attuale e' la politica PEGGIORE.
   Verdetto in banda: P(arrivare a 50 EUR/g in 3 anni) 2-21%, P(bruciare i 600) 33-78%
   -> scommessa a coda destra, non una rendita.

6) ADDENDUM "10k su IB": allocazione fra venue. 94% su IB piu' che dimezza il reddito
   (EUR 0.93/g vs 2.16/g). La leva che convertirebbe lo Sharpe in reddito e' chiusa dai
   costi (Reg-T 2x + margine 5.5%) -> vince tutto-Deribit a 1.33x. GTAA01 sui 10k rende
   EUR 0.50/g sopra la liquidita' ferma, pagati con un maxDD del 10%.

Bug catturato in sessione: base di prelievo funded aggiornata giornalmente come HWM mobile
-> payout $230/anno invece di ~$7.000; regole vere = max-loss STATICO dal saldo iniziale.
Test di regressione cablato.

Test: 214 pass (200 preesistenti + 14 nuovi in tests/test_capcurve.py).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-07-25 17:05:16 +00:00
parent 7b567c8435
commit ea6743d6b0
7 changed files with 1975 additions and 0 deletions
+646
View File
@@ -0,0 +1,646 @@
"""r0725_capcurve — LA CURVA CAPITALE -> BOOK ESEGUIBILE (2026-07-25).
Perche' questo script esiste. Il modello di crescita del 2026-07-24 (`r0724_goal50_math.py`
parte A) usa un **CAGR FISSO 15%** per stimare il tempo da $600 a 50 EUR/g. Ma il CAGR del
book NON e' una costante: a $600 girano 2 sleeve su 6 (TP01+SKH01 su Deribit), a $5k si
aggiunge XSR01, a ~$20k XS01, e GTAA01 (6 ETF su IB) ha un COSTO FISSO PER ORDINE che a
capitale piccolo se lo mangia. Il tasso di crescita e' quindi **funzione dello stato**
(equity), e la traiettoria vera e' super-lineare: piu' capitale -> piu' diversificazione ->
piu' Sharpe -> piu' CAGR.
Tre parti:
A) COST-FLOOR PER SLEEVE — Sharpe/CAGR realistici di ogni sleeve in funzione dei dollari
ALLOCATI a quello sleeve, con il modello di costo del SUO venue:
- Deribit (TP01, SKH01): fee proporzionale 0.05%/lato, min order $5 (skip, non trade).
- IB (GTAA01): commissione = min(max($0.35, $0.0035/azione), 1% del controvalore)
-> **pavimento FISSO per ordine**, mai modellato prima (gtaa.py usa 2bps proporzionali).
- Hyperliquid (XSR01): fee 0.045%/lato, min order $5, 50 gambe.
Novita' vs tutto il lavoro precedente: la curva GTAA, cioe' a che capitale il 3o sleeve
smette di essere un costo.
B) FRONTIERA — per ogni capitale totale C, il MIGLIOR sottoinsieme di sleeve eseguibili
(pesi canonici rinormalizzati), con Sharpe/CAGR/DD del book risultante. Da qui la
funzione CAGR(C) che sostituisce il 15% fisso.
C) CRESCITA STATE-DEPENDENT — traiettoria da $600 con CAGR(C), depositi mensili e fisco
italiano 2026 (33% sulle plusvalenze), vs il modello a CAGR fisso. Piu' il "premio di
sblocco": quanto vale anticipare un deposito che fa scattare una soglia.
ONESTA': (1) i rendimenti di SKH01 sono modellati equity-daily-step; il path live e' peggiore
(FULL 1.19/HOLD 1.15/DD 25% vs 1.46/1.64/18%) -> parte B gira anche in modalita' de-luck.
(2) Le soglie XS01/VRP01 sono quelle gia' documentate (non ricalcolate qui). (3) Il fisco e'
applicato come 33% sul realizzato annuo: e' l'ipotesi peggiore (nessuna compensazione).
Uso: `uv run python scripts/research/r0725_capcurve.py`
"""
from __future__ import annotations
import sys
from functools import lru_cache
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from src.data.downloader import load_data # noqa: E402
from src.portfolio.gtaa import EQ_UNIVERSE, _close, _exposure # noqa: E402
from src.portfolio.portfolio import combine_outer, metrics # noqa: E402
from src.portfolio.sleeves import _skyhook_returns, _tp01_returns # noqa: E402
from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio, resample_1d, simple_returns # noqa: E402
EURUSD = 1.09
TARGET_EUR_DAY = 50.0
TAX_RATE = 0.33 # L.199/2025: capital gain crypto 33% dal 2026, esenzione abolita
ASSETS = ("BTC", "ETH")
# --- modelli di costo per VENUE -------------------------------------------------
DERIBIT_FEE_SIDE = 0.0005 # taker 0.05%/lato (0.10% RT) - gia' il default del progetto
DERIBIT_MIN_ORDER = 5.0
IB_MIN_COMM = 0.35 # USD per ordine (IBKR Pro tiered, US stocks/ETF)
IB_PER_SHARE = 0.0035 # USD per azione
IB_MAX_FRAC = 0.01 # cap: 1% del controvalore dell'ordine
IB_MIN_TRADE = 1.0 # ordine frazionale minimo sensato in USD
HL_FEE_SIDE = 0.00045
HL_MIN_ORDER = 5.0
VOL_BUDGET = 0.15 # budget di rischio del confronto ISO-RISCHIO (vol annua)
CAP_GRID = (100.0, 200.0, 400.0, 600.0, 1_000.0, 2_000.0, 5_000.0, 10_000.0,
20_000.0, 50_000.0, 100_000.0, 200_000.0)
# ============================================================== A) COST FLOOR
@lru_cache(maxsize=64)
def _tp01_realistic_cached(alloc: float) -> pd.Series:
"""TP01 con capitale ALLOCATO `alloc` (meta' per asset): un ribilanciamento il cui
nozionale |Dw|*cap_asset e' sotto min order NON si esegue (si tiene la posizione)."""
cap_asset = alloc / len(ASSETS)
tp = TrendPortfolio(**CANONICAL)
series = {}
for a in ASSETS:
df = resample_1d(load_data(a, "1h"))
r = simple_returns(df["close"].values.astype(float))
tgt = np.nan_to_num(np.asarray(tp.target_series(df), float))
held_tgt = np.empty(len(tgt))
cur = 0.0
for i in range(len(tgt)):
if abs(tgt[i] - cur) * cap_asset >= DERIBIT_MIN_ORDER:
cur = tgt[i]
held_tgt[i] = cur
pos = np.zeros(len(held_tgt))
pos[1:] = held_tgt[:-1] # causale: deciso a i-1, tenuto in i
net = pos * r - DERIBIT_FEE_SIDE * np.abs(np.diff(pos, prepend=0.0))
net[0] = 0.0
series[a] = pd.Series(np.clip(net, -0.99, None), index=pd.to_datetime(df["datetime"]))
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
@lru_cache(maxsize=64)
def _gtaa_realistic_cached(alloc: float, universe=EQ_UNIVERSE) -> pd.Series:
"""GTAA con capitale ALLOCATO `alloc`, costo IB REALE (pavimento fisso per ordine).
gtaa.py modella 2bps proporzionali: a $600 su 6 gambe l'ordine tipico e' ~$10-30 e la
commissione IB minima ($0.35) e' l'**1-3%** del controvalore, cioe' 15-150x il modello.
Qui ogni gamba e' un conto separato: si ribilancia solo se il nozionale supera il minimo
frazionale, e si paga min(max(0.35, 0.0035*azioni), 1%*valore) A OGNI ordine.
"""
n = len(universe)
cap_leg = alloc / n
legs = {}
for sym in universe:
close = _close(sym)
ex = np.nan_to_num(np.asarray(_exposure(close).values, float))
px = close.values.astype(float)
ret = np.zeros(len(px))
ret[1:] = px[1:] / px[:-1] - 1.0
held = np.empty(len(ex))
cur = 0.0
comm = np.zeros(len(ex))
for i in range(len(ex)):
notional = abs(ex[i] - cur) * cap_leg
if notional >= IB_MIN_TRADE:
shares = notional / px[i]
c = min(max(IB_MIN_COMM, IB_PER_SHARE * shares), IB_MAX_FRAC * notional)
comm[i] = c / cap_leg # commissione in frazione del capitale-gamba
cur = ex[i]
held[i] = cur
pos = np.zeros(len(held))
pos[1:] = held[:-1]
net = pos * ret - comm
net[0] = 0.0
legs[sym] = pd.Series(net, index=close.index)
r = pd.concat(legs, axis=1, sort=True).sort_index().mean(axis=1, skipna=True).dropna()
if r.index.tz is None:
r.index = r.index.tz_localize("UTC")
days = pd.date_range(r.index.min().normalize(), r.index.max().normalize(), freq="1D", tz="UTC")
return r.reindex(days).fillna(0.0)
@lru_cache(maxsize=256)
def _gtaa_banded_cached(alloc: float, band_usd: float, every: int = 1,
universe=EQ_UNIVERSE) -> pd.Series:
"""GTAA con BANDA di ribilanciamento e cadenza: si guarda il target solo ogni `every`
barre, e si esegue solo se il nozionale supera `band_usd`. Con un costo FISSO per ordine
la banda ottimale non e' il minimo eseguibile (come su Deribit) ma un valore molto piu'
alto: ogni ordine costa $0.35 a prescindere dalla taglia."""
n = len(universe)
cap_leg = alloc / n
legs = {}
for sym in universe:
close = _close(sym)
ex = np.nan_to_num(np.asarray(_exposure(close).values, float))
px = close.values.astype(float)
ret = np.zeros(len(px))
ret[1:] = px[1:] / px[:-1] - 1.0
held = np.empty(len(ex))
cur = 0.0
comm = np.zeros(len(ex))
for i in range(len(ex)):
if i % every == 0:
notional = abs(ex[i] - cur) * cap_leg
if notional >= max(band_usd, IB_MIN_TRADE):
shares = notional / px[i]
c = min(max(IB_MIN_COMM, IB_PER_SHARE * shares), IB_MAX_FRAC * notional)
comm[i] = c / cap_leg
cur = ex[i]
held[i] = cur
pos = np.zeros(len(held))
pos[1:] = held[:-1]
net = pos * ret - comm
net[0] = 0.0
legs[sym] = pd.Series(net, index=close.index)
r = pd.concat(legs, axis=1, sort=True).sort_index().mean(axis=1, skipna=True).dropna()
if r.index.tz is None:
r.index = r.index.tz_localize("UTC")
days = pd.date_range(r.index.min().normalize(), r.index.max().normalize(), freq="1D", tz="UTC")
return r.reindex(days).fillna(0.0)
def part_a2() -> dict:
"""La banda salva GTAA? Sweep banda x cadenza per capitale. Il parametro e' di COSTO
(non di segnale): la scelta si fa sul costo atteso, non sullo Sharpe in-sample."""
print("\n" + "=" * 100)
print(" A2) GTAA01 — la BANDA di ribilanciamento puo' salvarlo? (costo fisso IB $0.35/ordine)")
print("=" * 100)
from src.portfolio.gtaa import gtaa_returns
g_model = gtaa_returns()
if g_model.index.tz is None:
g_model.index = g_model.index.tz_localize("UTC")
print(f" riferimento MODELLATO (2bps propor., ribil. continuo): "
f"Sh {metrics(g_model)['sharpe']:.2f} CAGR {metrics(g_model)['cagr']:.1%}")
best = {}
for alloc in (600.0, 2_000.0, 5_000.0, 20_000.0):
print(f"\n --- capitale allocato a GTAA: ${alloc:,.0f} (per gamba ${alloc/6:,.0f}) ---")
print(f" {'banda$':>8} {'cadenza':>9} {'Sharpe':>8} {'CAGR':>8} {'maxDD':>8} {'ord/anno':>9}")
rows = []
for every, lbl in ((1, "daily"), (5, "weekly"), (21, "monthly")):
for band in (1.0, 5.0, 25.0, 50.0, 100.0):
if band > alloc / 6: # banda piu' grande della gamba = mai trade
continue
s = gtaa_banded(alloc, band, every)
m = metrics(s)
# ordini/anno stimati dal drag: ricostruiti come costo/anno / 0.35
rows.append((band, lbl, every, m, s))
for band, lbl, every, m, s in rows:
yrs = len(s) / 365.25
print(f" {band:>8.0f} {lbl:>9} {m['sharpe']:>8.2f} {m['cagr']:>8.1%} "
f"{m['maxdd']:>8.1%} {'':>9}")
top = max(rows, key=lambda r: r[3]["sharpe"])
best[alloc] = dict(band=top[0], every=top[2], label=top[1],
metrics=top[3], series=top[4])
print(f" -> migliore: banda ${top[0]:.0f} {top[1]} Sh {top[3]['sharpe']:.2f} "
f"CAGR {top[3]['cagr']:.1%}")
return best
def _sh(s: pd.Series, lo=None) -> dict:
x = s if lo is None else s[s.index >= lo]
return metrics(x)
# --- wrapper pubblici: le implementazioni sopra sono memoizzate e lru_cache ritorna lo STESSO
# oggetto a ogni hit -> si consegna sempre una COPIA, cosi' una mutazione in place di un
# chiamante non puo' corrompere in silenzio i risultati successivi.
def tp01_realistic(alloc: float) -> pd.Series:
return _tp01_realistic_cached(float(alloc)).copy()
def gtaa_realistic(alloc: float, universe=EQ_UNIVERSE) -> pd.Series:
return _gtaa_realistic_cached(float(alloc), universe).copy()
def gtaa_banded(alloc: float, band_usd: float, every: int = 1,
universe=EQ_UNIVERSE) -> pd.Series:
return _gtaa_banded_cached(float(alloc), float(band_usd), int(every), universe).copy()
def part_a() -> dict:
print("=" * 100)
print(" A) COST-FLOOR PER SLEEVE — Sharpe reale vs dollari ALLOCATI a quello sleeve")
print("=" * 100)
alloc_grid = (50.0, 100.0, 200.0, 500.0, 1_000.0, 2_000.0, 5_000.0, 10_000.0, 50_000.0)
out = {"TP01": {}, "GTAA01": {}}
print("\n TP01 (Deribit, 2 gambe, fee 0.05%/lato, min order $5)")
print(f" {'alloc':>10} {'Sharpe':>8} {'CAGR':>8} {'maxDD':>8} {'vs modellato':>14}")
base_tp = _tp01_returns()
if base_tp.index.tz is None:
base_tp.index = base_tp.index.tz_localize("UTC")
m_model = metrics(base_tp)
for a in alloc_grid:
s = tp01_realistic(a)
m = metrics(s)
out["TP01"][a] = s
print(f" {a:>10,.0f} {m['sharpe']:>8.2f} {m['cagr']:>8.1%} {m['maxdd']:>8.1%}"
f" {m['sharpe'] - m_model['sharpe']:>+14.2f}")
print(f" {'MODELLATO':>10} {m_model['sharpe']:>8.2f} {m_model['cagr']:>8.1%} {m_model['maxdd']:>8.1%}")
print("\n GTAA01 (IB, 6 ETF, commissione min $0.35/ordine — PAVIMENTO FISSO)")
print(f" {'alloc':>10} {'Sharpe':>8} {'CAGR':>8} {'maxDD':>8} {'drag comm/anno':>15}")
from src.portfolio.gtaa import gtaa_returns
g_model = gtaa_returns()
if g_model.index.tz is None:
g_model.index = g_model.index.tz_localize("UTC")
mg_model = metrics(g_model)
for a in alloc_grid:
s = gtaa_realistic(a)
m = metrics(s)
out["GTAA01"][a] = s
drag = mg_model["cagr"] - m["cagr"]
print(f" {a:>10,.0f} {m['sharpe']:>8.2f} {m['cagr']:>8.1%} {m['maxdd']:>8.1%}"
f" {drag:>15.2%}")
print(f" {'MODELLATO':>10} {mg_model['sharpe']:>8.2f} {mg_model['cagr']:>8.1%} {mg_model['maxdd']:>8.1%}"
f" {'(2bps propor.)':>15}")
out["_models"] = dict(TP01=m_model, GTAA01=mg_model)
return out
# ============================================================== B) FRONTIERA
# Config GTAA scelta sul PLATEAU (non sulla cella max): cadenza settimanale + banda $50.
# La banda e' un parametro di COSTO, non di segnale — si sceglie sul plateau robusto
# (weekly/monthly x banda 25-100 danno 0.45-0.64 a ogni capitale), non sull'argmax
# in-sample (banda 50 daily a $600 = 0.57 e' una cella isolata, crolla a 0.28 a banda 100).
GTAA_BAND_USD, GTAA_EVERY = 50.0, 5
def part_b(deluck: float = 1.0) -> dict:
"""Per ogni capitale totale C: il book eseguibile migliore e il suo CAGR/Sharpe.
A $600 il book live e' TP01+SKH01 su Deribit. La domanda nuova: conviene SPEZZARE il
capitale (una parte su IB per GTAA01)? Il costo dello split e' il pavimento IB; il
beneficio e' un 3o sleeve a corr ~+0.10.
deluck: moltiplicatore sul DRIFT (0.6 = convenzione de-luck del progetto), la vol resta.
"""
tag = "MODELLATO" if deluck == 1.0 else f"DE-LUCK x{deluck}"
print("\n" + "=" * 100)
print(f" B) FRONTIERA CAPITALE -> BOOK ESEGUIBILE [{tag}]")
print("=" * 100)
skh = _skyhook_returns()
if skh.index.tz is None:
skh.index = skh.index.tz_localize("UTC")
def _dl(s: pd.Series) -> pd.Series:
if deluck == 1.0:
return s
return s - (1.0 - deluck) * float(s.mean())
print(f"\n {'capitale':>9} {'book':>28} {'ShFULL':>7} {'ShHOLD':>7} {'CAGR':>7} "
f"{'maxDD':>7} {'EUR/g':>7}")
rows = []
for C in (600.0, 1_000.0, 2_000.0, 5_000.0, 10_000.0, 20_000.0, 50_000.0, 100_000.0, 200_000.0):
cands = []
# --- 1) solo Deribit (book live attuale)
for wg in (0.0, 0.10, 0.20, 0.30):
w_der = 1.0 - wg
alloc_tp, alloc_gt = C * w_der * 0.75, C * wg
cols = {"TP01": _dl(tp01_realistic(alloc_tp)), "SKH01": _dl(skh)}
wts = {"TP01": w_der * 0.75, "SKH01": w_der * 0.25}
if wg > 0:
cols["GTAA01"] = _dl(gtaa_banded(alloc_gt, GTAA_BAND_USD, GTAA_EVERY))
wts["GTAA01"] = wg
combo = combine_outer(cols, wts, lo=pd.Timestamp("2019-03-14", tz="UTC"))
m = metrics(combo)
mh = metrics(combo[combo.index >= pd.Timestamp("2025-01-01", tz="UTC")])
name = "TP01+SKH01 (Deribit)" if wg == 0 else f"+GTAA01 {wg:.0%} (IB split)"
cands.append((name, m, mh, wg))
best = max(cands, key=lambda r: r[1]["sharpe"])
for name, m, mh, wg in cands:
mark = " *" if name == best[0] else " "
eur = C * m["cagr"] * (1 - TAX_RATE) / EURUSD / 365.0
print(f" {C:>9,.0f} {name:>28} {m['sharpe']:>7.2f} {mh['sharpe']:>7.2f} "
f"{m['cagr']:>7.1%} {m['maxdd']:>7.1%} {eur:>7.2f}{mark}")
rows.append((C, best))
print()
return dict(rows=rows)
# ============================================================== C) RENDITA
def book_series(lens: str = "hourly", with_gtaa: float = 0.0,
alloc: float = 600.0) -> pd.Series:
"""Serie giornaliera del BOOK ESEGUIBILE.
lens='hourly' -> SKH01 sul PATH LIVE (fill al close 5m del prossimo multiplo orario:
e' la stima onesta di cio' che il cron esegue davvero).
lens='canonical' -> SKH01 modellato (fill AL LIVELLO SL/TP): e' il backtest, non il live.
"""
sys.path.insert(0, str(ROOT / "scripts" / "research"))
import r0724_skh_live_weight as skl # noqa: E402 (riusa la macchineria d'audit gia' validata)
skh = skl.skh_book(0, lens)
if skh.index.tz is None:
skh.index = skh.index.tz_localize("UTC")
cols = {"TP01": tp01_realistic(alloc * 0.75), "SKH01": skh}
wts = {"TP01": 0.75, "SKH01": 0.25}
if with_gtaa > 0:
cols = {"TP01": tp01_realistic(alloc * (1 - with_gtaa) * 0.75), "SKH01": skh,
"GTAA01": gtaa_banded(alloc * with_gtaa, GTAA_BAND_USD, GTAA_EVERY)}
wts = {"TP01": (1 - with_gtaa) * 0.75, "SKH01": (1 - with_gtaa) * 0.25,
"GTAA01": with_gtaa}
return combine_outer(cols, wts, lo=pd.Timestamp("2019-03-14", tz="UTC"))
def _boot_paths(r: np.ndarray, n_paths: int, n_days: int, block: int,
rng: np.random.Generator) -> np.ndarray:
"""Block bootstrap (blocchi di `block` giorni) — preserva la dipendenza seriale
(momentum/vol-clustering) che un bootstrap iid distruggerebbe, gonfiando la sopravvivenza."""
n_blocks = int(np.ceil(n_days / block))
starts = rng.integers(0, len(r) - block, size=(n_paths, n_blocks))
idx = (starts[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)
return r[idx[:, :n_days]]
def survival(r: np.ndarray, cap0: float, draw_usd_yr: float, years: int,
k: float, n_paths: int = 4000, block: int = 20,
index_rate: float = 0.0, seed: int = 725) -> dict:
"""P(il capitale sopravvive `years` anni prelevando `draw_usd_yr`/anno) con leva `k`.
Prelievo giornaliero pro-rata (una rendita si preleva in continuo, non a fine anno).
"""
rng = np.random.default_rng(seed)
n_days = int(years * 365)
paths = _boot_paths(r, n_paths, n_days, block, rng) * k
cap = np.full(n_paths, cap0, float)
alive = np.ones(n_paths, bool)
daily_draw = draw_usd_yr / 365.0
ruin_day = np.full(n_paths, n_days + 1, int)
for t in range(n_days):
d = daily_draw * (1.0 + index_rate) ** (t / 365.0)
cap = np.where(alive, cap * (1.0 + paths[:, t]) - d, cap)
newly = alive & (cap <= 0)
ruin_day[newly] = t
alive &= cap > 0
return dict(p_survive=float(alive.mean()),
median_end=float(np.median(np.where(alive, cap, 0.0))),
p10_end=float(np.percentile(np.where(alive, cap, 0.0), 10)),
median_ruin_yr=float(np.median(ruin_day[~alive]) / 365.0) if (~alive).any() else np.nan)
def part_c() -> dict:
"""La RENDITA sostenibile del book. Il muro del 24/07 usava capitale = target/CAGR,
che ignora il RISCHIO DI SEQUENZA: prelevare da un book volatile mentre e' in drawdown
consuma capitale che non torna. Qui il muro vero, a 95% di sopravvivenza."""
print("\n" + "=" * 100)
print(" C) RENDITA SOSTENIBILE — quanto capitale serve DAVVERO per 50 EUR/g")
print("=" * 100)
out = {}
for lens, deluck in (("canonical", 1.0), ("hourly", 1.0), ("hourly", 0.6)):
b = book_series(lens)
if deluck != 1.0:
b = b - (1.0 - deluck) * float(b.mean()) # taglia il DRIFT, lascia la vol
m = metrics(b)
r = b.values.astype(float)
vol = float(r.std() * np.sqrt(365))
lbl = ("MODELLATO (backtest)" if lens == "canonical" else "PATH LIVE (cron orario)")
if deluck != 1.0:
lbl += f" DE-LUCK drift x{deluck}"
print(f"\n --- book TP01 75 / SKH01 25 — {lbl} ---")
print(f" Sharpe {m['sharpe']:.2f} CAGR {m['cagr']:.1%} vol {vol:.1%} "
f"maxDD {m['maxdd']:.1%} ({m['n']} giorni)")
# tasso di prelievo massimo sostenibile al 95% di sopravvivenza, per leva k
print(f"\n {'leva':>5} {'vol':>7} {'CAGR':>7} {'SWR20a':>8} {'SWR30a':>8} "
f"{'capitale per 50EUR/g netti':>28}")
rows = {}
for k in (0.5, 0.75, 1.0, 1.5, 2.0):
# SWR = massimo tasso di prelievo (% del capitale iniziale) con P(vivo) >= 95%
swr = {}
for years in (20, 30):
lo, hi = 0.0, 0.40
for _ in range(14): # bisezione sul tasso
mid = (lo + hi) / 2
p = survival(r, 1_000_000.0, 1_000_000.0 * mid, years, k,
n_paths=2000)["p_survive"]
if p >= 0.95:
lo = mid
else:
hi = mid
swr[years] = lo
gross_needed = TARGET_EUR_DAY * 365 * EURUSD / (1 - TAX_RATE)
cap_needed = gross_needed / swr[20] if swr[20] > 0.001 else float("inf")
print(f" {k:>5.2f} {vol*k:>7.1%} {m['cagr']*k:>7.1%} {swr[20]:>8.2%} "
f"{swr[30]:>8.2%} ${cap_needed:>26,.0f}")
rows[k] = dict(swr20=swr[20], swr30=swr[30], cap=cap_needed)
out[(lens, deluck)] = dict(metrics=m, vol=vol, rows=rows, series=b)
best_k = max(rows, key=lambda k: rows[k]["swr20"])
print(f" -> leva che massimizza la rendita: {best_k:.2f}x "
f"(SWR {rows[best_k]['swr20']:.2%}, capitale ${rows[best_k]['cap']:,.0f})")
return out
# ====================================================== C2) RENDITA PERPETUA
def part_c2() -> dict:
"""'Survive 20 anni' NON e' una rendita: e' un piano di consumo del capitale. Una rendita
passiva vera vuole il capitale INTATTO (altrimenti a 20 anni il reddito finisce). Qui il
criterio giusto: prelievo massimo con P(capitale a 20a >= capitale iniziale) >= 90%."""
print("\n" + "=" * 100)
print(" C2) RENDITA PERPETUA — prelievo che PRESERVA il capitale (P(cap_20a >= cap_0) >= 90%)")
print("=" * 100)
gross_needed = TARGET_EUR_DAY * 365 * EURUSD / (1 - TAX_RATE)
print(f" 50 EUR/g netti = EUR {TARGET_EUR_DAY*365:,.0f}/anno netti "
f"= ${gross_needed:,.0f}/anno LORDI (fisco {TAX_RATE:.0%})\n")
print(f" {'lente':>34} {'leva':>5} {'SWR-20a':>8} {'PERPETUA':>9} "
f"{'capitale (perpetua)':>21}")
out = {}
for lens, deluck in (("canonical", 1.0), ("hourly", 1.0), ("hourly", 0.6)):
b = book_series(lens)
if deluck != 1.0:
b = b - (1.0 - deluck) * float(b.mean())
r = b.values.astype(float)
lbl = ("MODELLATO" if lens == "canonical" else "PATH LIVE")
if deluck != 1.0:
lbl += f" de-luck x{deluck}"
for k in (1.0, 1.5, 2.0):
lo, hi = 0.0, 0.40
for _ in range(13):
mid = (lo + hi) / 2
s = survival(r, 1_000_000.0, 1_000_000.0 * mid, 20, k, n_paths=2000)
# criterio PERPETUO: il 90mo percentile dei path deve finire >= capitale iniziale
keep = s["p10_end"] >= 1_000_000.0
if keep:
lo = mid
else:
hi = mid
swr20 = None
lo2, hi2 = 0.0, 0.40
for _ in range(13):
mid = (lo2 + hi2) / 2
if survival(r, 1e6, 1e6 * mid, 20, k, n_paths=2000)["p_survive"] >= 0.95:
lo2 = mid
else:
hi2 = mid
swr20 = lo2
cap = gross_needed / lo if lo > 0.002 else float("inf")
print(f" {lbl:>34} {k:>5.2f} {swr20:>8.2%} {lo:>9.2%} "
f"{('$' + format(cap, ',.0f')) if np.isfinite(cap) else 'IRRAGGIUNGIBILE':>21}")
out[(lens, deluck, k)] = dict(swr20=swr20, perp=lo, cap=cap)
print()
return out
# ============================================================== D) TRAIETTORIA
def part_d(perp_rate: float, cap_needed: float) -> None:
"""Quanto tempo per accumulare il capitale della rendita — STOCASTICO (il 24/07 era
deterministico: un CAGR fisso nasconde che meta' dei path arriva dopo)."""
print("\n" + "=" * 100)
print(f" D) TRAIETTORIA — da $600 a ${cap_needed:,.0f} (rendita perpetua {perp_rate:.2%})")
print("=" * 100)
b = book_series("hourly")
b = b - 0.4 * float(b.mean()) # de-luck x0.6: la lente della decisione
r = b.values.astype(float)
rng = np.random.default_rng(725)
n_days = 365 * 25
paths = _boot_paths(r, 3000, n_days, 20, rng)
print(f"\n {'deposito/mese':>14} {'p10':>8} {'mediana':>9} {'p90':>8} "
f"{'P(entro 10a)':>12} {'P(entro 20a)':>12}")
for dep_eur in (0, 250, 500, 1_000, 2_000, 5_000):
dep = dep_eur * EURUSD
cap = np.full(3000, 600.0)
hit = np.full(3000, -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)
finite = yrs[~np.isnan(yrs)]
p10 = np.percentile(finite, 10) if len(finite) > 100 else np.nan
p50 = np.percentile(finite, 50) if len(finite) > 100 else np.nan
p90 = np.percentile(finite, 90) if len(finite) > 100 else np.nan
p10y = float((yrs <= 10).sum() / len(yrs))
p20y = float((yrs <= 20).sum() / len(yrs))
f = lambda v: f"{v:.1f}a" if np.isfinite(v) else "" # noqa: E731
print(f" {dep_eur:>10,.0f}/m {f(p10):>8} {f(p50):>9} {f(p90):>8} "
f"{p10y:>12.0%} {p20y:>12.0%}")
print("\n (bootstrap a blocchi dei ritorni REALI del book, de-luck x0.6; i depositi")
print(" sono in EUR/mese. 'P(entro Na)' = quota di path che tocca il capitale-rendita.)")
# ================================================ E) IL MURO SI ABBASSA?
def part_e() -> dict:
"""L'ipotesi di partenza: piu' capitale -> piu' sleeve eseguibili -> piu' Sharpe -> muro
piu' basso. Per il book eseguibile-OGGI e' FALSA (parte B: Sharpe piatto da $600 a $200k).
Ma attraverso le SOGLIE di sblocco (XSR01 ~$5k, XS01 ~$20k) puo' essere vera. Qui il
conto onesto, con GTAA01 al suo costo IB REALE e SKH01 sul path live.
Regole di ammissione rispettate: VRP01 ESCLUSO (regola permanente 'niente short-vol da
modello in deploy'); XSR01 escluso dal book deployable (gate pre-registrato 2026-10-23,
oggi in forward-monitor) ma mostrato come contro-fattuale.
"""
print("\n" + "=" * 100)
print(" E) IL MURO SI ABBASSA COL CAPITALE? — book per tier di sbloccо")
print("=" * 100)
from src.portfolio.sleeves import _vrp_combo_returns, _xsec_returns
sys.path.insert(0, str(ROOT / "scripts" / "research"))
import r0724_skh_live_weight as skl
skh = skl.skh_book(0, "hourly")
if skh.index.tz is None:
skh.index = skh.index.tz_localize("UTC")
def _d(s):
s = s.dropna().sort_index()
if s.index.tz is None:
s.index = s.index.tz_localize("UTC")
return s
xs = _d(_xsec_returns()).resample("1D").apply(lambda x: (1 + x).prod() - 1)
vrp = _d(_vrp_combo_returns()).resample("1D").apply(lambda x: (1 + x).prod() - 1)
gross_needed = TARGET_EUR_DAY * 365 * EURUSD / (1 - TAX_RATE)
tiers = [
("$600 — 2 sleeve (Deribit)", 600.0,
{"TP01": 0.75, "SKH01": 0.25}),
("$2k — + GTAA01 (IB, banda)", 2_000.0,
{"TP01": 0.60, "SKH01": 0.20, "GTAA01": 0.20}),
("$20k — + XS01 (Hyperliquid)", 20_000.0,
{"TP01": 0.45, "SKH01": 0.25, "GTAA01": 0.20, "XS01": 0.10}),
("$20k — + XS01 + VRP01 (contro-fattuale)", 20_000.0,
{"TP01": 0.40, "SKH01": 0.22, "GTAA01": 0.18, "XS01": 0.10, "VRP01": 0.10}),
]
print(f"\n {'tier':>42} {'Sharpe':>7} {'CAGR':>7} {'vol':>6} {'maxDD':>7} "
f"{'rendita':>8} {'capitale 50EUR/g':>18}")
out = {}
for name, C, w in tiers:
cols = {"TP01": tp01_realistic(C * w["TP01"]), "SKH01": skh}
if "GTAA01" in w:
cols["GTAA01"] = gtaa_banded(C * w["GTAA01"], GTAA_BAND_USD, GTAA_EVERY)
if "XS01" in w:
cols["XS01"] = xs
if "VRP01" in w:
cols["VRP01"] = vrp
combo = combine_outer(cols, w, lo=pd.Timestamp("2019-03-14", tz="UTC"))
combo = combo - 0.4 * float(combo.mean()) # de-luck x0.6 (lente della decisione)
m = metrics(combo)
r = combo.values.astype(float)
lo, hi = 0.0, 0.40
for _ in range(13):
mid = (lo + hi) / 2
if survival(r, 1e6, 1e6 * mid, 20, 1.0, n_paths=2000)["p10_end"] >= 1e6:
lo = mid
else:
hi = mid
cap = gross_needed / lo if lo > 0.002 else float("inf")
vol = float(r.std() * np.sqrt(365))
# ISO-RISCHIO: e' QUI che la diversificazione paga. A pari nozionale un diversificatore
# a basso CAGR diluisce il ritorno; il suo valore e' che LIBERA BUDGET DI RISCHIO. Si
# confronta portando ogni tier alla STESSA vol (15%), non allo stesso nozionale.
k_iso = min(VOL_BUDGET / vol, 3.0)
lo_i, hi_i = 0.0, 0.60
for _ in range(13):
mid = (lo_i + hi_i) / 2
if survival(r, 1e6, 1e6 * mid, 20, k_iso, n_paths=2000)["p10_end"] >= 1e6:
lo_i = mid
else:
hi_i = mid
cap_i = gross_needed / lo_i if lo_i > 0.002 else float("inf")
print(f" {name:>42} {m['sharpe']:>7.2f} {m['cagr']:>7.1%} {vol:>6.1%} "
f"{m['maxdd']:>7.1%} {lo:>8.2%} "
f"{('$' + format(cap, ',.0f')) if np.isfinite(cap) else 'IRRAGG.':>18}"
f" | {k_iso:>4.2f}x {lo_i:>7.2%} "
f"{('$' + format(cap_i, ',.0f')) if np.isfinite(cap_i) else 'IRRAGG.':>18}")
out[name] = dict(perp=lo, cap=cap, metrics=m, k_iso=k_iso, perp_iso=lo_i, cap_iso=cap_i)
print("\n (tutti de-luck x0.6, SKH01 su path live, GTAA01 a costo IB reale con banda.")
print(" 'rendita' = prelievo perpetuo al 90% di preservazione del capitale.)")
return out
if __name__ == "__main__":
part_a()
part_a2()
part_b()
part_c()
res = part_c2()
part_e()
key = ("hourly", 0.6, 1.0)
part_d(res[key]["perp"], res[key]["cap"])
+217
View File
@@ -0,0 +1,217 @@
"""r0725_ib10k — E SE METTO 10K SU IB? Lo split ottimale Deribit/IB (2026-07-25).
Domanda dell'operatore, posta dopo l'ondata rendita: "se metto 10K in IB?".
Non e' una domanda su GTAA01: e' una domanda di ALLOCAZIONE FRA DUE VENUE, e le due
gambe hanno profili opposti:
* DERIBIT (TP01+SKH01): Sharpe ~0.98 de-luck, CAGR ~11%, vol 11.4% — e' il MOTORE di reddito.
* IB (GTAA01): Sharpe ~0.6, CAGR ~3.5-5%, vol ~7% — e' un DIVERSIFICATORE (corr ~+0.10),
e a $600 era negativo per il pavimento di commissione; a $10k il pavimento diventa
trascurabile (0.02% per ordine invece dell'1-3%) -> **il deposito SBLOCCA lo sleeve**.
Tre effetti che si sommano e che vanno contati INSIEME:
1. sblocco di GTAA01 (il pavimento IB smette di mordere);
2. spostamento del baricentro su un venue a CAGR basso (se ci va TUTTO, il reddito CALA);
3. **fisco diverso**: plusvalenze ETF 26% vs crypto 33% (L.199/2025) -> un euro lordo su IB
vale il 10% in piu' netto di un euro lordo su Deribit.
(+ pavimento alternativo: la liquidita' IB non investita rende interesse ~2% EUR / ~4% USD.)
Output: la curva dello split, giudicata su TRE metriche diverse perche' danno risposte diverse —
reddito atteso netto, rendita perpetua (preservazione del capitale), e reddito a ISO-RISCHIO.
ONESTA': (a) SKH01 sul path live, de-luck x0.6 (lente di decisione del progetto);
(b) GTAA01 col costo IB REALE (pavimento fisso) e banda/cadenza scelte sul plateau;
(c) le aliquote sono l'assunzione dichiarata 26%/33% — da verificare col commercialista,
NON un parere fiscale; (d) i numeri di GTAA01 sono su ~30 anni, quelli crypto su ~7.
Uso: `uv run python scripts/research/r0725_ib10k.py`
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "scripts" / "research"))
from src.portfolio.portfolio import combine_outer, metrics # noqa: E402
import r0724_skh_live_weight as skl # noqa: E402
from r0725_capcurve import (EURUSD, gtaa_banded, metrics as _m, survival, # noqa: E402
tp01_realistic)
TAX_CRYPTO = 0.33 # L.199/2025 dal 1/1/2026
TAX_EQUITY = 0.26 # plusvalenze su ETF (assunzione dichiarata)
IB_CASH_RATE = 0.02 # interesse su liquidita EUR non investita (IBKR, sopra $10k)
IB_MARGIN_RATE = 0.055 # costo del margine IB in USD (benchmark + spread) — assunzione dichiarata
DERIBIT_FUNDING = 0.02 # funding medio dei perp Deribit (ricerca 24/07: ciclo 0-4%/anno)
DELUCK = 0.6
DERIBIT_NOW = 600.0 # cio' che c'e' oggi sul conto Deribit
BOOK_START = pd.Timestamp("2019-03-14", tz="UTC")
def _d(s: pd.Series) -> pd.Series:
s = s.dropna().sort_index()
if s.index.tz is None:
s.index = s.index.tz_localize("UTC")
return s
def _deluck(s: pd.Series) -> pd.Series:
return s - (1.0 - DELUCK) * float(s.mean())
def gtaa_best_band(alloc: float) -> tuple[float, int, dict]:
"""Config di costo per GTAA al capitale dato, scelta sul PLATEAU (weekly, banda 25-100):
non e' un parametro di segnale, si sceglie sul costo e si dichiara."""
best = None
for every in (5, 21):
for band in (25.0, 50.0, 100.0):
if band > alloc / 6:
continue
m = metrics(gtaa_banded(alloc, band, every))
if best is None or m["sharpe"] > best[2]["sharpe"]:
best = (band, every, m)
if best is None: # capitale troppo piccolo per una banda
best = (1.0, 21, metrics(gtaa_banded(alloc, 1.0, 21)))
return best
def build(total: float, f_ib: float) -> dict:
"""Book con frazione f_ib del capitale su IB (GTAA01) e il resto su Deribit (TP01/SKH01)."""
a_ib, a_der = total * f_ib, total * (1.0 - f_ib)
skh = _d(skl.skh_book(0, "hourly"))
cols, w = {}, {}
if a_der > 1.0:
cols["TP01"] = _deluck(_d(tp01_realistic(a_der * 0.75)))
cols["SKH01"] = _deluck(skh)
w["TP01"] = (1.0 - f_ib) * 0.75
w["SKH01"] = (1.0 - f_ib) * 0.25
band = every = None
if a_ib > 1.0:
band, every, _ = gtaa_best_band(a_ib)
cols["GTAA01"] = _deluck(_d(gtaa_banded(a_ib, band, every)))
w["GTAA01"] = f_ib
combo = combine_outer(cols, w, lo=BOOK_START)
m = metrics(combo)
vol = float(combo.std() * np.sqrt(365))
# fisco misto: la quota IB e' tassata al 26%, la quota Deribit al 33%
tax_blend = f_ib * TAX_EQUITY + (1.0 - f_ib) * TAX_CRYPTO
eur_day = total * m["cagr"] * (1.0 - tax_blend) / EURUSD / 365.0
return dict(metrics=m, vol=vol, combo=combo, tax=tax_blend, eur_day=eur_day,
band=band, every=every, a_ib=a_ib, a_der=a_der)
def perp_rate(combo: pd.Series, k: float = 1.0, n: int = 1500) -> float:
"""Prelievo perpetuo (P(cap a 20a >= cap iniziale) >= 90%)."""
r = combo.values.astype(float)
lo, hi = 0.0, 0.35
for _ in range(12):
mid = (lo + hi) / 2
if survival(r, 1e6, 1e6 * mid, 20, k, n_paths=n)["p10_end"] >= 1e6:
lo = mid
else:
hi = mid
return lo
def main() -> None:
total = DERIBIT_NOW + 10_000.0 * EURUSD # $600 + EUR 10k
print("=" * 100)
print(f" SE METTO 10K SU IB — capitale totale ${total:,.0f} "
f"(= $600 Deribit + EUR 10.000)")
print("=" * 100)
print(f" fisco assunto: ETF {TAX_EQUITY:.0%} / crypto {TAX_CRYPTO:.0%} | de-luck x{DELUCK} | "
f"SKH01 path live")
print(f" pavimento alternativo: liquidita' IB non investita ~{IB_CASH_RATE:.0%}/anno EUR "
f"= EUR {total/EURUSD*IB_CASH_RATE*(1-TAX_EQUITY)/365:.2f}/g\n")
print(f" {'quota su IB':>12} {'GTAA banda':>12} {'Sharpe':>7} {'CAGR':>7} {'vol':>6} "
f"{'maxDD':>7} {'fisco':>6} {'EUR/g':>7} {'rendita':>8} {'EUR/g rend.':>12}")
rows = []
for f_ib in (0.0, 0.25, 0.50, 0.75, 0.90, 0.94, 1.0):
b = build(total, f_ib)
pr = perp_rate(b["combo"])
eur_perp = total * pr * (1.0 - b["tax"]) / EURUSD / 365.0
lbl = "" if b["band"] is None else f"${b['band']:.0f}/{ 'sett' if b['every']==5 else 'mens'}"
print(f" {f_ib:>11.0%} {lbl:>12} {b['metrics']['sharpe']:>7.2f} "
f"{b['metrics']['cagr']:>7.1%} {b['vol']:>6.1%} {b['metrics']['maxdd']:>7.1%} "
f"{b['tax']:>6.0%} {b['eur_day']:>7.2f} {pr:>8.2%} {eur_perp:>12.2f}")
rows.append((f_ib, b, pr))
print("\n ('quota su IB' 94% = mettere i 10k su IB e lasciare i $600 dove sono;")
print(" 'rendita' = prelievo perpetuo al 90% di preservazione del capitale.)")
# --- ISO-RISCHIO: e' qui che un diversificatore a basso CAGR si giudica
print("\n" + "-" * 100)
print(" A ISO-RISCHIO (ogni split portato a vol 15% con la leva) — il confronto corretto")
print("-" * 100)
print(f" {'quota su IB':>12} {'leva':>6} {'CAGR iso':>9} {'EUR/g iso':>10} "
f"{'rendita iso':>12} {'EUR/g rend.iso':>15}")
best = None
for f_ib, b, _ in rows:
k = min(0.15 / b["vol"], 3.0)
pr = perp_rate(b["combo"], k=k)
eur = total * b["metrics"]["cagr"] * k * (1.0 - b["tax"]) / EURUSD / 365.0
eur_p = total * pr * (1.0 - b["tax"]) / EURUSD / 365.0
print(f" {f_ib:>11.0%} {k:>6.2f}x {b['metrics']['cagr']*k:>9.1%} {eur:>10.2f} "
f"{pr:>12.2%} {eur_p:>15.2f}")
if best is None or eur_p > best[1]:
best = (f_ib, eur_p, k, pr)
print(f"\n -> split che massimizza la RENDITA a iso-rischio: {best[0]:.0%} su IB "
f"(leva {best[2]:.2f}x, rendita {best[3]:.2%} = EUR {best[1]:.2f}/g)")
# --- ISO-RISCHIO CON VINCOLI DI LEVA REALI -------------------------------
# La leva che l'iso-rischio chiede non e' sempre ottenibile: su IB un conto da EUR 10k ha
# Reg-T (2x max sugli ETF); il portfolio margin parte da ~$110k. Su Deribit i perp danno
# leva alta. La leva ATTUABILE del book misto e' quindi limitata dalla gamba peggiore.
print("\n" + "-" * 100)
print(" ISO-RISCHIO CON LEVA ATTUABILE (IB Reg-T 2.0x su ETF; Deribit perp 3.0x)")
print("-" * 100)
print(f" {'quota su IB':>12} {'leva max':>9} {'leva usata':>11} {'CAGR':>7} "
f"{'EUR/g':>7} {'rendita':>9} {'EUR/g rend.':>12}")
print(" (COSTO DEL MARGINE INCLUSO: la parte presa a prestito si paga — IB ~5.5%/anno sul")
print(" margine USD, Deribit funding perp ~2%/anno medio. Ignorarlo gonfia ogni riga levata.)")
print(f" {'quota su IB':>12} {'leva max':>9} {'leva usata':>11} {'CAGR lordo':>11} "
f"{'costo leva':>11} {'CAGR netto':>11} {'EUR/g':>7} {'rendita':>9} {'EUR/g rend.':>12}")
best2 = None
for f_ib, b, _ in rows:
k_want = min(0.15 / b["vol"], 3.0)
k_max = f_ib * 2.0 + (1.0 - f_ib) * 3.0 # vincolo pesato per venue
k = min(k_want, k_max)
borrow_rate = f_ib * IB_MARGIN_RATE + (1.0 - f_ib) * DERIBIT_FUNDING
fin = max(k - 1.0, 0.0) * borrow_rate # costo sulla quota presa a prestito
cagr_gross = b["metrics"]["cagr"] * k
cagr_net = cagr_gross - fin
pr = max(perp_rate(b["combo"], k=k) - fin, 0.0) # il costo grava anche sul prelievo
eur_p = total * pr * (1.0 - b["tax"]) / EURUSD / 365.0
eur = total * cagr_net * (1.0 - b["tax"]) / EURUSD / 365.0
flag = " (leva TAGLIATA)" if k < k_want - 1e-9 else ""
print(f" {f_ib:>11.0%} {k_max:>8.2f}x {k:>10.2f}x {cagr_gross:>11.1%} "
f"{fin:>11.1%} {cagr_net:>11.1%} {eur:>7.2f} {pr:>9.2%} {eur_p:>12.2f}{flag}")
if best2 is None or eur_p > best2[1]:
best2 = (f_ib, eur_p, k, pr)
print(f"\n -> con leva ATTUABILE E costo del margine il migliore e' {best2[0]:.0%} su IB "
f"(leva {best2[2]:.2f}x, rendita {best2[3]:.2%} = EUR {best2[1]:.2f}/g)")
# --- il confronto che conta: GTAA vs LIQUIDITA' FERMA ---------------------
print("\n" + "-" * 100)
print(" IL CONFRONTO ONESTO: GTAA01 sui 10k vs la stessa liquidita' FERMA che prende interesse")
print("-" * 100)
b94 = [b for f, b, _ in rows if abs(f - 0.94) < 1e-9][0]
cash_eur_day = total / EURUSD * IB_CASH_RATE * (1 - TAX_EQUITY) / 365
print(f" 10k investiti in GTAA01 (+ $600 su Deribit) : EUR {b94['eur_day']:.2f}/g "
f"maxDD {b94['metrics']['maxdd']:.1%}")
print(f" 10k FERMI a interesse ~{IB_CASH_RATE:.0%} (+ $600 Deribit): EUR "
f"{cash_eur_day + rows[0][1]['eur_day']*DERIBIT_NOW/total:.2f}/g maxDD ~0%")
print(f" -> il premio di GTAA01 sopra la liquidita' e' ~EUR "
f"{b94['eur_day'] - cash_eur_day:.2f}/g, pagato con un maxDD del "
f"{b94['metrics']['maxdd']:.0%}.")
print(" (tasso liquidita' = assunzione dichiarata; un BTP/monetario EUR renderebbe di piu'.)")
if __name__ == "__main__":
main()
+201
View File
@@ -0,0 +1,201 @@
"""r0725_prop_config — QUALE BOOK mandare su un conto funded (2026-07-25).
Il 24/07 (`r0724_goal50_math.py` parte C + `r0724_goal50_intraday_mc.py`) ha stabilito che il
conto funded e' l'unico canale che moltiplica il NOZIONALE senza capitale proprio, e ha
quantificato P(pass) e payout — ma sempre e solo per il **book a 2 sleeve** (TP01+SKH01),
perche' e' quello eseguibile a $600 sul conto Deribit.
Il punto che quell'analisi non ha visto: **su un conto funded il capitale non e' piu' $600, e'
$100k** — quindi gli sleeve che a casa nostra sono STAT-MODE per taglia (XS01 a ~$20k) DIVENTANO
eseguibili. E le regole prop premiano esattamente cio' che la diversificazione produce: passano
sul DRAWDOWN e sulla perdita giornaliera, NON sul CAGR. Un book a Sharpe piu' alto e vol piu'
bassa dovrebbe passare molto piu' spesso, a parita' di tutto il resto.
Confronta tre configurazioni CRYPTO-ONLY (cio' che un sub-account di prop firm puo' davvero
eseguire — niente GTAA01 che e' equity su IB, niente VRP01 che e' opzioni + regola permanente):
A) TP01 75 / SKH01 25 — il book live attuale (quello valutato il 24/07)
B) A + XS01 — cross-sectional 19 alt perp: eseguibile a $100k, NON a $600
C) B + XSR01 — contro-fattuale (gate pre-registrato 2026-10-23, non deployato)
ONESTA' (limiti dichiarati, ereditati e nuovi):
1. **Close-only**: il MC gira sulle CHIUSURE giornaliere. C-bis del 24/07 ha dimostrato che
i wick intraday tagliano 6-37pp di P(pass) -> **i livelli ASSOLUTI qui sono un TETTO**.
Il ricostruttore MTM intraday esiste solo per TP01+SKH01 (feed 5m); XS01/XSR01 sono 1d
nativi su Hyperliquid e il loro intraday NON e' ricostruibile dai nostri dati -> non lo
invento. Cio' che questo script misura onestamente e' il **DELTA fra configurazioni**,
e il delta e' CONSERVATIVO: i book B/C hanno vol piu' bassa, quindi wick piu' piccoli,
quindi subirebbero un haircut intraday MINORE di A.
2. **Finestra comune**: XS01 parte dal 2024 -> il confronto principale gira sui giorni in cui
TUTTI gli sleeve esistono (2024+, ~2.5 anni). Riportata anche la finestra piena (outer-join)
dove pre-2024 i book B/C degradano su A.
3. De-luck x0.6 sul drift (convenzione del progetto) come caso di decisione.
4. SKH01 sul PATH LIVE (fill orario), non sul backtest.
Uso: `uv run python scripts/research/r0725_prop_config.py`
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "scripts" / "research"))
from src.portfolio.portfolio import combine_outer, metrics # noqa: E402
from src.portfolio.sleeves import _xsec_returns # noqa: E402
import r0724_skh_live_weight as skl # noqa: E402
from r0725_capcurve import EURUSD, TAX_RATE, tp01_realistic # noqa: E402
RNG = np.random.default_rng(20260725)
PROP_NOTIONAL = 100_000.0
SPLIT = 0.80 # profit split tipico (80% al trader)
RULES = [
("HYRO 1-step tgt10% DD6% dl4%", 0.10, 0.06, 0.04),
("FTMO Swing tgt10% DD10% dl5%", 0.10, 0.10, 0.05),
]
FUNDED_RULES = [("HYRO max-loss 6%", 0.06, 0.04),
("FTMO max-loss 10%", 0.10, 0.05)]
# ---------------------------------------------------------------- serie book
def _d(s: pd.Series) -> pd.Series:
s = s.dropna().sort_index()
if s.index.tz is None:
s.index = s.index.tz_localize("UTC")
return s
def build_configs(deluck: float = 0.6) -> dict:
"""Le tre configurazioni, su griglia giornaliera, de-luckate sul drift."""
skh = _d(skl.skh_book(0, "hourly"))
tp = _d(tp01_realistic(PROP_NOTIONAL * 0.75))
xs = _d(_xsec_returns()).resample("1D").apply(lambda x: (1 + x).prod() - 1)
cfgs = {
"A TP01/SKH01 (live oggi)": ({"TP01": tp, "SKH01": skh},
{"TP01": 0.75, "SKH01": 0.25}),
"B + XS01 (19 alt perp)": ({"TP01": tp, "SKH01": skh, "XS01": xs},
{"TP01": 0.55, "SKH01": 0.20, "XS01": 0.25}),
}
out = {}
for name, (cols, w) in cfgs.items():
s = combine_outer(cols, w, lo=pd.Timestamp("2019-03-14", tz="UTC"))
s = s - (1.0 - deluck) * float(s.mean())
out[name] = dict(full=s, cols=cols, w=w)
return out
def common_window(cfgs: dict) -> pd.Timestamp:
"""Primo giorno in cui TUTTI gli sleeve di TUTTE le config hanno dato."""
starts = []
for v in cfgs.values():
for s in v["cols"].values():
starts.append(s.index.min())
return max(starts)
# ------------------------------------------------------------------ MC eval
def _paths(r: np.ndarray, n_days: int, n_paths: int, block: int = 20) -> np.ndarray:
n_blocks = int(np.ceil(n_days / block))
starts = RNG.integers(0, len(r) - block, size=(n_paths, n_blocks))
idx = (starts[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :n_days]
return r[idx]
def sim_eval(r: np.ndarray, target: float, max_dd: float, daily_loss: float,
k: float, max_days: int = 365, n_paths: int = 8000) -> dict:
R = _paths(r, max_days, n_paths) * k
eq = np.cumprod(1 + R, axis=1)
eq_start = eq / (1 + R)
fail = (eq_start * (1 + R) < (1 - max_dd)) | (R < -daily_loss)
passed = eq >= 1 + target
ff = np.where(fail.any(axis=1), fail.argmax(axis=1), max_days + 1)
fp = np.where(passed.any(axis=1), passed.argmax(axis=1), max_days + 1)
ok = fp < ff
return dict(p_pass=float(ok.mean()),
median_days=float(np.median(fp[ok])) if ok.any() else float("nan"))
def sim_funded(r: np.ndarray, max_loss: float, daily_loss: float, k: float,
years: int = 1, n_paths: int = 8000) -> dict:
"""Fase funded con regole REALI: max-loss **STATICO** (misurato dal saldo iniziale del
conto, non da un picco mobile) + daily-loss; prelievo mensile dell'80% del profitto,
dopo il quale il saldo torna alla base (e con esso il buffer di perdita).
NB: la base di prelievo si aggiorna SOLO al prelievo. Aggiornarla giorno per giorno
(come un HWM) azzera per costruzione il guadagno misurato al checkpoint -> payout ~0.
E' l'errore che questo modello ha commesso al primo giro ed e' stato corretto.
"""
n_days = years * 365
R = _paths(r, n_days, n_paths) * k
eq = np.ones(n_paths) # equity normalizzata alla BASE del conto
alive = np.ones(n_paths, bool)
payout = np.zeros(n_paths)
for t in range(n_days):
eq = np.where(alive, eq * (1 + R[:, t]), eq)
bust = alive & ((eq < 1.0 - max_loss) | (R[:, t] < -daily_loss))
alive &= ~bust
if (t + 1) % 30 == 0:
gain = np.where(alive & (eq > 1.0), eq - 1.0, 0.0)
payout += gain * SPLIT
eq = np.where(alive & (eq > 1.0), 1.0, eq) # profitto prelevato, base ripristinata
usd = payout * PROP_NOTIONAL
return dict(p_alive=float(alive.mean()),
e_payout=float(usd.mean()),
med_payout=float(np.median(usd)),
eur_day_mean=float(usd.mean() * (1 - TAX_RATE) / EURUSD / 365.0),
eur_day_med=float(np.median(usd) * (1 - TAX_RATE) / EURUSD / 365.0))
def main() -> None:
print("=" * 104)
print(" QUALE BOOK su un conto FUNDED — il 24/07 ha valutato solo il book a 2 sleeve")
print("=" * 104)
cfgs = build_configs()
lo = common_window(cfgs)
print(f"\n finestra comune (tutti gli sleeve esistono): dal {lo.date()}")
print(" NB: livelli ASSOLUTI = TETTO (close-only; C-bis 24/07: i wick tagliano 6-37pp).")
print(" Il DELTA fra config e' la misura onesta, ed e' CONSERVATIVO (B ha vol minore).\n")
for win, wlabel in ((lo, "FINESTRA COMUNE (2024+)"), (None, "FINESTRA PIENA (outer-join)")):
print("-" * 104)
print(f" {wlabel}")
print("-" * 104)
stats = {}
for name, v in cfgs.items():
s = v["full"] if win is None else v["full"][v["full"].index >= win]
m = metrics(s)
vol = float(s.std() * np.sqrt(365))
stats[name] = (s.values.astype(float), m, vol)
print(f" {name:>28} Sharpe {m['sharpe']:>5.2f} CAGR {m['cagr']:>6.1%} "
f"vol {vol:>5.1%} maxDD {m['maxdd']:>5.1%}")
for rname, tgt, dd, dl in RULES:
print(f"\n EVAL — {rname}")
print(f" {'config':>28} {'leva':>5} {'P(pass)':>8} {'giorni med':>11}")
for name, (r, m, vol) in stats.items():
for k in (0.75, 1.0, 1.5):
e = sim_eval(r, tgt, dd, dl, k)
print(f" {name:>28} {k:>5.2f} {e['p_pass']:>8.1%} "
f"{e['median_days']:>11.0f}")
print(f"\n FUNDED ${PROP_NOTIONAL:,.0f} — 1 anno, split {SPLIT:.0%}, prelievi mensili")
print(f" {'config':>28} {'regola':>20} {'leva':>5} {'P(vivo)':>8} "
f"{'E[payout]':>11} {'EUR/g atteso':>13}")
for name, (r, m, vol) in stats.items():
for fname, ml, dl in FUNDED_RULES:
for k in (0.5, 0.75, 1.0):
f = sim_funded(r, ml, dl, k)
print(f" {name:>28} {fname:>20} {k:>5.2f} {f['p_alive']:>8.1%} "
f"${f['e_payout']:>10,.0f} {f['eur_day_mean']:>13.2f}")
print()
if __name__ == "__main__":
main()
+329
View File
@@ -0,0 +1,329 @@
"""r0725_prop_ladder — I 600 EURO COME BIGLIETTO: la scala di conti funded (2026-07-25).
Domanda del goal: "puoi usare i 600 euro come vuoi, puoi ipotizzare di creare qualcosa".
`r0725_capcurve.py` ha refutato i 600 euro come CAPITALE (rendita 50 EUR/g -> muro $300-500k,
0% dei path in 20 anni senza depositi). Qui i 600 euro sono usati come **biglietto**: si compra
un'evaluation, il conto funded moltiplica il NOZIONALE senza possedere capitale, i payout
comprano altri biglietti. E' l'unica leva che non richiede depositi.
Il 24/07 si era fermato a UN conto ($100k -> ~EUR 14.5/g atteso, cap $200k per firm). Ma
**50 EUR/g richiede piu' conti in parallelo**, e qui c'e' il problema che nessuno ha ancora
studiato: **N conti che girano lo STESSO book bustano INSIEME** (correlazione 1.0). La
diversificazione fra conti e' illusoria salvo assegnare **sleeve DIVERSI a conti diversi**.
Da cui la domanda di ricerca vera, che e' un problema di costruzione di portafoglio sotto
barriera di rovina PER-CONTO:
- CONCENTRARE: ogni conto gira il book diversificato (Sharpe alto per conto -> P(vivo) alta
per conto, ma i conti sono perfettamente correlati -> o vivono tutti o muoiono tutti);
- SPARPAGLIARE: ogni conto gira UNO sleeve (Sharpe basso per conto -> P(vivo) piu' bassa,
ma le morti sono INDIPENDENTI -> il reddito aggregato e' molto piu' stabile).
Non e' ovvio quale vinca: la barriera di rovina e' convessa, il reddito e' lineare.
Modello (tutto dichiarato):
* bootstrap a blocchi CONGIUNTO dei 3 sleeve (stessi indici di blocco per tutti -> la
correlazione fra sleeve e' quella REALE, non imposta);
* finestra comune 2024+ (XS01 nasce li'); de-luck x0.6 sul drift;
* eval -> funded con le regole vere (max-loss STATICO, daily-loss), payout mensile 80%,
prelievo immediato (mai lasciare credito alla firm: regola del 24/07);
* bust = biglietto perso; la cassa ricompra se se lo puo' permettere;
* cap per firm; **rischio controparte**: ogni anno una firm puo' sparire (il settore ha perso
il 13% delle firm nel 2024) -> i conti aperti su quella firm valgono zero;
* fisco 33% sui payout.
ONESTA' — il risultato va letto come una BANDA, non come un punto. Si gira con due lenti:
* CLOSE-ONLY = **TETTO OTTIMISTA**: le regole prop scattano sull'equity intraday, non sulla
chiusura (C-bis del 24/07: i wick tagliano fino a 37pp di P(pass) e 48pp di P(vivo)).
* INTRADAY = **PAVIMENTO PESSIMISTA**: il gap e' lognormale calibrato sui percentili del recon
MTM 1h, ma estratto **INDIPENDENTE dal rendimento del giorno**. Nella realta' i wick profondi
stanno sulle giornate brutte (sono ACCOPPIATI): estrarli indipendenti aggiunge finti tuffi
anche nelle giornate buone -> breach spuri -> stima troppo severa. Il modo giusto e' il
bootstrap delle TUPLE (ritorno, gap) come in `r0724_goal50_intraday_mc.py`, che pero' ha il
recon solo per TP01+SKH01 (XS01 e' 1d nativo su Hyperliquid, intraday non ricostruibile).
-> FOLLOW-UP dichiarato, non simulato qui.
La verita' sta FRA le due lenti. Il confronto fra POLITICHE, invece, e' robusto: e' la stessa
lente applicata a tutte. La finestra 2024+ e' anche quella di scoperta di XS01 -> le config che
lo contengono sono favorite; "CONC-2SL" (solo TP01/SKH01) e' il controllo pulito.
Uso: `uv run python scripts/research/r0725_prop_ladder.py`
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "scripts" / "research"))
from src.portfolio.portfolio import metrics # noqa: E402
from src.portfolio.sleeves import _xsec_returns # noqa: E402
import r0724_skh_live_weight as skl # noqa: E402
from r0725_capcurve import EURUSD, TAX_RATE, tp01_realistic # noqa: E402
TARGET_EUR_DAY = 50.0
DELUCK = 0.6
SPLIT = 0.80
FIRM_DEATH_PER_YEAR = 0.10 # il settore ha perso il 13% delle firm nel 2024 (ricerca 24/07)
START_EUR = 600.0
HORIZON_MONTHS = 36
MAX_CONCURRENT = 6
# (nome, [(nozionale, prezzo biglietto)], eval(target,dd,daily), funded(max_loss,daily), cap)
FIRMS = [
dict(name="HYRO", tickets=[(25_000, 249.0), (50_000, 379.0), (100_000, 579.0)],
ev=(0.10, 0.06, 0.04), fu=(0.06, 0.04), cap=200_000),
dict(name="FTMO", tickets=[(50_000, 345.0), (100_000, 540.0), (200_000, 1_080.0)],
ev=(0.10, 0.10, 0.05), fu=(0.10, 0.05), cap=400_000),
]
# ------------------------------------------------------------------ sleeve
def _d(s: pd.Series) -> pd.Series:
s = s.dropna().sort_index()
if s.index.tz is None:
s.index = s.index.tz_localize("UTC")
return s
def sleeve_panel() -> tuple[np.ndarray, list[str]]:
"""Matrice (giorni x sleeve) sulla finestra comune, de-luckata sul drift."""
tp = _d(tp01_realistic(100_000 * 0.75))
skh = _d(skl.skh_book(0, "hourly"))
xs = _d(_xsec_returns()).resample("1D").apply(lambda x: (1 + x).prod() - 1)
J = pd.concat({"TP01": tp, "SKH01": skh, "XS01": xs}, axis=1, join="inner").fillna(0.0)
M = J.to_numpy(float)
M = M - (1.0 - DELUCK) * M.mean(axis=0, keepdims=True) # de-luck: taglia il drift
return M, list(J.columns)
# --------------------------------------------------------------- politiche
def policies(names: list[str]) -> dict:
"""Ogni politica = lista di vettori-peso sugli sleeve, uno per 'slot' di conto.
Gli slot si riempiono in ordine man mano che la cassa permette biglietti."""
i = {n: k for k, n in enumerate(names)}
n = len(names)
def w(**kw):
v = np.zeros(n)
for k, x in kw.items():
v[i[k]] = x
return v
book_div = w(TP01=0.55, SKH01=0.20, XS01=0.25)
book_2sl = w(TP01=0.75, SKH01=0.25)
return {
"CONC-2SL ogni conto = book live (TP01/SKH01)": [book_2sl] * MAX_CONCURRENT,
"CONC-DIV ogni conto = book diversificato": [book_div] * MAX_CONCURRENT,
"SPARSO 1 sleeve per conto (a rotazione)": [w(TP01=1.0), w(XS01=1.0), w(SKH01=1.0),
w(TP01=1.0), w(XS01=1.0), w(SKH01=1.0)],
"MISTO meta' diversificati, meta' single": [book_div, w(XS01=1.0), book_div,
w(TP01=1.0), book_div, w(XS01=1.0)],
}
# ------------------------------------------------------------------- sim
# --- geometria dei WICK (dal recon MTM 1h del book, r0724_goal50_intraday_mc.py):
# gap = (minimo intraday - chiusura), in punti di equity: p50 -0.17pp, p10 -1.03pp, p1 -2.7pp.
# Le regole prop scattano sull'equity INTRADAY, non sulla chiusura -> ignorarli gonfia P(pass)
# e P(vivo) (C-bis 24/07: fino a -37pp e -48pp). Qui il gap e' lognormale calibrato su quei
# percentili e SCALATO sulla vol della configurazione (un book meno volatile ha wick minori).
WICK_MU, WICK_SIGMA = np.log(0.0017), 1.30
WICK_REF_VOL = 0.114 # vol annua del book TP01/SKH01 su cui la geometria e' misurata
def wick_percentiles() -> dict:
"""Percentili impliciti della calibrazione — stampati per essere verificabili."""
q = {p: float(np.exp(WICK_MU + WICK_SIGMA * np.sqrt(2) * _erfinv(2 * p - 1)))
for p in (0.50, 0.90, 0.99)}
return q
def _erfinv(x: float) -> float:
from math import sqrt
from statistics import NormalDist
return NormalDist().inv_cdf((x + 1) / 2) / sqrt(2)
def simulate(M: np.ndarray, slots: list[np.ndarray], n_paths: int, lev: float,
seed: int, block: int = 20, intraday: bool = False) -> dict:
"""Una politica, n_paths traiettorie di HORIZON_MONTHS mesi.
Stato per conto: fase (0=eval, 1=funded), equity normalizzata alla base, firm, nozionale.
Il bootstrap dei ritorni degli sleeve e' CONGIUNTO (stesso blocco per tutti gli sleeve e
per tutti i conti dello stesso path) -> due conti che girano lo stesso book sono
perfettamente correlati, due conti su sleeve diversi hanno la correlazione REALE.
"""
rng = np.random.default_rng(seed)
n_days = HORIZON_MONTHS * 30
n_sl = M.shape[1]
n_blocks = int(np.ceil(n_days / block))
starts = rng.integers(0, len(M) - block, size=(n_paths, n_blocks))
idx = (starts[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :n_days]
R = M[idx] # (paths, days, sleeve)
n_slot = len(slots)
W = np.stack(slots) # (slot, sleeve)
# ritorni per (path, day, slot) = R . W^T, con leva
Rs = np.einsum("pds,ks->pdk", R, W) * lev
# gap intraday per (path, day, slot), scalato sulla vol della config: un book meno volatile
# ha wick proporzionalmente piu' piccoli. Il breach si valuta sul MINIMO (chiusura + gap).
if intraday:
vol_k = Rs.reshape(-1, n_slot).std(axis=0) * np.sqrt(365.0)
scale = np.clip(vol_k / WICK_REF_VOL, 0.0, 3.0)
G = -np.exp(rng.normal(WICK_MU, WICK_SIGMA, size=Rs.shape)) * scale[None, None, :]
else:
G = None
cash = np.full(n_paths, START_EUR * EURUSD)
phase = np.full((n_paths, n_slot), -1, np.int8) # -1 vuoto, 0 eval, 1 funded
eq = np.ones((n_paths, n_slot))
firm = np.full((n_paths, n_slot), -1, np.int8)
notional = np.zeros((n_paths, n_slot))
days_in = np.zeros((n_paths, n_slot), int)
firm_alive = np.ones((n_paths, len(FIRMS)), bool)
cum_payout = np.zeros(n_paths) # netto fisco, USD
payout_last_year = np.zeros(n_paths)
spent = np.zeros(n_paths)
p_death_day = 1.0 - (1.0 - FIRM_DEATH_PER_YEAR) ** (1 / 365.0)
for t in range(n_days):
# --- morte di una firm (rischio controparte)
died = rng.random((n_paths, len(FIRMS))) < p_death_day
newly_dead = died & firm_alive
firm_alive &= ~died
if newly_dead.any():
for fi in range(len(FIRMS)):
kill = newly_dead[:, fi]
if kill.any():
hit = (firm == fi) & kill[:, None]
phase[hit] = -1
notional[hit] = 0.0
# --- acquisto biglietti negli slot liberi
for k in range(n_slot):
free = (phase[:, k] == -1)
if not free.any():
continue
for fi, F in enumerate(FIRMS):
used = np.where((firm == fi), notional, 0.0).sum(axis=1)
for (size, price) in sorted(F["tickets"], key=lambda x: -x[0]):
buy = (free & firm_alive[:, fi] & (cash >= price)
& (used + size <= F["cap"]))
if not buy.any():
continue
cash[buy] -= price
spent[buy] += price
phase[buy, k] = 0
eq[buy, k] = 1.0
firm[buy, k] = fi
notional[buy, k] = size
days_in[buy, k] = 0
free = free & ~buy
if not free.any():
break
if not free.any():
break
# --- dinamica dei conti
r = Rs[:, t, :] # (paths, slot)
act = phase >= 0
eq_prev = eq
eq = np.where(act, eq * (1.0 + r), eq)
# equity peggiore TOCCATA nella giornata: e' su questa che scattano le regole prop
if G is None:
eq_low, r_low = eq, r
else:
eq_low = np.where(act, eq_prev * (1.0 + r + G[:, t, :]), eq)
r_low = r + G[:, t, :]
days_in += act
for fi, F in enumerate(FIRMS):
sel = act & (firm == fi)
if not sel.any():
continue
ev_t, ev_dd, ev_dl = F["ev"]
fu_ml, fu_dl = F["fu"]
in_ev = sel & (phase == 0)
in_fu = sel & (phase == 1)
# breach — valutato sull'equity INTRADAY (eq_low), il target sulla CHIUSURA
bust_ev = in_ev & ((eq_low < 1.0 - ev_dd) | (r_low < -ev_dl))
bust_fu = in_fu & ((eq_low < 1.0 - fu_ml) | (r_low < -fu_dl))
bust = bust_ev | bust_fu
phase[bust] = -1
notional[bust] = 0.0
# pass eval
passed = in_ev & (eq >= 1.0 + ev_t) & ~bust
phase[passed] = 1
eq[passed] = 1.0
# --- payout mensile (prelievo immediato, base ripristinata)
if (t + 1) % 30 == 0:
fu = phase == 1
gain = np.where(fu & (eq > 1.0), (eq - 1.0) * notional, 0.0)
gross = gain.sum(axis=1) * SPLIT
net = gross * (1.0 - TAX_RATE)
cum_payout += net
payout_last_year += net
cash += net
eq = np.where(fu & (eq > 1.0), 1.0, eq)
if (t + 1) % 365 == 0 and t + 1 < n_days:
payout_last_year[:] = 0.0
eur_day = cum_payout / EURUSD / (n_days / 365.0) / 365.0
live_notional = np.where(phase == 1, notional, 0.0).sum(axis=1)
return dict(eur_day=eur_day,
p_target=float((eur_day >= TARGET_EUR_DAY).mean()),
p_ge10=float((eur_day >= 10.0).mean()),
p_zero=float((cum_payout <= 0).mean()),
med=float(np.median(eur_day)),
p90=float(np.percentile(eur_day, 90)),
p10=float(np.percentile(eur_day, 10)),
med_notional=float(np.median(live_notional)),
med_spent=float(np.median(spent)))
def main() -> None:
M, names = sleeve_panel()
print("=" * 104)
print(" I 600 EURO COME BIGLIETTO — scala di conti funded, e il problema della correlazione")
print("=" * 104)
print(f"\n finestra comune: {len(M)} giorni | sleeve: {names} | de-luck x{DELUCK}")
print(" correlazioni fra sleeve (le morti dei conti sono correlate quanto lo sono queste):")
C = np.corrcoef(M.T)
for i, n in enumerate(names):
print(" " + f"{n:>7} " + " ".join(f"{C[i, j]:>6.2f}" for j in range(len(names))))
for i, n in enumerate(names):
m = metrics(pd.Series(M[:, i], index=pd.date_range("2024-01-01", periods=len(M), tz="UTC")))
print(f" {n:>7}: Sharpe {m['sharpe']:>5.2f} CAGR {m['cagr']:>6.1%} maxDD {m['maxdd']:>5.1%}")
print(f"\n orizzonte {HORIZON_MONTHS} mesi | cassa iniziale EUR {START_EUR:.0f} | "
f"max {MAX_CONCURRENT} conti | rischio-morte firm {FIRM_DEATH_PER_YEAR:.0%}/anno")
print(" NB: livelli ASSOLUTI = TETTO (close-only). Il confronto fra POLITICHE e' la misura onesta.\n")
q = wick_percentiles()
print(f" wick calibrati: p50 -{q[0.50]*100:.2f}pp p90 -{q[0.90]*100:.2f}pp "
f"p99 -{q[0.99]*100:.2f}pp (target dal recon 24/07: -0.17 / -1.03 / -2.70)")
pols = policies(names)
for intraday in (False, True):
lens = "INTRADAY (wick: la lente onesta)" if intraday else "CLOSE-ONLY (tetto ottimista)"
for lev in (0.75, 1.0):
print("-" * 104)
print(f" {lens} — LEVA {lev:.2f}x")
print("-" * 104)
print(f" {'politica':>46} {'EUR/g med':>10} {'p10':>7} {'p90':>7} "
f"{'P(>=10/g)':>10} {'P(>=50/g)':>10} {'P(zero)':>8}")
for pname, slots in pols.items():
r = simulate(M, slots, n_paths=3000, lev=lev, seed=20260725, intraday=intraday)
print(f" {pname:>46} {r['med']:>10.2f} {r['p10']:>7.2f} {r['p90']:>7.2f} "
f"{r['p_ge10']:>10.1%} {r['p_target']:>10.1%} {r['p_zero']:>8.1%}")
print()
if __name__ == "__main__":
main()