1227b2baab
L'operatore ha dato i numeri veri: EUR 5.000 subito + EUR 500/mese. Cambia la scala (conto da $597 a $6.047 = 10.1x), quindi ricalcolato in dedicata invece che estrapolato. QUANDO: traguardo EUR 50/g ($272.061) a p10 9.4a / MEDIANA 11.6a / p90 14.4a; P(entro 15a) 94%, P(entro 20a) 100%. Rendita mediana: EUR 6.37/g a 3 anni, EUR 11.57/g a 5, EUR 35.18/g a 10, EUR 87.63/g a 15. VALIDAZIONE INCROCIATA: la variante "solo EUR 500/mese da $600" da' 12.4 anni = esattamente il numero pubblicato il 25/07 con macchineria diversa. IL LUMP VALE 0.8 ANNI (12.4 -> 11.6), MENO di quanto suggerisse il "fattore 6" del calendario, e la ragione e' aritmetica: EUR 5.000 sono ~10 mesi di versamenti a EUR 500, quindi comprano ~10 mesi. Anticipare vale in proporzione a quanto si anticipa. La mia aspettativa era piu' alta ed e' corretta. SOGLIE: $3k e $5k superate il giorno 1 (GTAA01 e XSR01 diventano eseguibili); $13k a ~0.9 anni (GTAA01 entra nel book deployable); $20k a ~1.6 anni = LA DECISIONE VENUE DEL 26/07 SMETTE DI ESSERE UN'IPOTESI E DIVENTA UNA DATA (~19 mesi); $117k a ~7.6 anni (XS01). Le soglie superate subito NON autorizzano ad anticipare il gate XSR01 del 23/10. CON IL RISCHIO DI VENUE: P(traguardo) 100% -> 88% a p=1% (P(perso tutto) 23.2%); a p=5% il capitale mediano e' ZERO. Il piano regge fino a p=2%. ⚠️ AZIONE OPERATIVA TROVATA (non eseguita, e' config su soldi veri): `book._cap` ripiega su max_notional_per_asset_usd=$300 quando l'equity reale non e' leggibile. A $597 il fallback era INERTE (equity/2 = $298 ~ $300); dopo il versamento equity/2 vale ~$3.023, quindi un fallback strozzerebbe il book al ~10% del target, in silenzio. E' l'azione gia' pre-registrata il 2026-07-02 ("al deposito alzare il cap a equity/2"). Cablato test_il_cap_fisso_diventa_una_strozzatura_dopo_un_deposito, che FALLISCE se si deposita senza adeguare il cap. Aggiunta anche la sezione (F) frontiera di sostenibilita' a r0726_deposits.py: capitale e rendita per (importo x anni sostenuti), e il confronto "tirare poco tempo vs comodo a lungo" — EUR 400/m per 5a batte EUR 200/m per 20a, ma EUR 600/m per 3a perde contro EUR 250/m per 15a. Book, pesi, cron, config INVARIATI. 380 test verdi (+3). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
299 lines
16 KiB
Python
299 lines
16 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).")
|
||
|
||
# ------------------------------------------------------------ (F) frontiera di sostenibilita'
|
||
# "Quanto posso versare?" non ha una risposta da modello: dipende dal bilancio di chi versa.
|
||
# Ma la domanda DIETRO ("conviene tirare?") si': si misura come iso-risultato fra un importo
|
||
# alto tenuto poco e uno basso tenuto a lungo.
|
||
print("\n" + "=" * 100)
|
||
print(" (F) FRONTIERA DI SOSTENIBILITA' — conviene tirare, o restare comodi piu' a lungo?")
|
||
print("=" * 100)
|
||
print("\n Capitale mediano a 20 anni per (importo mensile × anni per cui riesci a tenerlo):")
|
||
amounts = (100, 150, 200, 250, 400, 600)
|
||
spans = (3, 5, 10, 15, 20)
|
||
print(f"\n {'€/mese':>8}" + "".join(f"{s:>12}a" for s in spans))
|
||
grid = {}
|
||
for eur in amounts:
|
||
cells = []
|
||
for sp in spans:
|
||
sch = monthly_schedule(n_days, float(eur), stop_year=sp)
|
||
cap, _ = simulate(paths, sch)
|
||
grid[(eur, sp)] = float(np.median(cap))
|
||
cells.append(f"${grid[(eur, sp)]:>11,.0f}")
|
||
print(f" {eur:>8}" + "".join(cells))
|
||
|
||
print("\n Rendita netta mediana corrispondente (€/giorno a 20 anni):")
|
||
print(f"\n {'€/mese':>8}" + "".join(f"{s:>12}a" for s in spans))
|
||
for eur in amounts:
|
||
cells = [f"{rent_from_capital(np.array([grid[(eur, sp)]]), perp)[0]:>11.2f}€"
|
||
for sp in spans]
|
||
print(f" {eur:>8}" + "".join(cells))
|
||
|
||
# il confronto che risponde alla domanda: tirare poco tempo vs comodo a lungo
|
||
print("\n IL CONFRONTO CHE DECIDE — stessa fatica percepita, esiti opposti:")
|
||
for hi, hi_yrs, lo, lo_yrs in ((400, 5, 200, 20), (600, 3, 250, 15), (400, 10, 250, 20)):
|
||
a, b = grid[(hi, hi_yrs)], grid[(lo, lo_yrs)]
|
||
tot_a = monthly_schedule(n_days, float(hi), stop_year=hi_yrs).sum()
|
||
tot_b = monthly_schedule(n_days, float(lo), stop_year=lo_yrs).sum()
|
||
win = "il PRIMO" if a > b else "il SECONDO"
|
||
print(f" €{hi}/m per {hi_yrs:>2}a (${tot_a:>7,.0f} versati) -> ${a:>9,.0f} vs "
|
||
f"€{lo}/m per {lo_yrs:>2}a (${tot_b:>7,.0f}) -> ${b:>9,.0f} vince {win}")
|
||
|
||
print("\n REGOLA CHE NE ESCE: l'importo massimo NON e' l'obiettivo. L'obiettivo e' il")
|
||
print(" massimo importo che sopravvive a un anno brutto — perche' l'unico modo di")
|
||
print(" perdere davvero e' smettere presto (§A: i primi 5 anni sono il 58% del")
|
||
print(" risultato), e si smette presto quando si e' versato piu' del sostenibile.")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|