4703f75f9f
Tutte le traiettorie del 25-26/07 assumevano versamento PIATTO, ININTERROTTO, PER SEMPRE: l'ipotesi meno realistica dell'intero piano. Misurate le deviazioni che succedono davvero, con la stessa macchineria (block bootstrap sui ritorni reali del book live, fattore d'ancora x0.89 misurato). (1) SMETTERE — il costo non e' proporzionale ai soldi mancanti. EUR 250/m per K anni poi stop, orizzonte 20a: 3a ($10.410) -> $202.771 / P(muro) 32.7%; 5a -> $287.081 / 53.3%; 20a ($66.818) -> $496.778 / 90.0%. I PRIMI 5 ANNI SONO IL 25% DEI SOLDI E IL 58% DEL RISULTATO -> un'interruzione al 12° anno costa poco, una al 3° quasi tutto. Argomento per partire con un importo sostenibile invece che ambizioso. (2) CRESCENTE E' PEGGIO DI PIATTO A PARI SOLDI. EUR 150/m +5%/anno versa EUR 67.998 -> $401.889; piatto EUR 250 versa EUR 66.818 -> $496.778 = +24% con gli stessi soldi, solo perche' entrano prima. Metrica giusta per confrontare piani di taglia diversa = $ finale / $ versato (piatti 7.4x, crescenti 5.1-5.9x). (3) STESSO TOTALE, CALENDARIO DIVERSO = FATTORE 6. EUR 60.000 distribuiti: ultimi 10 anni $178.494 (11%) / piatto 20a $496.778 (90%) / primi 5 anni $1.104.587 (99.4%). NON significa "versa tutto subito": un piano che non si sostiene non e' un piano. Verificato COL RISCHIO DI VENUE DENTRO (il front-load mette piu' capitale sull'exchange prima = proprio il rischio del giorno): REGGE, 2.22x -> 2.09x a p=2%, perche' il rischio colpisce il tempo, non il calendario. MA a p=5% il capitale mediano e' $0 PER OGNI CALENDARIO -> formulazione piu' netta del rischio di venue trovata finora: non erode il piano, lo CANCELLA. (4) LA DOMANDA INVERSA — rendita netta EUR/g mediana per versamento e orizzonte. EUR 150/m -> 23.96/g a 15 anni; EUR 250/m -> 39.11/g a 15a e 91.30/g a 20a (P(EUR 50/g) 90%). Riformula l'obiettivo: EUR 50/g e' UN punto sulla griglia, non l'unico risultato. Non-linearita': da 15 a 20 anni la rendita piu' che raddoppia a ogni livello. (5) FREQUENZA = la decisione meno importante. Mensile fino a ~$2/trasferimento, bimestrale sopra; differenze 1-3% del capitale finale. Verificato che un deposito NON resta strozzato: col cap dinamico cap = equity/2 = il nozionale massimo richiedibile. ORDINE DI IMPORTANZA: versare o no (da mai a 16 anni) > quando (6x) > quanto presto si smette (5 anni = 58% del risultato) > piatto vs crescente (24%) > frequenza (1-3%). Book, pesi, cron, config INVARIATI. 377 test verdi (+12). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
256 lines
13 KiB
Python
256 lines
13 KiB
Python
#!/usr/bin/env python
|
||
"""r0726_deposits.py — il piano dei VERSAMENTI, sotto le ipotesi che il piano non ha mai fatto.
|
||
|
||
CONTESTO. La rivalutazione di fine giornata ha misurato che il book e' dentro il suo plateau su
|
||
ogni asse (ottimizzare il peso = +0.030 Sharpe, gate fallito) mentre i versamenti valgono la
|
||
differenza fra **mai** e **16 anni**. Quindi il vincolo binding non e' piu' la strategia.
|
||
|
||
Ma tutte le traiettorie calcolate finora (25/07 e 26/07) assumono la stessa cosa:
|
||
**versamento PIATTO, ININTERROTTO, PER SEMPRE**. E' l'ipotesi meno realistica del piano intero.
|
||
Qui si misurano le quattro deviazioni che contano davvero, tutte con la stessa macchineria
|
||
(block bootstrap sui ritorni REALI del book live, fattore d'ancora ×0.89 misurato).
|
||
|
||
(A) INTERRUZIONE — si versa per K anni e poi si smette. E' la deviazione piu' probabile di
|
||
tutte (lavoro, spese, vita). Quanto costa smettere, e quando smettere fa meno male?
|
||
(B) CRESCITA — €150/mese che crescono del 5%/anno contro €250/mese piatti. Piu' realistico
|
||
di un importo fisso per vent'anni, e la domanda e' se conviene A PARI TOTALE VERSATO.
|
||
(C) FRONT vs BACK LOADING — stesso totale, calendario diverso. Dice se vale la pena
|
||
sforzarsi ADESSO invece che dopo, e di quanto.
|
||
(D) LA DOMANDA INVERSA — invece di "quando arrivo a €50/g", **"che rendita compro con quello
|
||
che posso permettermi?"**. Se €50/g non e' raggiungibile, il numero utile e' l'altro.
|
||
|
||
uv run python scripts/research/r0726_deposits.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"))
|
||
|
||
import r0725_capcurve as CC # noqa: E402
|
||
import r0726_capwall_refresh as WR # noqa: E402
|
||
|
||
DELUCK = 0.89 # fattore d'ancora MISURATO (r0726_deluck_factor), non scelto a occhio
|
||
START = 600.0
|
||
N_PATHS = 4000
|
||
BLOCK = 20
|
||
SEED = 20260726
|
||
|
||
|
||
def deluck_returns() -> np.ndarray:
|
||
"""Ritorni giornalieri del book live con il drift ridotto del fattore d'ancora misurato."""
|
||
base = CC.book_series("hourly")
|
||
mu = float(base.values.mean())
|
||
return (base - (1.0 - DELUCK) * mu).values.astype(float)
|
||
|
||
|
||
def simulate(paths: np.ndarray, schedule: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
||
"""Capitale finale e totale versato, dato un CALENDARIO di versamenti giornaliero (USD/giorno).
|
||
|
||
`schedule[t]` = quanto entra il giorno t. Vettoriale su tutti i path insieme.
|
||
Ritorna (capitale finale per path, totale versato — uguale per tutti, ma ritornato come
|
||
scalare replicato per rendere impossibile confrontare per sbaglio grandezze diverse).
|
||
"""
|
||
n_paths, n_days = paths.shape
|
||
cap = np.full(n_paths, START)
|
||
for t in range(n_days):
|
||
cap = cap * (1.0 + paths[:, t])
|
||
if schedule[t]:
|
||
cap += schedule[t]
|
||
return cap, np.full(n_paths, START + float(schedule.sum()))
|
||
|
||
|
||
def monthly_schedule(n_days: int, eur_month: float, stop_year: float | None = None,
|
||
growth: float = 0.0) -> np.ndarray:
|
||
"""Calendario: versamento ogni 30 giorni, opzionalmente crescente e/o interrotto."""
|
||
s = np.zeros(n_days)
|
||
for t in range(30, n_days, 30):
|
||
if stop_year is not None and t >= stop_year * 365:
|
||
break
|
||
yrs = t / 365.0
|
||
s[t] = eur_month * CC.EURUSD * ((1.0 + growth) ** yrs)
|
||
return s
|
||
|
||
|
||
def rent_from_capital(cap: np.ndarray, perp_rate: float) -> np.ndarray:
|
||
"""Rendita NETTA in €/giorno da un capitale, al tasso di prelievo perpetuo misurato."""
|
||
return cap * perp_rate * (1 - CC.TAX_RATE) / 365.0 / CC.EURUSD
|
||
|
||
|
||
def main() -> None:
|
||
print("=" * 100)
|
||
print(" r0726 — I VERSAMENTI: interruzione, crescita, calendario, e la domanda inversa")
|
||
print("=" * 100)
|
||
|
||
r = deluck_returns()
|
||
gross = CC.TARGET_EUR_DAY * 365 * CC.EURUSD / (1 - CC.TAX_RATE)
|
||
_, perp, wall = WR.perp_and_wall(r, 1.0, gross)
|
||
print(f" book live de-luckato ×{DELUCK:.2f} | rendita perpetua {perp:.2%} | "
|
||
f"muro €{CC.TARGET_EUR_DAY:.0f}/g = ${wall:,.0f}")
|
||
print(f" block bootstrap, {N_PATHS} path, blocchi da {BLOCK} giorni, partenza ${START:.0f}")
|
||
|
||
rng = np.random.default_rng(SEED)
|
||
YEARS = 20
|
||
n_days = 365 * YEARS
|
||
paths = CC._boot_paths(r, N_PATHS, n_days, BLOCK, rng)
|
||
|
||
# ------------------------------------------------------------ (A) interruzione
|
||
print("\n" + "=" * 100)
|
||
print(" (A) SMETTERE DI VERSARE — €250/mese per K anni, poi stop. Orizzonte 20 anni.")
|
||
print("=" * 100)
|
||
print(f"\n {'versi per':>10}{'totale versato':>16}{'cap. mediano a 20a':>21}"
|
||
f"{'P(muro entro 20a)':>19}{'rendita mediana':>17}")
|
||
ref = None
|
||
for k in (3, 5, 8, 10, 15, 20):
|
||
sch = monthly_schedule(n_days, 250.0, stop_year=k)
|
||
cap, paid = simulate(paths, sch)
|
||
p_wall = float((cap >= wall).mean())
|
||
rent = float(np.median(rent_from_capital(cap, perp)))
|
||
if k == 20:
|
||
ref = (float(np.median(cap)), p_wall)
|
||
print(f" {k:>8}a {paid[0]:>15,.0f}${np.median(cap):>20,.0f}"
|
||
f"{p_wall:>18.1%}{rent:>15.2f} €/g")
|
||
print("\n Lettura: la curva NON e' proporzionale al totale versato — gli anni iniziali")
|
||
print(" valgono di piu' perche' compongono piu' a lungo. Smettere presto costa il doppio")
|
||
print(" di quanto suggerisca il totale mancante.")
|
||
|
||
# ------------------------------------------------------------ (B) crescita
|
||
print("\n" + "=" * 100)
|
||
print(" (B) VERSAMENTO CRESCENTE vs PIATTO — a confronto anche A PARI TOTALE VERSATO")
|
||
print("=" * 100)
|
||
plans = [("piatto €250", 250.0, 0.0), ("piatto €300", 300.0, 0.0),
|
||
("€150 +5%/anno", 150.0, 0.05), ("€200 +5%/anno", 200.0, 0.05),
|
||
("€150 +8%/anno", 150.0, 0.08)]
|
||
print(f"\n {'piano':<16}{'totale versato':>16}{'cap. mediano':>15}"
|
||
f"{'P(muro)':>10}{'$ finale / $ versato':>22}")
|
||
for lab, eur, g in plans:
|
||
sch = monthly_schedule(n_days, eur, growth=g)
|
||
cap, paid = simulate(paths, sch)
|
||
print(f" {lab:<16}{paid[0]:>15,.0f}${np.median(cap):>14,.0f}"
|
||
f"{float((cap >= wall).mean()):>10.1%}{np.median(cap)/paid[0]:>21.2f}x")
|
||
print("\n L'ultima colonna e' la METRICA GIUSTA per confrontare piani di taglia diversa:")
|
||
print(" quanto capitale finale compra ogni dollaro versato. Un piano crescente versa")
|
||
print(" di piu' TARDI, quindi quei dollari compongono meno.")
|
||
|
||
# ------------------------------------------------------------ (C) calendario
|
||
print("\n" + "=" * 100)
|
||
print(" (C) STESSO TOTALE, CALENDARIO DIVERSO — conviene sforzarsi adesso?")
|
||
print("=" * 100)
|
||
TOT = 250.0 * 12 * 20 # totale del piano di riferimento, in EUR
|
||
print(f"\n Totale fissato: €{TOT:,.0f} (= €250/mese per 20 anni), distribuito diversamente:")
|
||
print(f"\n {'calendario':<28}{'cap. mediano':>15}{'P(muro)':>10}{'vs piatto':>12}")
|
||
layouts = [("piatto su 20 anni", 20, 0), ("tutto nei primi 10 anni", 10, 0),
|
||
("tutto nei primi 5 anni", 5, 0), ("solo negli ultimi 10 anni", 10, 10)]
|
||
base_med = None
|
||
scheds = {}
|
||
for lab, span, delay in layouts:
|
||
eur_m = TOT / (span * 12)
|
||
s = np.zeros(n_days)
|
||
for t in range(30, n_days, 30):
|
||
if delay * 365 <= t < (delay + span) * 365:
|
||
s[t] = eur_m * CC.EURUSD
|
||
scheds[lab] = s
|
||
cap, paid = simulate(paths, s)
|
||
med = float(np.median(cap))
|
||
if base_med is None:
|
||
base_med = med
|
||
print(f" {lab:<28}${med:>14,.0f}{float((cap >= wall).mean()):>10.1%}"
|
||
f"{med/base_med:>11.2f}x")
|
||
print("\n ⚠️ Questo NON dice 'versa tutto subito': dice quanto vale il tempo. Un piano")
|
||
print(" che non riesci a sostenere non e' un piano — il confronto serve a scegliere fra")
|
||
print(" calendari SOSTENIBILI, non a giustificare uno sforzo che si rompe al terzo anno.")
|
||
|
||
# --- e il vantaggio del front-loading regge al RISCHIO DI VENUE? ---
|
||
# Anticipare i versamenti mette PIU' capitale sull'exchange PRIMA: il numero sopra e'
|
||
# calcolato senza il rischio che il conto sparisca, che e' proprio il rischio che cresce con
|
||
# quanto tieni li' e per quanto tempo (r0726_venue_risk). Va misurato, non assunto.
|
||
print(f"\n Lo stesso confronto CON il rischio di venue (salto a zero a probabilita' annua p,")
|
||
print(f" 100% Deribit come da decisione del 26/07):")
|
||
print(f"\n {'calendario':<28}" + "".join(f"{'p=' + f'{p:.1%}':>16}" for p in (0.005, 0.02, 0.05)))
|
||
rng_v = np.random.default_rng(SEED + 1)
|
||
for lab, span, delay in layouts:
|
||
cells = []
|
||
for p in (0.005, 0.02, 0.05):
|
||
haz = 1.0 - (1.0 - p) ** (1.0 / 365.0) # hazard giornaliero -> p annua
|
||
alive = rng_v.random((N_PATHS, n_days)) > haz
|
||
surv = np.cumprod(alive, axis=1) # 1 finche' vivo, 0 dopo il fallimento
|
||
cap = np.full(N_PATHS, START)
|
||
s = scheds[lab]
|
||
for t in range(n_days):
|
||
cap = cap * (1.0 + paths[:, t])
|
||
if s[t]:
|
||
cap += s[t]
|
||
cap = cap * surv[:, t] # il conto sparisce col saldo dentro
|
||
cells.append(float(np.median(cap)))
|
||
print(f" {lab:<28}" + "".join(f"${c:>15,.0f}" for c in cells))
|
||
print("\n Il front-loading resta avanti perche' il rischio di venue colpisce il TEMPO, non")
|
||
print(" il calendario dei versamenti: anticipare non aumenta la probabilita' di essere")
|
||
print(" colpiti, aumenta solo quanto c'e' dentro quando succede — e nel frattempo ha")
|
||
print(" composto di piu'. La conclusione non si ribalta, ma il margine si assottiglia.")
|
||
|
||
# ------------------------------------------------------------ (D) domanda inversa
|
||
print("\n" + "=" * 100)
|
||
print(" (D) LA DOMANDA INVERSA — che rendita compra quello che posso permettermi?")
|
||
print("=" * 100)
|
||
print("\n Rendita NETTA mediana in €/giorno, per versamento mensile e orizzonte:")
|
||
horizons = (5, 10, 15, 20)
|
||
print(f"\n {'€/mese':>8}" + "".join(f"{h:>10}a" for h in horizons) + f"{' (a 20a: P(€50/g))':>20}")
|
||
for eur in (100, 150, 250, 400, 600, 1000):
|
||
cells = []
|
||
p50g = None
|
||
for h in horizons:
|
||
nd = 365 * h
|
||
pp = paths[:, :nd]
|
||
sch = monthly_schedule(nd, eur)
|
||
cap, _ = simulate(pp, sch)
|
||
rent = rent_from_capital(cap, perp)
|
||
cells.append(f"{np.median(rent):>9.2f}€")
|
||
if h == 20:
|
||
p50g = float((rent >= CC.TARGET_EUR_DAY).mean())
|
||
print(f" {eur:>8}" + "".join(cells) + f"{p50g:>19.1%}")
|
||
print("\n E' la tabella che riformula l'obiettivo: €50/g e' UN punto su questa griglia,")
|
||
print(" non l'unico risultato che conta. Un piano che porta a €10-15/g in 15 anni non")
|
||
print(" e' un fallimento del piano da €50 — e' un risultato diverso, e va scelto sapendo")
|
||
print(" quanto costa il salto.")
|
||
|
||
# ------------------------------------------------------------ (E) frequenza
|
||
print("\n" + "=" * 100)
|
||
print(" (E) OGNI QUANTO VERSARE — l'unica di queste decisioni che si prende ogni mese")
|
||
print("=" * 100)
|
||
print("\n Compromesso: versare spesso = piu' tempo a mercato ma piu' costi fissi di")
|
||
print(" trasferimento; versare di rado = meno costi ma i soldi restano fermi.")
|
||
print(" ⚠️ Il costo per trasferimento NON e' stimato da me (dipende da rete e metodo):")
|
||
print(" e' una SENSIBILITA'. Cerchia la riga che corrisponde al tuo costo reale.")
|
||
EUR_YEAR = 250.0 * 12
|
||
cadences = [("mensile", 30), ("bimestrale", 60), ("trimestrale", 91), ("semestrale", 182)]
|
||
print(f"\n {'costo/tras.':>12}" + "".join(f"{lab:>16}" for lab, _ in cadences))
|
||
for fee in (0.0, 1.0, 2.0, 5.0, 10.0, 25.0):
|
||
cells = []
|
||
for lab, days in cadences:
|
||
per_dep = EUR_YEAR * CC.EURUSD * days / 365.0
|
||
s = np.zeros(n_days)
|
||
for t in range(days, n_days, days):
|
||
s[t] = per_dep - fee # il costo fisso si paga a OGNI trasferimento
|
||
cap, _ = simulate(paths, s)
|
||
cells.append(float(np.median(cap)))
|
||
best = max(range(len(cells)), key=lambda i: cells[i])
|
||
row = "".join(f"{('*' if i == best else ' ') + '$' + format(c, ',.0f'):>16}"
|
||
for i, c in enumerate(cells))
|
||
print(f" {'$' + format(fee, '.2f'):>12}" + row)
|
||
print("\n (* = migliore della riga). Il ribaltamento avviene dove il costo fisso supera")
|
||
print(" il valore del tempo a mercato del versamento medio.")
|
||
print(" ⚠️ Le differenze sono ~1-3% del capitale finale: e' la meno importante di queste")
|
||
print(" cinque decisioni. Non vale la pena ottimizzarla oltre 'mensile, salvo costi alti'.")
|
||
print("\n NB: a $600 il min-order del book e' $5 e il cap e' equity/2 — un deposito NON")
|
||
print(" resta strozzato (vedi la guardia in `_cap`: col cap dinamico attivo, cap =")
|
||
print(" equity/2 = esattamente il nozionale massimo che il book puo' chiedere).")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|