research(deposit-timing): il timing dei bonifici non paga — 0/12 celle, e il segnale compra davvero il 51% piu' in basso

§44. Prima misura di un calendario di versamento CONDIZIONALE (le 5 leve del piano
misurate finora confrontano solo calendari deterministici).

12 celle dichiarate prima: buy-the-dip x4 soglie, risk-off (TP01 a 0x), valore-medio
x3, anti-dip x4. Stesso flusso di cassa per tutte (EUR X/mese sul conto corrente);
cio' che cambia e' quando entrano nel libro, con la liquidita' in attesa a 0% reale.
Lente L3 CONGIUNTA (funding + fisco), muro $313k, block bootstrap 3000 percorsi.

Replica 4/4 prima di ogni delta: muro $313.143, riga EUR250/m 23,4a / P(20a) 14,4%,
riga EUR500/m 17,3a / 85,4%, e P0 del motore col buffer di cassa BIT-EXACT contro
PN.accumula.

SCARTATO. 0/12 in ogni lente (mercato vero, IID, drift dimezzato, EUR250, EUR500);
migliore -0,03% appaiato contro una risoluzione MC della differenza appaiata di 2,0pp,
e su una cella che aspetta 1 giorno = P0 travestito.

Il meccanismo, misurato invece che assunto: buy-the-dip 20% compra davvero al 0,494
del livello medio del piatto (51% piu' in basso) e arriva al capitale-rendita nel 6,3%
dei casi contro l'85,4%. Comprare meglio e comprare tardi sono la stessa mossa.
Sotto la soglia bassa si ribalta: dip 5% compra a 1,009, cioe' PIU' IN ALTO — una
condizione poco profonda non compra il calo, ritarda dentro la salita.

Due errori catturati su me stesso da un controllo: (i) il ritardo-gemello dava
"informazione" +11,5% sul mercato vero e mi avrebbe fatto scrivere che il dip predice
— sotto IID, dove non c'e' niente da prevedere, la stessa colonna vale +14,9%, quindi
e' meccanica (attesa variabile contro fissa); (ii) senza la colonna dell'attesa avrei
letto P4 anti-dip come "stesso segno => rumore": e' piatto perche' in un mercato che
sale la sua condizione e' quasi sempre gia' vera (16 giorni contro 3682).

Rischio di venue separato dal timing: la cassa fuori dall'exchange riduce la penalita'
di +2,4pp a p=1% e +3,8pp a p=2% su dip 10%, contro una penalita' di timing di -15,7pp,
e P(libro azzerato) e' identica per tutte le politiche.

Ordine di grandezza: la migliore politica di timing vale -0,1pp di P(20a); passare da
EUR250 a EUR500 al mese vale +71pp.

Solo script di ricerca. Book/pesi/cron/config INVARIATI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M8Ncho6QV9FWLdyy4VyQf
This commit is contained in:
Adriano Dal Pastro
2026-08-23 01:05:11 +00:00
parent 6f74dcb843
commit fc55698096
+695
View File
@@ -0,0 +1,695 @@
#!/usr/bin/env python
"""r0823_deposit_timing.py — §44 DEPOSIT-TIMING: il timing dei BONIFICI paga?
LA DOMANDA. Nel progetto e' misurato che la leva dominante non e' la strategia ma il
versamento: *versare o no* (da mai a ~16 anni) > *quando* (fattore 6 a pari soldi totali) >
*quanto presto si smette* (i primi 5 anni sono il 25% dei soldi e il 58% del risultato) >
*piatto vs crescente* (24%) > *frequenza* (1-3%). Ma TUTTE quelle misure confrontano
**calendari deterministici**. Nessuno ha mai misurato un calendario **CONDIZIONALE**: versare
di piu' dopo un drawdown, o quando il libro e' risk-off, o su una qualunque regola che guarda
lo stato del conto.
A pari soldi totali versati e pari calendario di DISPONIBILITA', una regola di deposito
condizionale batte il versamento piatto?
⚠️ E' MARKET TIMING SUI PROPRI BONIFICI, e va giudicato come tale. Il null e' severo (sotto).
ATTESA A PRIORI, DICHIARATA PRIMA DI MISURARE (coordinatore, e la sottoscrivo): **NON
funziona**. Comprare piu' in basso alza il rendimento sul capitale versato, ma la mediana e'
dominata dal fatto che i soldi entrano PIU' TARDI, e il progetto ha gia' misurato che entrare
prima vale un fattore 6 (r0726_deposits punto 3). L'esito e' riportato comunque, anche se
refuta l'attesa.
IL VINCOLO DI REALTA' CHE RENDE LA DOMANDA NON BANALE. I soldi non compaiono. Ogni politica
riceve lo STESSO flusso di cassa (EUR X/mese sul conto corrente); cio' che cambia e' **quando
entrano nel libro**. La liquidita' in attesa rende **0% reale netto** (assunzione del progetto:
XEON) — ed e' meta' del costo di ogni politica che aspetta. Senza questo vincolo la domanda
sarebbe truccata: "versare di piu' nei cali" senza dire da dove vengono i soldi e' un confronto
fra due piani di versamento diversi, non fra due timing.
LE POLITICHE, DICHIARATE TUTTE PRIMA (e contate come trial: 12 celle candidate)
P0 PIATTO — versa tutto il buffer il giorno stesso. E' l'incumbent.
P1 BUY-THE-DIP(th) — accantona; versa TUTTO il buffer quando l'indice di equity del libro
sta >= th sotto il suo massimo. th in {5,10,15,20}% (4 celle)
P2 RISK-OFF — versa quando TP01 e' a 0x (segnale gia' pubblico e causale, ~33% dei
giorni). (1 cella)
P3 VALORE-MEDIO(g) — versa quanto serve a riportare il capitale su una traiettoria
bersaglio che cresce a g. Mai vendere. g in {0,8,15}%/anno (3 celle)
P4 ANTI-DIP(th) — lo SPECULARE OBBLIGATORIO: versa dopo un rialzo di th dal minimo
corrente. th in {5,10,15,20}% (4 celle)
CONTROLLO DEL RITARDO — per ogni politica condizionale, la stessa attesa MEDIA ma
DETERMINISTICA (versa k giorni dopo, k = attesa misurata di quella
politica, nessun segnale). Separa "il segnale informa" da "aspettare
costa". Non e' una cella candidata: e' un null.
I NULL
(a) BOOTSTRAP IID (blocco=1) — distrugge la dipendenza seriale. Se una politica vince anche
li', sta sfruttando la meccanica del bootstrap, non il mercato: con ritorni indipendenti
il passato non informa il futuro e ogni residuo e' cassa/ritardo.
(b) P4 ANTI-DIP — se P1 e P4 danno lo stesso segno, si sta misurando rumore.
(c) DRIFT DIMEZZATO — lo stress standard del progetto sul libro (7,4 anni di storia, due tori).
LA LENTE E' QUELLA UFFICIALE: L3 CONGIUNTA di r0822d_piano_vero (funding esatto DENTRO la serie
+ fisco d'accumulo coerente + ancora x0.89 misurata). Bersaglio = il muro di quella lente.
CONVENZIONE DEL TRAGUARDO: si arriva quando **libro + cassa** >= muro. E' la convenzione
FAVOREVOLE alle politiche condizionali (la cassa che aspetta conta lo stesso), scelta apposta
perche' l'attesa a priori e' che perdano: se perdono anche cosi', il risultato e' piu' forte.
La convenzione severa (solo libro) e' riportata come sensibilita'.
nice -n 19 uv run python scripts/research/r0823_deposit_timing.py
"""
from __future__ import annotations
import sys
import time
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path("/opt/docker/PythagorasGoal")
sys.path.insert(0, str(ROOT / "scripts" / "research"))
sys.path.insert(0, str(ROOT))
import r0725_capcurve as CC # noqa: E402 TAX_RATE, EURUSD, _boot_paths
import r0727_tasse as TX # noqa: E402 PATRIMONIALE, CARRY_ANNI
import r0807_piano_netto as PN # noqa: E402 passo_fiscale, accumula, muro
import r0822d_piano_vero as PV # noqa: E402 costruisci_serie, deluck, leggi
from src.data.downloader import load_data # noqa: E402
from src.strategies.trend_portfolio import (CANONICAL, TrendPortfolio, # noqa: E402
resample_1d)
# --- costanti dichiarate ---------------------------------------------------
START = PV.START # $635, il conto Deribit vero
N_PATHS = 3_000 # la taglia della tabella pubblicata (seed 725)
BLOCK = PV.BLOCK # 20 giorni
ANNI = PV.ANNI_TRAJ # 25 (20 troncherebbe la coda)
SEED = PV.SEED_TRAJ # 725
SEMI_MC = (725, 2026, 20260823) # per la risoluzione Monte Carlo
DEP_MAIN = 500.0 # il piano dichiarato dall'operatore (EUR/mese)
DEP_ALT = 250.0 # il livello di cui si discute da un mese
CASH_RATE = 0.0 # XEON ~0% reale netto: assunzione del progetto
DD_GRID = (0.05, 0.10, 0.15, 0.20)
VA_GRID = (0.00, 0.08, 0.15)
P_VENUE = (0.0, 0.01, 0.02)
ASSETS = ("BTC", "ETH")
T0 = time.time()
def sez(t: str) -> None:
print("\n" + "=" * 104)
print(f" {t}")
print("=" * 104)
def clock() -> str:
return f"[{time.time() - T0:5.0f}s]"
# ===========================================================================
# 0) LE SERIE — ritorni (lente L3) ed ESPOSIZIONE di TP01, sullo stesso indice
# ===========================================================================
def esposizione_tp01(alloc: float = 635.0) -> pd.Series:
"""Esposizione GIA' TENUTA dal libro (0..2), causale, con la stessa regola di min-order di
`CC._tp01_realistic_cached`: un ribilanciamento sotto $5 non si esegue.
Non e' una re-implementazione della strategia: `target_series` e' importata dal modulo di
produzione. Qui serve solo il livello di posizione, non i ritorni (quelli restano quelli
ufficiali di `CC.book_series`, gia' dentro la serie della lente).
"""
tp = TrendPortfolio(**CANONICAL)
cap_asset = alloc / len(ASSETS)
ser = {}
for a in ASSETS:
df = resample_1d(load_data(a, "1h"))
tgt = np.nan_to_num(np.asarray(tp.target_series(df), float))
held = np.empty(len(tgt))
cur = 0.0
for i in range(len(tgt)):
if abs(tgt[i] - cur) * cap_asset >= CC.DERIBIT_MIN_ORDER:
cur = tgt[i]
held[i] = cur
pos = np.zeros(len(held))
pos[1:] = held[:-1] # causale: deciso a i-1, tenuto in i
ser[a] = pd.Series(pos, index=pd.to_datetime(df["datetime"]))
J = pd.concat(ser, 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
def boot_idx(n_src: int, n_paths: int, n_days: int, block: int,
rng: np.random.Generator) -> np.ndarray:
"""Gli INDICI del block bootstrap di `CC._boot_paths`, per poter trasportare insieme ai
ritorni una seconda serie allineata (l'esposizione di TP01, che serve a P2).
Controllo di identita' in sezione 1: `r[boot_idx(...)]` deve essere bit-exact
`CC._boot_paths(r, ...)` a parita' di generatore.
"""
n_blocks = int(np.ceil(n_days / block))
starts = rng.integers(0, n_src - block, size=(n_paths, n_blocks))
idx = (starts[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)
return idx[:, :n_days].astype(np.int32)
# ===========================================================================
# IL MOTORE — accumulo con buffer di CASSA e politica di versamento
# ===========================================================================
def accumula_pol(paths: np.ndarray, flat: np.ndarray | None, dep_eur: float,
bersaglio: float, aliquota: float, patrimoniale: float,
policy, start: float = START, cash_rate: float = CASH_RATE,
fail: np.ndarray | None = None,
traguardo_include_cassa: bool = True) -> dict:
"""Accumulo su path GIA' ESTRATTI, con una politica che decide QUANDO la cassa entra.
Il flusso di cassa e' identico per ogni politica: `dep_eur` ogni 30 giorni sul CONTO
CORRENTE. La politica decide solo quanto trasferire nel libro, ogni giorno.
Causalita': il ritorno del giorno t si applica PRIMA del trasferimento (come in
`PN.accumula`), quindi cio' che entra il giorno t rende dal giorno t+1; e la decisione usa
l'indice di equity fino alla chiusura di t, che a quel punto e' osservabile.
`policy(t, cap, cash, dd, ru, flat_t)` ritorna l'importo da trasferire (USD, per path).
`fail`: maschera (path, giorno) di fallimento del venue — il libro va a zero, la CASSA no.
"""
n_paths, n_days = paths.shape
dep = dep_eur * CC.EURUSD
cash_d = (1.0 + cash_rate) ** (1.0 / 365.0) - 1.0
cap = np.full(n_paths, start)
cash = np.zeros(n_paths)
eqi = np.ones(n_paths) # indice di equity del libro, NEUTRO ai versamenti
peak = np.ones(n_paths)
trough = np.ones(n_paths)
carry = np.zeros((n_paths, TX.CARRY_ANNI))
anno_start = cap.copy()
flusso = np.zeros(n_paths)
colpito = np.full(n_paths, -1, int)
versato = start
cassa_int = 0.0 # integrale della cassa (per la legge di Little)
persi = np.zeros(n_paths, bool)
pnum = np.zeros(n_paths) # sum(importo * livello di equity all'ingresso)
pden = np.zeros(n_paths) # sum(importo)
for t in range(n_days):
rt = paths[:, t]
cap *= (1.0 + rt)
eqi *= (1.0 + rt)
np.maximum(peak, eqi, out=peak)
np.minimum(trough, eqi, out=trough)
if fail is not None:
f = fail[:, t]
if f.any():
cap = np.where(f, 0.0, cap)
anno_start = np.where(f, 0.0, anno_start)
flusso = np.where(f, 0.0, flusso) # la perdita non diventa credito d'imposta
persi |= f
cash *= (1.0 + cash_d)
if t % 30 == 0 and t > 0 and dep > 0:
cash += dep
versato += dep
move = policy(t, cap, cash, 1.0 - eqi / peak, eqi / trough - 1.0,
None if flat is None else flat[:, t])
if move is not None:
move = np.minimum(np.maximum(move, 0.0), cash)
cap += move
cash -= move
flusso += move
pnum += move * eqi # a che LIVELLO e' entrato ogni dollaro
pden += move
cassa_int += float(cash.mean())
tot = cap + cash if traguardo_include_cassa else cap
nuovi = (colpito < 0) & (tot >= bersaglio)
colpito[nuovi] = t
if (t + 1) % 365 == 0 and (aliquota > 0 or patrimoniale > 0):
PN.passo_fiscale(cap, anno_start, flusso, carry, aliquota, patrimoniale)
anno_start = cap.copy()
flusso[:] = 0.0
lam = dep / 30.0 if dep > 0 else np.nan # USD/giorno di flusso in entrata
attesa = (cassa_int / n_days) / lam if dep > 0 else 0.0 # Little: W = L / lambda
prezzo = np.where(pden > 0, pnum / np.maximum(pden, 1e-12), np.nan)
return dict(cap=cap, cash=cash, colpito=colpito, versato=versato,
attesa_g=float(attesa), p_perso=float(persi.mean()),
prezzo=float(np.nanmedian(prezzo)),
non_versata=float(np.median(cash)))
# ===========================================================================
# LE POLITICHE
# ===========================================================================
def pol_piatto():
return lambda t, cap, cash, dd, ru, fl: cash
def pol_dip(th: float):
return lambda t, cap, cash, dd, ru, fl: np.where(dd >= th, cash, 0.0)
def pol_antidip(th: float):
return lambda t, cap, cash, dd, ru, fl: np.where(ru >= th, cash, 0.0)
def pol_riskoff():
return lambda t, cap, cash, dd, ru, fl: np.where(fl, cash, 0.0)
def pol_va(g: float, dep_eur: float, start: float = START):
"""Valore-medio: traiettoria bersaglio deterministica, versa la differenza, mai vende."""
dep = dep_eur * CC.EURUSD
gm = (1.0 + g) ** (30.0 / 365.0) - 1.0
state = {"T": start}
def f(t, cap, cash, dd, ru, fl):
if t % 30 or t == 0:
return None
state["T"] = state["T"] * (1.0 + gm) + dep
return np.maximum(state["T"] - cap, 0.0)
return f
def pol_lag(k: int, dep_eur: float):
"""Il controllo del ritardo: versa esattamente k giorni dopo l'arrivo, senza guardare nulla."""
dep = dep_eur * CC.EURUSD
def f(t, cap, cash, dd, ru, fl):
return np.full(len(cap), dep) if (t > k and (t - k) % 30 == 0) else None
return f
def celle(dep_eur: float) -> dict:
"""Le 12 celle candidate, dichiarate qui e in nessun altro posto."""
C = {"P0 piatto": pol_piatto()}
for th in DD_GRID:
C[f"P1 dip {th:.0%}"] = pol_dip(th)
C["P2 risk-off"] = pol_riskoff()
for g in VA_GRID:
C[f"P3 VA g={g:.0%}"] = pol_va(g, dep_eur)
for th in DD_GRID:
C[f"P4 anti {th:.0%}"] = pol_antidip(th)
return C
N_TRIAL = 12 # P1(4) + P2(1) + P3(3) + P4(4). P0 e' l'incumbent, i lag sono null.
# ===========================================================================
def leggi(colpito: np.ndarray) -> dict:
"""Mediana INCONDIZIONATA (non-arrivo = +inf, MAI -1) + condizionata + P per orizzonte."""
y = np.where(colpito >= 0, colpito / 365.0, np.inf)
a = y[np.isfinite(y)]
return dict(med=float(np.median(y)),
med_cond=float(np.median(a)) if len(a) > 100 else float("nan"),
p10=float((y <= 10).mean()), p15=float((y <= 15).mean()),
p20=float((y <= 20).mean()))
def fmt(d: dict) -> str:
m = f">{ANNI}a" if not np.isfinite(d["med"]) else f"{d['med']:.1f}a"
c = "" if np.isnan(d["med_cond"]) else f"{d['med_cond']:.1f}a"
return f"{m:>7}{c:>8}"
def riga(nome: str, res: dict, perp: float, base: dict | None = None,
prezzo0: float | None = None) -> str:
d = leggi(res["colpito"])
tot = res["cap"] + res["cash"]
rend = float(np.median(tot * perp / 365.0 / CC.EURUSD))
dp = "" if base is None else f"{d['p20'] - base['p20']:+7.1%}"
pz = "" if not prezzo0 else f"{res['prezzo'] / prezzo0:.3f}"
return (f" {nome:<16}{fmt(d)}{d['p15']:>8.1%}{d['p20']:>8.1%}"
f"{dp:>9}{rend:>10.2f}{res['attesa_g']:>8.0f}{pz:>8}")
HDR = (f" {'politica':<16}{'med':>7}{'m|arr':>8}{'P(15a)':>8}{'P(20a)':>8}"
f"{'dP20':>9}{'EUR/g 20a':>10}{'attesa':>8}{'prezzo':>8}")
def tabella(paths, flat, dep, muro, perp, titolo, **kw) -> dict:
print(f"\n {titolo}")
print(HDR)
out = {}
base = None
p0 = None
for nome, pol in celle(dep).items():
r = accumula_pol(paths, flat, dep, muro, CC.TAX_RATE, TX.PATRIMONIALE, pol, **kw)
d = leggi(r["colpito"])
if base is None:
base, p0 = d, r["prezzo"]
out[nome] = r
print(riga(nome, r, perp, None if nome.startswith("P0") else base,
None if nome.startswith("P0") else p0))
return out
# ===========================================================================
def main() -> None:
print("=" * 104)
print(" r0823 §44 DEPOSIT-TIMING — il timing dei BONIFICI paga? (lente L3: funding + fisco)")
print("=" * 104)
# ------------------------------------------------------------------ 0
B = PV.costruisci_serie()
idx = pd.DatetimeIndex(B.index)
r = PV.deluck(B["fund"].values.astype(float)) # lente L3 CONGIUNTA, ancora x0.89
E = esposizione_tp01().reindex(idx).ffill().fillna(0.0)
flat_src = (E.values <= 1e-9)
print(f"\n serie L3 CONGIUNTA (libro live 75/25, funding esatto, ancora x{PV.DELUCK}) — "
f"{len(r)} giorni {idx.min().date()}..{idx.max().date()}")
print(f" drift {r.mean() * 365:.2%} · vol {r.std() * 365 ** 0.5:.2%} · "
f"esposizione TP01 media {E.mean():.3f}x · giorni a 0x = {flat_src.mean():.1%} {clock()}")
# ------------------------------------------------------------------ 1
sez("1) CONTROLLO DI REPLICA — prima di ogni delta, riprodurre i numeri gia' pubblicati")
ok = {}
netto = CC.TARGET_EUR_DAY * 365 * CC.EURUSD
perp, muro = PN.muro(r, netto, CC.TAX_RATE, TX.PATRIMONIALE)
ok["R1"] = abs(muro - 313_000.0) < 1_500.0
print(f"\n R1 muro L3 congiunta: perpetua {perp:.2%} · ${muro:,.0f}"
f"{'':>10}pubblicato $313k {'OK' if ok['R1'] else 'DIVERGE'} {clock()}")
rng = np.random.default_rng(SEED)
ii = boot_idx(len(r), N_PATHS, ANNI * 365, BLOCK, rng)
paths = r[ii]
flat = flat_src[ii]
rng2 = np.random.default_rng(SEED)
ok["R2"] = bool(np.array_equal(paths, CC._boot_paths(r, N_PATHS, ANNI * 365, BLOCK, rng2)))
print(f" R2 `boot_idx` trasporta gli STESSI path di `CC._boot_paths` (per portarsi dietro")
print(f" l'esposizione di TP01, che serve a P2):{'':>19}"
f"{'BIT-EXACT' if ok['R2'] else 'DIVERGE'}")
print(f"\n R3 la riga pubblicata della tabella PIANO-VERO (L3, da ${START:.0f}):")
att = {250.0: (23.4, 22.1, 0.14), 500.0: (17.3, 17.3, 0.85)}
r3 = True
print(f" {'EUR/m':>7}{'med.inc':>10}{'atteso':>9}{'med|arr':>10}{'atteso':>9}"
f"{'P(20a)':>9}{'atteso':>9}")
for dep, (a1, a2, a3) in att.items():
d = leggi(PN.accumula(paths, dep, muro, CC.TAX_RATE, TX.PATRIMONIALE,
start=START)["colpito"])
r3 &= abs(d["med"] - a1) < 0.2 and abs(d["p20"] - a3) < 0.02
print(f" {dep:>7.0f}{d['med']:>10.1f}{a1:>9.1f}{d['med_cond']:>10.1f}{a2:>9.1f}"
f"{d['p20']:>9.1%}{a3:>9.0%}")
ok["R3"] = r3
# R4: P0 del mio motore == PN.accumula, bit-exact
A = PN.accumula(paths, DEP_MAIN, muro, CC.TAX_RATE, TX.PATRIMONIALE, start=START)
Bp = accumula_pol(paths, flat, DEP_MAIN, muro, CC.TAX_RATE, TX.PATRIMONIALE,
pol_piatto(), start=START)
dcap = float(np.max(np.abs(A["cap"] - Bp["cap"])))
dcol = int(np.max(np.abs(A["colpito"] - Bp["colpito"])))
ok["R4"] = dcap == 0.0 and dcol == 0 and abs(A["versato"] - Bp["versato"]) < 1e-9
print(f"\n R4 P0 di QUESTO motore (con buffer di cassa) vs `PN.accumula` importata:")
print(f" max|dif| capitale = {dcap:.3e} · max|dif| giorno d'arrivo = {dcol} · "
f"versato identico{'':>3}{'BIT-EXACT' if ok['R4'] else 'DIVERGE'}")
print(f" -> il buffer non introduce nulla: con versamento immediato il motore E' quello pubblicato.")
print(f"\n ESITO REPLICA: {sum(ok.values())}/{len(ok)}"
+ ", ".join(f"{k}={'OK' if v else 'NO'}" for k, v in ok.items()))
if not all(ok.values()):
raise SystemExit("REPLICA INCOMPLETA: un delta sarebbe indistinguibile da un bug.")
# ------------------------------------------------------------------ 2
sez("2) RISOLUZIONE MONTE CARLO — quanto vale la cifra che si pubblica")
print(f"\n {N_PATHS} percorsi, blocchi da {BLOCK} giorni, {ANNI} anni. I confronti fra")
print(" politiche sono APPAIATI (stessi percorsi) quindi la loro varianza e' molto minore")
print(" di quella dei livelli. Misurata su 3 semi:")
mc = []
for s in SEMI_MC:
rg = np.random.default_rng(s)
i2 = boot_idx(len(r), N_PATHS, ANNI * 365, BLOCK, rg)
p2, f2 = r[i2], flat_src[i2]
b = leggi(accumula_pol(p2, f2, DEP_MAIN, muro, CC.TAX_RATE, TX.PATRIMONIALE,
pol_piatto(), start=START)["colpito"])
d = leggi(accumula_pol(p2, f2, DEP_MAIN, muro, CC.TAX_RATE, TX.PATRIMONIALE,
pol_dip(0.10), start=START)["colpito"])
mc.append((s, b["p20"], d["p20"], d["p20"] - b["p20"]))
del p2, f2, i2
print(f"\n {'seme':>10}{'P0 P(20a)':>12}{'P1 dip 10% P(20a)':>20}{'differenza APPAIATA':>22}")
for s, a, bb, dd in mc:
print(f" {s:>10}{a:>12.1%}{bb:>20.1%}{dd:>22.2%}")
liv = max(x[1] for x in mc) - min(x[1] for x in mc)
dif = max(x[3] for x in mc) - min(x[3] for x in mc)
print(f"\n ampiezza fra semi: LIVELLO {liv:.2%} · DIFFERENZA APPAIATA {dif:.2%}")
print(f" -> si cita la differenza appaiata al punto percentuale; un livello si cita a "
f"{liv:.1%} di rumore. {clock()}")
# ------------------------------------------------------------------ 3
sez(f"3) LA TABELLA — 12 celle candidate contro il piatto, stesso flusso di cassa")
print(f"""
Lettura delle colonne. `med` = mediana INCONDIZIONATA degli anni al capitale-rendita
(non-arrivo = +inf, mai -1); `m|arr` = mediana condizionata all'arrivo, che NON si legge mai
senza la sua P accanto; `dP20` = differenza APPAIATA di P(20a) contro P0 sugli stessi
percorsi; `attesa` = giorni medi che un euro passa in cassa prima di entrare nel libro
(legge di Little, L/lambda); `prezzo` = livello di equity medio PONDERATO PER IMPORTO a cui
i soldi sono entrati, in rapporto a quello di P0 — sotto 1.000 la politica ha comprato
davvero piu' in basso. Bersaglio ${muro:,.0f}; traguardo su libro+cassa.""")
R = {}
for dep in (DEP_MAIN, DEP_ALT):
R[dep] = tabella(paths, flat, dep, muro, perp,
f"EUR {dep:.0f}/mese di flusso disponibile {clock()}", start=START)
# ------------------------------------------------------------------ 4
sez("4) IL CONTROLLO DEL RITARDO — «il segnale informa» contro «aspettare costa»")
print("""
Ogni politica condizionale fa DUE cose: sceglie il momento (informazione) e ritarda i soldi
(costo). Il controllo isola la seconda: stessa attesa MEDIA, ma deterministica, nessun
segnale. Se la politica condizionale non batte il proprio ritardo-gemello, non ha
informazione — ha solo aspettato.""")
info = {}
for etichetta, (P, FL, RR) in {"BLOCCHI 20g (mercato vero)": (paths, flat, R[DEP_MAIN]),
"IID blocco=1 (null)": (None, None, None)}.items():
if P is None: # il null iid: si ricostruiscono i path e le celle
rg = np.random.default_rng(SEED)
i2 = boot_idx(len(r), N_PATHS, ANNI * 365, 1, rg)
P, FL = r[i2], flat_src[i2]
del i2
RR = {n: accumula_pol(P, FL, DEP_MAIN, muro, CC.TAX_RATE, TX.PATRIMONIALE,
pol, start=START) for n, pol in celle(DEP_MAIN).items()}
print(f"\n {etichetta}")
print(f" {'politica':<16}{'attesa g.':>11}{'P(20a) reale':>14}"
f"{'P(20a) ritardo-gemello':>25}{'informazione':>15}")
col = {}
for nome in ("P1 dip 5%", "P1 dip 10%", "P1 dip 15%", "P1 dip 20%", "P2 risk-off"):
rr = RR[nome]
k = int(round(rr["attesa_g"]))
g = accumula_pol(P, FL, DEP_MAIN, muro, CC.TAX_RATE, TX.PATRIMONIALE,
pol_lag(k, DEP_MAIN), start=START)
a, b = leggi(rr["colpito"])["p20"], leggi(g["colpito"])["p20"]
col[nome] = a - b
print(f" {nome:<16}{k:>11}{a:>14.1%}{b:>25.1%}{a - b:>15.2%}")
info[etichetta] = col
if "IID" in etichetta:
del P, FL, RR
k1 = "BLOCCHI 20g (mercato vero)"
k2 = "IID blocco=1 (null)"
cost15 = (leggi(R[DEP_MAIN]["P1 dip 15%"]["colpito"])["p20"]
- leggi(R[DEP_MAIN]["P0 piatto"]["colpito"])["p20"])
print(f"""
⚠️ IL PRIMO SGUARDO DICE «IL SEGNALE INFORMA» — E IL NULL IID LO REFUTA. Sulle celle profonde
la colonna `informazione` e' positiva sul mercato vero ({info[k1]['P1 dip 15%']:+.1%} a dip 15%,
{info[k1]['P1 dip 20%']:+.1%} a dip 20%): a pari attesa MEDIA, aspettare il calo batte aspettare a
caso, e leggendo solo questa riga si concluderebbe che il buy-the-dip contiene previsione.
Ma sotto IID, dove per COSTRUZIONE il passato non dice nulla del futuro, la stessa colonna
vale {info[k2]['P1 dip 15%']:+.1%} / {info[k2]['P1 dip 20%']:+.1%} — cioe' NON PIU' PICCOLA, anzi piu' grande.
-> il divario contro il ritardo-gemello e' MECCANICO, non previsione: la politica
condizionale ha attesa VARIABILE (spesso zero, ogni tanto lunghissima) mentre il gemello ha
attesa FISSA, e versa il buffer tutto in una volta. Un'attesa variabile con la stessa media
di una fissa non produce lo stesso capitale (Jensen), e il null lo mostra senza ambiguita'.
⚠️ Il gemello non e' un controllo perfetto proprio per questo: e' il controllo del RITARDO
MEDIO, non della sua distribuzione. Il suo compito qui e' fissare l'ordine di grandezza —
il ritardo di dip 15% costa {abs(cost15):.0%} di P(20a) contro un divario col gemello di
{info[k1]['P1 dip 15%']:+.1%}, e sarebbe la conclusione anche prendendo il gemello alla lettera.""")
print(f" {clock()}")
# ------------------------------------------------------------------ 5
sez("5) I NULL")
print("""
(a) BOOTSTRAP IID (blocco = 1 giorno). Distrugge ogni dipendenza seriale: il passato non
informa il futuro per costruzione. Una politica che li' mantiene il suo vantaggio lo
stava prendendo dalla meccanica del bootstrap, non dal mercato. (Il controllo del
ritardo su questo stesso null e' gia' in sezione 4.)""")
rg = np.random.default_rng(SEED)
i_iid = boot_idx(len(r), N_PATHS, ANNI * 365, 1, rg)
p_iid, f_iid = r[i_iid], flat_src[i_iid]
del i_iid
I = tabella(p_iid, f_iid, DEP_MAIN, muro, perp, f"IID (blocco=1) {clock()}", start=START)
del p_iid, f_iid
print("""
(c) DRIFT DIMEZZATO — lo stress standard del progetto (7,4 anni di storia, due tori dentro).
Bersaglio tenuto FISSO al muro della lente piena: si stressa l'accumulo, non l'obiettivo
(col muro ricalcolato non arriverebbe nessuno e il confronto perderebbe potenza).""")
r_half = r - 0.5 * r.mean()
rg = np.random.default_rng(SEED)
i_h = boot_idx(len(r_half), N_PATHS, ANNI * 365, BLOCK, rg)
p_h, f_h = r_half[i_h], flat_src[i_h]
del i_h
H = tabella(p_h, f_h, DEP_MAIN, muro, perp,
f"DRIFT DIMEZZATO ({r_half.mean() * 365:.2%}/anno) {clock()}", start=START)
half20 = (leggi(H["P1 dip 20%"]["colpito"])["p20"]
- leggi(H["P0 piatto"]["colpito"])["p20"])
del p_h, f_h
# ------------------------------------------------------------------ 6
sez("6) RISCHIO DI VENUE — una politica che tiene i soldi FUORI e' anche una politica di rischio")
print("""
Il fatto strutturale: la cassa che aspetta non e' sull'exchange. Se il vantaggio (o il minor
danno) di una politica condizionale venisse da li', sarebbe un risultato sul RISCHIO, non sul
timing. I due effetti si separano leggendo la stessa coppia a p=0 (solo timing) e a p>0.
Convenzione: al fallimento il LIBRO va a zero, la CASSA sopravvive, e si riparte (conto nuovo,
versamenti che continuano) — dichiarata, e diversa da quella di r0726_venue_risk, che nel caso
a un venue solo scartava anche i versamenti futuri. `p` NON e' stimato: e' sensibilita'.""")
print(f"\n {'p annua':>9} {'politica':<16}{'P(20a)':>9}{'dP20 vs P0':>13}"
f"{'P(libro azzerato)':>20}{'EUR/g a 20a':>13}{'effetto rischio':>17}")
d0 = {}
for p in P_VENUE:
h = 1.0 - (1.0 - p) ** (1.0 / 365.0) if p > 0 else 0.0
F = None
if p > 0:
rgv = np.random.default_rng(726)
F = np.zeros((N_PATHS, ANNI * 365), bool)
tt = rgv.geometric(h, size=N_PATHS)
for _ in range(8):
m = tt < ANNI * 365
if not m.any():
break
F[np.where(m)[0], tt[m]] = True
tt = tt + rgv.geometric(h, size=N_PATHS)
base = None
for nome in ("P0 piatto", "P1 dip 10%", "P1 dip 20%", "P4 anti 10%"):
rr = accumula_pol(paths, flat, DEP_MAIN, muro, CC.TAX_RATE, TX.PATRIMONIALE,
celle(DEP_MAIN)[nome], start=START, fail=F)
d = leggi(rr["colpito"])
if base is None:
base = d["p20"]
rend = float(np.median((rr["cap"] + rr["cash"]) * perp / 365.0 / CC.EURUSD))
dd = d["p20"] - base
if p == 0.0:
d0[nome] = dd
eff = "" if p == 0.0 else f"{dd - d0[nome]:+.2%}"
print(f" {f'{p:.1%}' if nome.startswith('P0') else '':>9} {nome:<16}"
f"{d['p20']:>9.1%}{dd:>13.2%}{rr['p_perso']:>20.1%}{rend:>13.2f}{eff:>17}")
del F
print(f"""
`effetto rischio` = di quanto il rischio di venue RIDUCE la penalita' della politica
condizionale rispetto a p=0. E' il pezzo di risultato che NON e' timing: la cassa in attesa
non e' sull'exchange, quindi non brucia. Il meccanismo esiste ed e' misurabile, ma vale
qualche punto percentuale contro una penalita' di timing di decine — e la colonna
`P(libro azzerato)` e' IDENTICA per tutte le politiche (il conto salta comunque: cambia
quanto c'e' sopra, non se salta). Chi volesse comprare quella protezione la compra con un
SECONDO CONTO, non ritardando i bonifici (25/07 §4: «un rischio non-diversificabile si compra
con un secondo CONTO, non con un secondo sleeve»).""")
print(f" {clock()}")
# ------------------------------------------------------------------ 7
sez("7) SENSIBILITA' — le due convenzioni che potrebbero decidere al posto della misura")
print(f"\n (i) CASSA a 0% (assunzione XEON) contro cassa al 2%/anno nominale:")
print(f" {'politica':<16}{'P(20a) cassa 0%':>18}{'P(20a) cassa 2%':>18}{'delta':>10}")
for nome in ("P0 piatto", "P1 dip 10%", "P1 dip 20%", "P2 risk-off", "P3 VA g=0%"):
a = leggi(R[DEP_MAIN][nome]["colpito"])["p20"]
b = leggi(accumula_pol(paths, flat, DEP_MAIN, muro, CC.TAX_RATE, TX.PATRIMONIALE,
celle(DEP_MAIN)[nome], start=START,
cash_rate=0.02)["colpito"])["p20"]
print(f" {nome:<16}{a:>18.1%}{b:>18.1%}{b - a:>10.2%}")
print(f"\n (ii) TRAGUARDO su libro+cassa (favorevole alle condizionali, usato sopra) contro")
print(f" traguardo su SOLO LIBRO (severo: la cassa a 0% non produce rendita):")
print(f" {'politica':<16}{'P(20a) libro+cassa':>21}{'P(20a) solo libro':>20}{'delta':>10}")
for nome in ("P0 piatto", "P1 dip 10%", "P1 dip 20%", "P2 risk-off", "P3 VA g=0%"):
a = leggi(R[DEP_MAIN][nome]["colpito"])["p20"]
b = leggi(accumula_pol(paths, flat, DEP_MAIN, muro, CC.TAX_RATE, TX.PATRIMONIALE,
celle(DEP_MAIN)[nome], start=START,
traguardo_include_cassa=False)["colpito"])["p20"]
print(f" {nome:<16}{a:>21.1%}{b:>20.1%}{b - a:>10.2%}")
print(f" {clock()}")
# ------------------------------------------------------------------ 8
sez("VERDETTO")
P0 = R[DEP_MAIN]["P0 piatto"]
base = leggi(P0["colpito"])
tab = {n: leggi(v["colpito"]) for n, v in R[DEP_MAIN].items() if n != "P0 piatto"}
best = max(tab, key=lambda k: tab[k]["p20"])
vere = {n: v for n, v in tab.items() if R[DEP_MAIN][n]["attesa_g"] >= 30.0}
bvera = max(vere, key=lambda k: vere[k]["p20"])
sopra = sum(1 for n in tab if tab[n]["p20"] > base["p20"])
dips = [tab[n]["p20"] - base["p20"] for n in tab if n.startswith("P1")]
anti = [tab[n]["p20"] - base["p20"] for n in tab if n.startswith("P4")]
pz = {n: R[DEP_MAIN][n]["prezzo"] / P0["prezzo"] for n in tab}
def conta(D: dict) -> str:
b = leggi(D["P0 piatto"]["colpito"])["p20"]
dd = {n: leggi(v["colpito"])["p20"] - b for n, v in D.items() if n != "P0 piatto"}
k = max(dd, key=lambda x: dd[x])
return f"{sum(1 for x in dd.values() if x > 0)}/12 ({k} {dd[k]:+.2%})"
conte = " · ".join(f"{lab} {conta(D)}" for lab, D in
(("EUR500", R[DEP_MAIN]), ("EUR250", R[DEP_ALT]),
("IID", I), ("drift/2", H)))
d250 = leggi(R[DEP_ALT]["P0 piatto"]["colpito"])
print(f"""
1. NESSUNA delle {N_TRIAL} celle candidate batte il versamento piatto: {sopra}/{N_TRIAL} sopra
P0, migliore in assoluto {best} a {tab[best]['p20'] - base['p20']:+.2%} appaiato — dentro la
risoluzione Monte Carlo della differenza appaiata ({dif:.2%}) e su una cella che aspetta
{R[DEP_MAIN][best]['attesa_g']:.0f} giorno, cioe' e' P0 travestito. La migliore cella DAVVERO
condizionale (attesa >= 30 giorni) e' {bvera}: {vere[bvera]['p20'] - base['p20']:+.1%}.
Segno medio: P1 buy-the-dip {np.mean(dips):+.1%} · P4 anti-dip {np.mean(anti):+.1%}.
Per lente (celle STRETTAMENTE sopra P0 su 12, e la migliore): {conte}.
Nessuna lente produce una cella sopra il rumore MC ({dif:.1%}) -> non c'e' niente da
deflazionare: il conteggio dei trial non e' il vincolo, il vincolo e' che il segno e'
negativo e monotono nella profondita' della condizione.
2. IL SEGNALE FUNZIONA E PERDE LO STESSO, ed e' la riga che spiega tutto. Colonna `prezzo`:
P1 dip 15% compra al {pz['P1 dip 15%']:.3f} del livello medio di P0 e P1 dip 20% al
{pz['P1 dip 20%']:.3f} — cioe' {1 - pz['P1 dip 20%']:.0%} PIU' IN BASSO, esattamente cio' che la
politica promette. E arriva al capitale-rendita nel {tab['P1 dip 20%']['p20']:.1%} dei casi contro
{base['p20']:.1%}. Comprare meglio e comprare TARDI sono la stessa mossa, e la seconda meta'
domina di un ordine di grandezza.
⚠️ E il dettaglio che ribalta l'intuizione sulle soglie BASSE: P1 dip 5% compra a
{pz['P1 dip 5%']:.3f}, cioe' PIU' IN ALTO di P0. Su una serie che sale, una condizione poco
profonda non compra il calo: ritarda dentro la salita. Solo le soglie che fanno male
comprano davvero piu' in basso — ed e' esattamente il motivo per cui non conviene.
3. P4 ANTI-DIP, il controllo speculare, NON e' simmetrico — e la sua asimmetria e' la misura
del meccanismo: su una serie che sale, un rialzo del 20% dal minimo arriva in
{R[DEP_MAIN]['P4 anti 20%']['attesa_g']:.0f} giorni mentre un ribasso del 20% dal massimo ne chiede
{R[DEP_MAIN]['P1 dip 20%']['attesa_g']:.0f}. Il costo non e' «scommettere sul lato sbagliato»: e'
«mettere una condizione rara davanti a un bonifico». Qualunque condizione rara, in
qualunque direzione, costa. P1 e P4 NON danno lo stesso segno di taglia: se lo dessero
staremmo misurando rumore.
4. ORDINE DI GRANDEZZA, che e' il modo in cui questo risultato va usato: la migliore politica
di timing vale {tab[bvera]['p20'] - base['p20']:+.1%} di P(20a) — indistinguibile da zero
sulla risoluzione MC di {dif:.1%}; passare da EUR {DEP_ALT:.0f} a EUR {DEP_MAIN:.0f} al mese vale
{base['p20'] - d250['p20']:+.0%} ({d250['p20']:.0%} -> {base['p20']:.0%}). Non c'e' rapporto da
calcolare fra un effetto misurato e uno che non si distingue dal rumore: la prima colonna
e' una decisione, la seconda no.
5. ATTESA A PRIORI (dichiarata prima di misurare): «non funziona, perche' entrare piu' tardi
domina il comprare piu' in basso». CONFERMATA, e col meccanismo misurato invece che
assunto. Cio' che l'attesa NON prevedeva e' il punto 2: il segnale compra davvero molto
piu' in basso — {1 - pz['P1 dip 20%']:.0%} — e perde comunque. Sapere PERCHE' non serve vale piu'
del verdetto: la domanda non tornera' sotto forma di «e se aspettassi un calo piu'
profondo?», perche' e' proprio la profondita' a peggiorare il risultato, in modo monotono
su tutte e 4 le soglie e su tutte e 4 le lenti.
6. DUE ERRORI CATTURATI SU ME STESSO, entrambi da un controllo e non a occhio.
(i) La colonna `informazione` di sezione 4 sul solo mercato vero mi avrebbe fatto scrivere
«il buy-the-dip contiene previsione»: il null IID la rende PIU' GRANDE su ritorni dove non
c'e' niente da prevedere -> e' meccanica (attesa variabile contro fissa). Una colonna
positiva non e' un'evidenza finche' non gira anche sul null.
(ii) Senza la colonna `attesa` avrei letto P4 anti-dip come «stesso segno di P1 => rumore»
(regola dichiarata nel mandato). Falso: P4 e' piatto perche' in un mercato che sale la sua
condizione e' quasi sempre gia' vera ({R[DEP_MAIN]['P4 anti 20%']['attesa_g']:.0f} giorni di attesa contro
{R[DEP_MAIN]['P1 dip 20%']['attesa_g']:.0f}), cioe' e' P0 travestito, non uno specchio informativo.
Lo specchio va letto sulla colonna dell'ATTESA, non su quella del segno.
COSA QUESTO NON MISURA: politiche che cambiano il FLUSSO disponibile (versare di piu' quando
si guadagna di piu' e' un'altra domanda, ed e' fuori dal controllo del libro); orizzonti
diversi da 25 anni; il caso in cui la cassa in attesa renda davvero qualcosa (sez. 7 (i):
al 2% nominale la penalita' si riduce di ~4pp e non cambia segno); e il fatto che la finestra
storica del libro sono 7,4 anni con due tori — su un campione con un orso lungo il costo del
ritardo sarebbe minore (lo si vede nella colonna DRIFT DIMEZZATO: la penalita' scende da
{tab['P1 dip 20%']['p20'] - base['p20']:+.0%} a {half20:+.0%}, ma resta negativa a tutte e 12 le celle).
""")
print(f" fine {clock()}")
if __name__ == "__main__":
main()