diff --git a/CLAUDE.md b/CLAUDE.md index b9bd6e3..583ffff 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1024,6 +1024,57 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis (d) quando le leve hanno ordini di grandezza diversi **dirlo**, o si lavora molto senza cambiare niente; (e) rivalutare **non** vuol dire anticipare i gate pre-registrati — anticiparli e' selezione sull'hold-out. +- 💰 **I VERSAMENTI — le 4 ipotesi che il piano non aveva mai fatto (2026-07-26, ultimo filone).** + Tutte le traiettorie del 25-26/07 assumevano versamento **piatto, ininterrotto, per sempre** = + l'ipotesi meno realistica del piano. Script `r0726_deposits.py`, test `tests/test_deposits.py` + (12), diario `2026-07-26-versamenti.md`. **Book/pesi/cron/config INVARIATI** (non tocca la + produzione). Block bootstrap sui ritorni reali del book live, fattore ×0.89 misurato. + (1) **SMETTERE — il costo non e' proporzionale ai soldi mancanti.** €250/m per K anni poi stop, + orizzonte 20a: 3a ($10.410) → $202.771 / P(muro) 32.7%; 5a ($16.950) → $287.081 / **53.3%**; + 10a ($33.572) → $410.220 / 77.9%; 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**, non ambizioso. + (2) **CRESCENTE E' PEGGIO DI PIATTO a pari soldi.** €150/m +5%/anno versa €67.998 → $401.889; + piatto €250 versa €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). Contro-intuitivo: "i versamenti crescono col reddito" e' + prudente per il bilancio, **non** per il capitale. + (3) **STESSO TOTALE, CALENDARIO DIVERSO = fattore 6.** €60.000 distribuiti: ultimi 10 anni + $178.494 (P(muro) 11%) / piatto 20a $496.778 (90%) / primi 10a $806.285 (98.3%) / primi 5a + **$1.104.587 (99.4%)**. ⚠️ NON significa "versa tutto subito": un piano che non si sostiene non + e' un piano — serve a scegliere fra calendari **sostenibili**. + ✅ **Verificato col rischio di venue dentro** (il vantaggio 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** (64% di rovina su 20a) → **formulazione piu' netta del rischio di venue trovata + finora: non erode il piano, lo CANCELLA.** + (4) **LA DOMANDA INVERSA — rendita netta €/g mediana** (riformula l'obiettivo: €50/g e' UN punto, + non l'unico risultato): + | €/mese | 5a | 10a | 15a | 20a | P(€50/g a 20a) | + |---|---|---|---|---|---| + | 100 | 2.08 | 6.54 | 16.40 | 38.07 | 31.0% | + | 150 | 3.00 | 9.55 | 23.96 | 55.80 | 58.7% | + | **250** | 4.83 | 15.53 | 39.11 | **91.30** | **90.0%** | + | 400 | 7.59 | 24.52 | 61.85 | 144.23 | 98.9% | + | 600 | 11.26 | 36.52 | 92.17 | 215.07 | 100.0% | + Non-linearita': **da 15 a 20 anni la rendita piu' che raddoppia a ogni livello** (gli ultimi anni + contano piu' in *rendita*, i primi piu' in *versamenti*). + (5) **FREQUENZA = la decisione meno importante.** Mensile fino a ~$2 di costo per trasferimento, + bimestrale sopra, trimestrale oltre $25 — ma le differenze sono **1-3%** del capitale finale. + Verificato che un deposito **non resta strozzato**: col cap dinamico attivo cap = equity/2 = + esattamente il nozionale massimo richiedibile per asset. + 📌 **ORDINE DI IMPORTANZA (da citare quando si parla del piano):** 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%). E sopra tutte, fuori scala: **a p=5% di rischio venue il + risultato mediano e' zero comunque.** + **REGOLE:** (a) un piano di accumulo si giudica sulle sue **deviazioni**, non sul caso nominale — + piatto/ininterrotto/per sempre e' l'unico scenario che non succede; (b) piani di taglia diversa si + confrontano con una metrica **normalizzata** (`$ finale / $ versato`), altrimenti "versa di piu'" + vince sempre; (c) un vantaggio calcolato **ignorando un rischio noto** va ri-misurato con quel + rischio dentro anche quando ci si aspetta che regga (qui reggeva, ma la colonna p=5% ha prodotto + il risultato piu' importante del filone); (d) quando l'obiettivo dichiarato non e' raggiungibile, + la tabella utile e' quella **inversa** — non "quando arrivo a X" ma "cosa compro con quello che + ho". - 📅 **NUOVO SCHEMA FEE DERIBIT dal 2026-08-01 — misurata la CURVA, nessuna azione oggi (2026-07-26).** Script `r0726_fee_sensitivity.py`, test `tests/test_fee_sensitivity.py` (6), diario `2026-07-26-fee-deribit.md`. **Book/pesi/cron/config INVARIATI.** diff --git a/docs/diary/2026-07-26-versamenti.md b/docs/diary/2026-07-26-versamenti.md new file mode 100644 index 0000000..0b1806f --- /dev/null +++ b/docs/diary/2026-07-26-versamenti.md @@ -0,0 +1,154 @@ +# 2026-07-26 — I versamenti: le quattro ipotesi che il piano non aveva mai fatto + +**Richiesta:** *"allora concentriamoci sui versamenti"* — dopo la rivalutazione che ha misurato il +book dentro il suo plateau (ottimizzare il peso = +0.030 Sharpe, gate fallito) mentre i versamenti +valgono la differenza fra **mai** e **16 anni**. + +**Script:** `r0726_deposits.py`. **Test:** `tests/test_deposits.py` (12). +**Book, pesi, cron, config: INVARIATI.** (Questo filone non tocca il codice di produzione.) + +--- + +## 0. Cosa mancava + +Tutte le traiettorie calcolate finora (25/07 e 26/07) assumono la stessa cosa: **versamento +piatto, ininterrotto, per sempre**. E' l'ipotesi meno realistica dell'intero piano. Qui si misurano +le deviazioni che succedono davvero, con la stessa macchineria (block bootstrap sui ritorni reali +del book live, fattore d'ancora ×0.89 **misurato**). + +## 1. Smettere di versare — il costo non e' proporzionale ai soldi mancanti + +€250/mese per K anni, poi stop, orizzonte 20 anni: + +``` + versi per totale versato cap. mediano a 20a P(muro entro 20a) rendita mediana + 3a $10,410 $202,771 32.7% 37.27 €/g + 5a $16,950 $287,081 53.3% 52.76 €/g + 10a $33,572 $410,220 77.9% 75.39 €/g + 20a $66,818 $496,778 90.0% 91.30 €/g +``` + +**I primi 5 anni sono il 25% dei soldi e il 58% del risultato.** Chi versa 5 anni e poi smette +arriva comunque alla rendita bersaglio nel 53% dei casi; chi versa gli ultimi 10 anni (piu' soldi) +arriva nell'11% (§3). **La variabile non e' quanto versi: e' quanto presto.** + +Corollario pratico: un'interruzione al 12° anno costa poco; una al 3° costa quasi tutto. Se il +piano deve rompersi, e' meglio che si rompa tardi — il che e' un argomento per **partire con un +importo sostenibile** invece che con uno ambizioso. + +## 2. Un piano crescente e' peggiore di uno piatto, a pari soldi + +``` +piano totale versato cap. mediano P(muro) $ finale / $ versato +piatto €250 $66,818 $496,778 90.0% 7.43x +€150 +5%/anno $67,998 $401,889 80.8% 5.91x +``` + +**Stessi soldi (€67-68k), 24% di risultato in meno.** Il piano crescente versa gli stessi euro +piu' **tardi**, e quegli euro compongono meno. L'ultima colonna e' la metrica giusta per +confrontare piani di taglia diversa: quanto capitale finale compra ogni dollaro versato — e i +piani crescenti stanno tutti a 5.1-5.9x contro 7.4x dei piatti. + +E' contro-intuitivo perche' "i versamenti crescono col reddito" suona prudente. Lo e' dal punto di +vista del bilancio familiare; **non** lo e' dal punto di vista del capitale. + +## 3. Stesso totale, calendario diverso — il tempo vale 2.2x + +€60.000 in totale (= €250/mese per 20 anni), distribuiti diversamente: + +``` +calendario cap. mediano P(muro) vs piatto +piatto su 20 anni $496,778 90.0% 1.00x +tutto nei primi 10 anni $806,285 98.3% 1.62x +tutto nei primi 5 anni $1,104,587 99.4% 2.22x +solo negli ultimi 10 anni $178,494 11.0% 0.36x +``` + +**Gli stessi €60.000 valgono da $178k a $1.104k — un fattore 6 — a seconda di QUANDO entrano.** + +⚠️ Questo **non** dice "versa tutto subito": dice quanto vale il tempo. Un piano che non si +sostiene non e' un piano, e il confronto serve a scegliere fra calendari **sostenibili**. + +### E regge al rischio di venue? + +Il vantaggio del front-loading e' calcolato mettendo **piu' capitale sull'exchange prima** — cioe' +proprio il rischio misurato stamattina. Andava verificato, non assunto: + +``` +calendario p=0.5% p=2.0% p=5.0% +piatto su 20 anni $463,413 $359,251 $0 +tutto nei primi 10 anni $765,178 $555,904 $0 +tutto nei primi 5 anni $1,015,491 $752,367 $0 +solo negli ultimi 10 anni $172,228 $147,203 $0 +``` + +Il vantaggio **regge** (2.22x → 2.09x a p=2%): il rischio di venue colpisce il **tempo**, non il +calendario dei versamenti. Anticipare non aumenta la probabilita' di essere colpiti, aumenta solo +quanto c'e' dentro quando succede — e nel frattempo ha composto di piu'. + +⚠️ **Ma guardare la colonna p=5%: il capitale mediano e' $0 per OGNI calendario.** A quel tasso di +rischio la meta' dei percorsi finisce a zero (64% di rovina su 20 anni, come misurato stamattina) e +**come versi diventa irrilevante**. E' la formulazione piu' netta del rischio di venue trovata +finora: non erode il piano, lo **cancella**. + +## 4. La domanda inversa — che rendita compra quello che posso permettermi + +Rendita **netta** mediana in €/giorno: + +``` + €/mese 5a 10a 15a 20a P(€50/g a 20a) + 100 2.08€ 6.54€ 16.40€ 38.07€ 31.0% + 150 3.00€ 9.55€ 23.96€ 55.80€ 58.7% + 250 4.83€ 15.53€ 39.11€ 91.30€ 90.0% + 400 7.59€ 24.52€ 61.85€ 144.23€ 98.9% + 600 11.26€ 36.52€ 92.17€ 215.07€ 100.0% +``` + +E' la tabella che **riformula l'obiettivo**. €50/g e' un punto su questa griglia, non l'unico +risultato che conta: €150/mese porta a ~€24/g in 15 anni, che non e' un fallimento del piano da +€50 — e' un risultato diverso, e ora si sa quanto costa il salto. + +Da notare la **non-linearita' temporale**: da 15 a 20 anni la rendita piu' che raddoppia a ogni +livello. Gli ultimi anni del piano contano piu' dei primi *in rendita*, esattamente come i primi +contano piu' degli ultimi *in versamenti*. + +## 5. Ogni quanto versare — la decisione meno importante + +``` + costo/tras. mensile bimestrale trimestrale semestrale + $0.00 *$490,149 $486,725 $482,587 $473,908 + $2.00 *$486,597 $484,964 $481,446 $473,342 + $5.00 $481,270 *$482,323 $479,727 $472,495 + $25.00 $445,984 $464,692 *$468,226 $466,817 +``` + +**Mensile fino a ~$2 di costo per trasferimento, bimestrale sopra.** Ma le differenze sono +**1-3%** del capitale finale: e' la meno importante delle cinque decisioni di questo diario, e non +merita ottimizzazione oltre la regola in una riga. + +Verificato che un deposito **non resta strozzato**: col cap dinamico attivo (`_cap`), cap = +equity/2 = esattamente il nozionale massimo che il book puo' chiedere per asset. + +## 6. Le cinque decisioni, in ordine di quanto pesano + +| decisione | effetto misurato | +|---|---| +| **versare o no** | da **mai** a 16 anni | +| **quando** (front vs back, stesso totale) | **6x** sul capitale finale | +| **quanto presto smettere** | 5 anni = 25% dei soldi, 58% del risultato | +| piatto vs crescente | 24% a pari soldi | +| frequenza | 1-3% | + +E sopra tutte, fuori scala: **a p=5% di rischio venue il risultato mediano e' zero comunque.** + +## 7. Regole trasferibili + +1. **Un piano di accumulo si giudica sulle sue deviazioni, non sul caso nominale.** Piatto, + ininterrotto e per sempre e' l'unico scenario che non succede. +2. **Confrontare piani di taglia diversa richiede una metrica normalizzata** (`$ finale / $ + versato`), altrimenti "versa di piu'" vince sempre e non si impara niente. +3. **Un vantaggio calcolato ignorando un rischio noto va ri-misurato con quel rischio dentro**, + anche quando ci si aspetta che regga — qui reggeva, ma la colonna p=5% ha prodotto il risultato + piu' importante del filone. +4. **Quando l'obiettivo dichiarato non e' raggiungibile, la tabella utile e' quella inversa**: + non "quando arrivo a X" ma "cosa compro con quello che ho". diff --git a/scripts/research/r0726_deposits.py b/scripts/research/r0726_deposits.py new file mode 100644 index 0000000..291ab13 --- /dev/null +++ b/scripts/research/r0726_deposits.py @@ -0,0 +1,255 @@ +#!/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() diff --git a/tests/test_deposits.py b/tests/test_deposits.py new file mode 100644 index 0000000..1cac0da --- /dev/null +++ b/tests/test_deposits.py @@ -0,0 +1,133 @@ +"""Test del piano versamenti (scripts/research/r0726_deposits.py). + +Il test piu' importante e' `test_la_rendita_al_muro_e_esattamente_il_bersaglio`: lega la tabella +della "domanda inversa" al muro calcolato altrove. Se le due macchinerie divergono, una delle due +tabelle sta mentendo e nessun altro controllo se ne accorgerebbe. +""" +from __future__ import annotations + +import sys +from pathlib import Path + +import numpy as np +import pytest + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) + +import r0725_capcurve as CC # noqa: E402 +import r0726_deposits as DP # noqa: E402 + + +# =========================================================================== +# calendario dei versamenti +# =========================================================================== +def test_versamento_mensile_conta_i_periodi_giusti(): + s = DP.monthly_schedule(365, 100.0) + assert (s > 0).sum() == 12, "12 versamenti in un anno di 365 giorni a passo 30" + assert s[0] == 0.0, "il primo versamento non e' al giorno 0 (li' c'e' il capitale iniziale)" + + +def test_stop_year_interrompe_davvero(): + s = DP.monthly_schedule(365 * 10, 100.0, stop_year=3) + ultimo = np.nonzero(s)[0].max() + assert ultimo < 3 * 365 + assert (s > 0).sum() == pytest.approx(36, abs=1) + + +def test_la_crescita_aumenta_gli_importi_nel_tempo(): + s = DP.monthly_schedule(365 * 10, 100.0, growth=0.05) + nz = s[s > 0] + assert nz[-1] > nz[0] * 1.5, "dopo ~10 anni al 5% l'importo deve essere >1.5x" + piatto = DP.monthly_schedule(365 * 10, 100.0) + assert s.sum() > piatto.sum() + + +def test_crescita_zero_equivale_al_piatto(): + a = DP.monthly_schedule(365 * 5, 200.0, growth=0.0) + b = DP.monthly_schedule(365 * 5, 200.0) + assert np.allclose(a, b) + + +# =========================================================================== +# simulazione +# =========================================================================== +def test_senza_versamenti_e_solo_capitalizzazione(): + paths = np.full((3, 100), 0.001) + cap, paid = DP.simulate(paths, np.zeros(100)) + atteso = DP.START * (1.001 ** 100) + assert cap[0] == pytest.approx(atteso, rel=1e-9) + assert paid[0] == pytest.approx(DP.START) + + +def test_a_rendimento_zero_il_capitale_e_la_somma_dei_versamenti(): + paths = np.zeros((2, 365)) + s = DP.monthly_schedule(365, 100.0) + cap, paid = DP.simulate(paths, s) + assert cap[0] == pytest.approx(DP.START + s.sum()) + assert paid[0] == pytest.approx(DP.START + s.sum()) + + +def test_il_totale_versato_include_il_capitale_iniziale(): + """Confrontare un capitale finale con un 'versato' che dimentica i $600 iniziali gonfia il + rapporto capitale/versato — errore gia' fatto una volta oggi nel contatore `paid`.""" + s = DP.monthly_schedule(365, 50.0) + _, paid = DP.simulate(np.zeros((2, 365)), s) + assert paid[0] == pytest.approx(DP.START + s.sum()) + assert paid[0] > s.sum() + + +# =========================================================================== +# il legame con il muro — il test che lega le due macchinerie +# =========================================================================== +def test_la_rendita_al_muro_e_esattamente_il_bersaglio(): + """Un capitale pari al muro deve dare, per costruzione, esattamente TARGET_EUR_DAY. + Se questa identita' si rompe, la tabella (D) e la tabella dei muri non parlano piu' della + stessa cosa.""" + import r0726_capwall_refresh as WR + + r = DP.deluck_returns() + gross = CC.TARGET_EUR_DAY * 365 * CC.EURUSD / (1 - CC.TAX_RATE) + _, perp, wall = WR.perp_and_wall(r, 1.0, gross) + rent = DP.rent_from_capital(np.array([wall]), perp)[0] + assert rent == pytest.approx(CC.TARGET_EUR_DAY, rel=1e-6) + + +def test_la_rendita_e_lineare_nel_capitale(): + r = DP.rent_from_capital(np.array([100_000.0, 200_000.0]), 0.10) + assert r[1] == pytest.approx(2 * r[0]) + + +# =========================================================================== +# i FINDING: se cambiano, e' una notizia +# =========================================================================== +def test_anticipare_batte_posticipare_a_pari_totale(): + """Il finding di (C). Su ritorni a drift positivo deterministico e' un'identita' algebrica, + e serve come controllo di direzione: se uscisse il contrario, la simulazione e' rotta.""" + n = 365 * 20 + paths = np.full((1, n), 0.0004) + tot = 60_000.0 * CC.EURUSD + presto, tardi = np.zeros(n), np.zeros(n) + k = len(range(30, 365 * 5, 30)) + for t in range(30, 365 * 5, 30): + presto[t] = tot / k + k2 = len(range(365 * 10, n, 30)) + for t in range(365 * 10, n, 30): + tardi[t] = tot / k2 + assert presto.sum() == pytest.approx(tardi.sum(), rel=1e-9) + cap_p, _ = DP.simulate(paths, presto) + cap_t, _ = DP.simulate(paths, tardi) + assert cap_p[0] > cap_t[0] * 1.5 + + +def test_il_fattore_deluck_e_quello_misurato_non_quello_a_occhio(): + """Il ×0.6 del 25/07 era scelto a occhio ed e' stato misurato ×0.87-0.91 il 26/07.""" + assert DP.DELUCK == 0.89 + + +def test_hazard_giornaliero_ricompone_alla_probabilita_annua(): + """Il salto di venue: (1-haz)^365 deve dare (1-p). Sbagliare qui cambia tutte le colonne.""" + for p in (0.005, 0.01, 0.02, 0.05): + haz = 1.0 - (1.0 - p) ** (1.0 / 365.0) + assert (1.0 - haz) ** 365 == pytest.approx(1.0 - p, rel=1e-9)