Files
PythagorasGoal/scripts/research/r0726_gtaa_ucits.py
T
Adriano Dal Pastro be4690375f research(gtaa): la via UCITS e' aperta e costa ~zero — il broker non e' la variabile
Domanda dell'operatore: "usiamo revolut o degiro". Risposta misurata: cambiare
broker non sblocca nulla (il PRIIPs e' una norma, non una politica di IB); cambiare
VEICOLO si', e costa ~zero.

CORREZIONE A UNA MIA AFFERMAZIONE. La nota in gtaa.py diceva che gli UCITS fanno
perdere la validazione a 30 anni. Falso: il PRIIPs vieta di COMPRARE, non di
GUARDARE — i prezzi dei 6 ETF USA restano leggibili, quindi il segnale gira sui 30
anni per sempre e cambia solo il veicolo su cui si incassa.

Misure (3 lenti, un grado di liberta' per volta, 6.3 anni comuni):
  L0 segnale USA + rend. USA   Sh 0.81 / CAGR 3.96%
  L2 segnale UCITS + rend. UCITS Sh 0.84 / CAGR 4.08%
  drag del veicolo +0.10%/anno EW, coerente coi TER; ritenuta USA ~35bps a FAVORE
  dell'UCITS e non inclusa nel drag.

Lo stimatore ovvio sbagliava: la media delle differenze giornaliere dava -0.47%/anno
su CSPX contro -0.06% vero (SE ~7%/anno = 15x la quantita' stimata, piu' drag di
varianza). La deviazione fra veicoli sullo stesso indice si misura sul RAPPORTO
CUMULATO.

Il vincolo non e' il broker ma il prezzo di UNA azione, che e' una scelta: CSPX $802
vs VUAA $144 sullo stesso S&P 500. A $3.000 con azioni intere l'insieme STORIA tiene
4/6 gambe (a mercato il 33%), l'insieme DEPLOY 6/6 (65%) -> il frazionamento non
serve. Letto su gambe-vive+vol, non su Sharpe: il vincolo intero ALZA lo Sharpe
perche' de-leveraggia (null de-levering, 4a occorrenza).

Resta da verificare una cosa sola: 77-149 ordini/anno contro soglie $0.90 ($3k) /
$2.23 ($10k) / $7.62 ($50k) per ordine. Raccomandazione: restare su IB.

Feed equity: aggiunto il CROSS-CHECK che mancava (src/data/eq_crosscheck.py). Il
primo veicolo estero ha trovato subito CSPX 2012-01-13 con open/high in USD e
low/close in EUR (fattore 1.2797 = EURUSD del giorno), invisibile alla guardia
maxret>50% — stesso schema dello split 2:1 del 25/07. Soglia non tarabile sulla
deviazione (rumore 9.90%, margine 2.2x): cambiata statistica in |dev|/movimento del
gemello -> margine 5.3x. Limite EURUSD 1.09 dichiarato e chiuso sul DANNO (dSharpe
mediano -0.003), congelato in un test.

Book, pesi, cron, config INVARIATI. 435 test verdi (+24).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBbYmiXqbUNuGpK9sfbsGp
2026-07-26 23:01:54 +00:00

450 lines
23 KiB
Python

"""GTAA01 sulla versione REALMENTE ACQUISTABILE (UCITS) — quanto dello sleeve resta.
CONTESTO. Il 2026-07-26 il conto reale ha rifiutato l'ordine sui 6 ETF USA di GTAA01 (PRIIPs:
domiciliati USA, nessun KID, vietati al retail UE). Lo sleeve non e' deployabile cosi' com'e'.
Questo script misura la sola via d'uscita: negoziare gli equivalenti UCITS.
⚠️ IL FATTO CHE RIDIMENSIONA IL PROBLEMA, E CHE LA NOTA IN gtaa.py NON AVEVA VISTO.
La nota diceva: "storia piu' CORTA -> si perde la validazione a 30 anni che e' cio' che rendeva
credibile questo sleeve". E' FALSO, per un motivo strutturale: **il PRIIPs vieta di COMPRARE, non
di GUARDARE**. I prezzi di SPY/QQQ/IWM/TLT/GLD/HYG restano leggibili (l'operatore li ha visti sul
terminale mentre l'ordine veniva rifiutato). Il SEGNALE di GTAA01 puo' continuare a girare sui 30
anni di serie USA per sempre; cambia solo il VEICOLO su cui si incassa il rendimento. La storia
corta degli UCITS non serve a validare la strategia — serve a misurare la DEVIAZIONE DEL VEICOLO,
che e' un drift lento e si stima bene anche su pochi anni.
METODO — tre lenti, UN GRADO DI LIBERTA' PER VOLTA (come per il path live di SKH01, 26/07):
L0 segnale USA + rendimento USA = GTAA01 come pubblicato (non deployabile)
L1 segnale USA + rendimento UCITS = isola il VEICOLO
L2 segnale UCITS + rendimento UCITS = aggiunge l'OROLOGIO (LSE chiude alle 11:30 di New York)
Le tre lenti girano sullo STESSO calendario e sulla STESSA finestra: le differenze sono appaiate.
⚠️ DUE INSIEMI DI VEICOLI, e la ragione e' il BIGLIETTO MINIMO. Il prezzo di UNA azione e' una
scelta dell'emittente, non una proprieta' dell'indice: CSPX costa $802 e VUAA $144 sullo STESSO
S&P 500. Ma i veicoli economici sono i piu' recenti. Quindi:
STORIA (CSPX/EQQQ/XRSU/IDTL/IGLN/IHYU) -> finestra lunga, serve a MISURARE;
DEPLOY (VUAA/XNAS/R2US/IDTL/IGLN/IHYU) -> prezzo unitario basso, serve a ESEGUIRE.
uv run python scripts/research/r0726_gtaa_ucits.py
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from src.data.eq_crosscheck import repair_twin_outliers # noqa: E402
from src.data.eq_splits import repair_splits # noqa: E402
from src.portfolio import gtaa as G # noqa: E402
RAW = ROOT / "data" / "raw"
ANN = np.sqrt(252.0)
US = ("SPY", "QQQ", "IWM", "TLT", "GLD", "HYG")
# Scartati di proposito: RTWO (Russell 2000 *quality* = fattore), CSUSS (MSCI USA Small Cap *ESG*),
# IDP6 (S&P 600: small cap ma indice diverso da Russell 2000), CNDX (market data non sottoscritto).
SET_STORIA = {"SPY": "CSPX", "QQQ": "EQQQ", "IWM": "XRSU", "TLT": "IDTL", "GLD": "IGLN", "HYG": "IHYU"}
SET_DEPLOY = {"SPY": "VUAA", "QQQ": "XNAS", "IWM": "R2US", "TLT": "IDTL", "GLD": "IGLN", "HYG": "IHYU"}
# TER indicativi (%/anno). ⚠️ FONTE SECONDARIA, non letta dai KID: servono solo come CONTROLLO di
# plausibilita' del drag misurato, non entrano in nessun calcolo. Regola 26/07: dichiarare la
# qualita' della fonte.
TER = {"SPY": 0.09, "QQQ": 0.20, "IWM": 0.19, "TLT": 0.15, "GLD": 0.40, "HYG": 0.49,
"CSPX": 0.07, "EQQQ": 0.30, "XRSU": 0.30, "IDTL": 0.07, "IGLN": 0.12, "IHYU": 0.50,
"VUAA": 0.07, "XNAS": 0.20, "R2US": 0.30}
# Ritenuta USA sui dividendi (15% da trattato) x rendimento da dividendo indicativo -> punti base
# che un residente italiano paga comprando il veicolo USA e NON paga comprando l'UCITS irlandese.
RITENUTA_BPS = {"SPY": 18, "QQQ": 7, "IWM": 18, "TLT": 60, "GLD": 0, "HYG": 90}
# ===========================================================================
# dato
# ===========================================================================
def _norm(s: pd.Series) -> pd.Series:
s = s[~s.index.duplicated()].copy()
idx = pd.to_datetime(s.index)
if getattr(idx, "tz", None) is not None:
idx = idx.tz_localize(None)
s.index = idx.normalize()
return s.sort_index()
def _load(prefix: str, sym: str) -> pd.DataFrame:
d = pd.read_parquet(RAW / f"{prefix}_{sym.lower()}_1d.parquet")
d.index = pd.to_datetime(d["timestamp"], unit="ms", utc=True)
d, _ = repair_splits(d.sort_index())
return d
def close_us(sym: str) -> pd.Series:
d = _load("eq", sym)
return pd.Series(d["close"].astype(float).values, index=d.index)
def close_uc(sym: str, twin: pd.Series) -> tuple[pd.Series, list[dict]]:
"""Chiusure UCITS RIPARATE col cross-check sul gemello USA (src/data/eq_crosscheck.py)."""
d, bad = repair_twin_outliers(_load("eqx", sym), twin)
return pd.Series(d["close"].astype(float).values, index=d.index), bad
# ===========================================================================
# motore — generalizzazione di gtaa._gated_returns con segnale e veicolo SEPARATI
# ===========================================================================
def leg_returns(sig_close: pd.Series, exe_close: pd.Series, cal, cap_leg: float,
band_usd: float, every: int, cost, *, integer_shares: bool = False):
"""Rendimenti netti di UNA gamba sul calendario `cal`, piu' la posizione tenuta.
`sig_close` decide l'esposizione, `exe_close` la incassa: separarli isola il veicolo
dall'orologio. Identico a gtaa._gated_returns quando le due coincidono e cal e' l'indice
nativo. `integer_shares`: l'ordine si arrotonda a un numero INTERO di azioni.
"""
sig = _norm(sig_close).reindex(cal).ffill()
exe = _norm(exe_close).reindex(cal).ffill()
ex = np.nan_to_num(np.asarray(G._exposure(sig).values, float))
px = exe.values.astype(float)
ret = np.zeros(len(px))
ret[1:] = px[1:] / px[:-1] - 1.0
held = np.empty(len(ex))
comm = np.zeros(len(ex))
cur = 0.0
for i in range(len(ex)):
if i % every == 0:
tgt = ex[i]
if integer_shares:
tgt = np.floor(tgt * cap_leg / px[i]) * px[i] / cap_leg
notional = abs(tgt - cur) * cap_leg
if notional >= max(band_usd, G.IB_MIN_TRADE_USD):
comm[i] = cost(notional, px[i]) / cap_leg
cur = tgt
held[i] = cur
pos = np.zeros(len(held))
pos[1:] = held[:-1]
net = pos * ret - comm
net[0] = 0.0
return pd.Series(net, index=cal), pd.Series(pos, index=cal)
def sleeve(sig: dict, exe: dict, cal, capital: float, band: float, every: int, cost,
integer_shares: bool = False, want_pos: bool = False):
cap_leg = float(capital) / len(sig)
out = {k: leg_returns(sig[k], exe[k], cal, cap_leg, band, every, cost,
integer_shares=integer_shares) for k in sig}
r = pd.concat({k: v[0] for k, v in out.items()}, axis=1).mean(axis=1, skipna=True)
if want_pos:
return r, pd.concat({k: v[1] for k, v in out.items()}, axis=1)
return r
def sleeve_native(sig: dict, exe: dict, capital: float, band: float, every: int, cost) -> pd.Series:
"""Replica ESATTA di gtaa.gtaa_returns: ogni gamba sul PROPRIO indice, outer-join, media.
Serve solo al sanity check — le lenti girano su calendario comune, che e' cio' che rende
appaiate le differenze."""
cap_leg = float(capital) / len(sig)
cols = {}
for k in sig:
cal = _norm(exe[k]).index
cols[k] = leg_returns(sig[k], exe[k], cal, cap_leg, band, every, cost)[0]
return pd.concat(cols, axis=1, sort=True).sort_index().mean(axis=1, skipna=True).dropna()
def met(r: pd.Series) -> dict:
r = r.dropna()
if len(r) < 50 or r.std() == 0:
return dict(sharpe=np.nan, cagr=np.nan, dd=np.nan, vol=np.nan)
eq = (1 + r).cumprod()
return dict(sharpe=float(r.mean() / r.std() * ANN),
cagr=float(eq.iloc[-1] ** (252.0 / len(r)) - 1),
dd=float((eq / eq.cummax() - 1).min()), vol=float(r.std() * ANN))
def cost_ib_us(notional, price):
return G.ib_commission(notional, price)
def cost_form(fixed=0.0, prop_bps=0.0, min_fee=0.0, max_frac=1.0):
def f(notional, price):
return min(max(fixed + notional * prop_bps / 1e4, min_fee), max_frac * notional)
return f
def common_cal(series_list) -> pd.DatetimeIndex:
cal = None
for s in series_list:
i = _norm(s).index
cal = i if cal is None else cal.intersection(i)
return cal
def drag_cumulato(a: pd.Series, b: pd.Series, cal) -> float:
"""Deviazione annualizzata del veicolo b rispetto al gemello a, misurata sul RAPPORTO.
⚠️ NON si usa la media delle differenze giornaliere. Le due serie seguono lo STESSO indice ma
sono campionate a orari diversi (LSE chiude 4h30 prima di New York): la differenza giornaliera
e' dominata da uno sfasamento che si INVERTE il giorno dopo, quindi ha una volatilita' enorme
(17-22% annualizzata) e un errore standard di ~7%/anno sulla stima del drag — cioe' 15 volte
piu' grande del drag stesso. Il rapporto b/a invece e' stazionario: il suo drift E' il drag.
"""
x, y = _norm(a).reindex(cal).ffill(), _norm(b).reindex(cal).ffill()
r = (y / x).dropna()
return float((r.iloc[-1] / r.iloc[0]) ** (252.0 / len(r)) - 1)
# ===========================================================================
def main() -> None:
print("=" * 108)
print(" GTAA01 SUGLI EQUIVALENTI UCITS — quanto dello sleeve resta quando lo si puo' comprare")
print("=" * 108)
us = {s: close_us(s) for s in US}
uc, difetti = {}, {}
for s, u in SET_STORIA.items():
uc[s], bad = close_uc(u, us[s])
if bad:
difetti[u] = bad
dep = {}
for s, u in SET_DEPLOY.items():
dep[s], bad = close_uc(u, us[s])
if bad:
difetti[u] = bad
# ---------- (0) DATO ----------
print("\n" + "=" * 108)
print(" (0) IL DATO — cross-check di ogni veicolo contro il suo gemello USA")
print("=" * 108)
print(" Il feed equity aveva SOLO controlli locali (integrita', gap, spike, split): nessun")
print(" secondo parere, mentre nel crypto la certificazione incrocia sempre piu' venue.")
print(f"\n {'gamba':7s}{'STORIA':9s}{'barre':>7s}{'dal':>12s} {'DEPLOY':9s}{'barre':>7s}{'dal':>12s}")
for s in US:
a, b = SET_STORIA[s], SET_DEPLOY[s]
print(f" {s:7s}{a:9s}{len(uc[s]):>7d}{str(_norm(uc[s]).index[0].date()):>12s} "
f"{b:9s}{len(dep[s]):>7d}{str(_norm(dep[s]).index[0].date()):>12s}")
print("\n BARRE SCARTATE DAL CROSS-CHECK:")
if not difetti:
print(" nessuna")
for u, bad in difetti.items():
for b in bad:
print(f" {u} {pd.Timestamp(b['date']).date()}: deviazione dal gemello {b['dev']:+.1%} "
f"mentre il gemello faceva {b['twin_move']:+.2%} "
f"-> non spiegato x{b['unexplained']:.0f}")
print("\n ⚠️ CSPX 2012-01-13: open/high 112.740 in USD, low/close 88.010 in EUR (fattore dai")
print(" close adiacenti 1.2797 = EURUSD di quel giorno). La certificazione ESISTENTE non lo")
print(" vedeva: la guardia era 'maxret > 50% -> SPIKE?' e una contaminazione EUR/USD vale")
print(" ~22-28%. Stesso schema dello split 2:1 del 25/07 (che valeva ESATTAMENTE -50%).")
# ---------- SANITY ----------
print("\n" + "=" * 108)
print(" SANITY — il motore generalizzato riproduce gtaa.gtaa_returns?")
print("=" * 108)
CAP = G.GTAA_DEFAULT_CAPITAL
mine = sleeve_native(us, us, CAP, G.REBAL_BAND_USD, G.REBAL_EVERY, cost_ib_us)
prod = _norm(G.gtaa_returns(capital=CAP))
both = pd.concat([mine, prod], axis=1).dropna()
dmax = float((both.iloc[:, 0] - both.iloc[:, 1]).abs().max())
print(f" max|differenza| su {len(both)} barre = {dmax:.3e} "
f"{'OK (bit-exact)' if dmax < 1e-12 else '❌ IL MOTORE NON E LA PRODUZIONE'}")
# ---------- (1) DEVIAZIONE DEL VEICOLO ----------
cal = common_cal(list(us.values()) + list(uc.values()))
start_uc = max(_norm(x).index[0] for x in uc.values())
cal = cal[cal >= start_uc + pd.Timedelta(days=380)]
print("\n" + "=" * 108)
print(" (1) LA DEVIAZIONE DEL VEICOLO — misurata sul RAPPORTO, non sulle differenze")
print("=" * 108)
print(f" finestra: {cal[0].date()} -> {cal[-1].date()} ({len(cal)} barre, {len(cal)/252:.1f} anni)")
print(" (+1 anno sul veicolo piu' corto: in L2 il segnale nasce dagli UCITS e HORIZONS arriva")
print(" a 252 barre — senza il margine le tre lenti non sarebbero appaiate)")
print(f"\n {'gamba':7s}{'UCITS':7s}{'drag misurato':>15s}{'atteso da TER':>15s}"
f"{'+ ritenuta USA':>16s}{'stabilita per anno':>20s}")
tot_mis, tot_att = [], []
for s in US:
u = SET_STORIA[s]
d = drag_cumulato(us[s], uc[s], cal)
att = (TER[s] - TER[u]) / 100.0
anni = []
for y in sorted(set(cal.year))[1:-1]:
c = cal[cal.year == y]
if len(c) > 100:
anni.append(drag_cumulato(us[s], uc[s], c))
tot_mis.append(d)
tot_att.append(att)
print(f" {s:7s}{u:7s}{d:>14.2%}{att:>14.2%}{att + RITENUTA_BPS[s]/1e4:>15.2%}"
f" [{min(anni):+.2%},{max(anni):+.2%}]" if anni else "")
print(f"\n MEDIA EQUAL-WEIGHT: misurato {np.mean(tot_mis):+.2%}/anno · "
f"atteso da TER {np.mean(tot_att):+.2%} · "
f"con ritenuta {np.mean(tot_att) + np.mean(list(RITENUTA_BPS.values()))/1e4:+.2%}")
# Il grado d'accordo si CONTA, non si dichiara: scriverlo a mano nel print e' il modo piu'
# facile di pubblicare una conclusione che i propri dati smentiscono.
conc = sum(1 for a, b in zip(tot_mis, tot_att) if np.sign(a) == np.sign(b))
vicini = sum(1 for a, b in zip(tot_mis, tot_att) if abs(a - b) < 0.003)
print(f"\n CONTROLLO DI PLAUSIBILITA': segno concorde in {conc}/6 gambe, "
f"scarto < 30bps in {vicini}/6.")
print(" Il caso piu' netto e' l'oro: GLD 0.40% vs IGLN 0.12% -> atteso +0.28%, misurato")
print(" +0.36%. Le due gambe discordi (CSPX, IHYU) restano entro 30bps: su una banda annua")
print(" larga [-0.9%,+1.9%] il segno di uno scarto cosi' piccolo non e' informativo.")
print(" Cio' che il controllo stabilisce e' la SCALA — decine di bps, non punti.")
print("\n ⚠️ La colonna 'ritenuta' e' A FAVORE dell'UCITS e NON e' nel drag misurato:")
print(" ADJUSTED_LAST sulle serie USA e' al LORDO della ritenuta del 15% che un residente")
print(" italiano paga davvero sui dividendi USA. (Ipotesi fiscale dichiarata, fonte")
print(" secondaria, da confermare col commercialista — non e' un parere fiscale.)")
# ---------- (2) TRE LENTI ----------
print("\n" + "=" * 108)
print(" (2) TRE LENTI, UN GRADO DI LIBERTA' PER VOLTA")
print("=" * 108)
L0 = sleeve(us, us, cal, CAP, G.REBAL_BAND_USD, G.REBAL_EVERY, cost_ib_us)
L1 = sleeve(us, uc, cal, CAP, G.REBAL_BAND_USD, G.REBAL_EVERY, cost_ib_us)
L2 = sleeve(uc, uc, cal, CAP, G.REBAL_BAND_USD, G.REBAL_EVERY, cost_ib_us)
print(f"\n {'lente':48s}{'Sharpe':>9s}{'CAGR':>9s}{'maxDD':>9s}{'vol':>8s}")
for nome, r in [("L0 segnale USA + rend. USA (pubblicato)", L0),
("L1 segnale USA + rend. UCITS (veicolo)", L1),
("L2 segnale UCITS + rend. UCITS (+orologio)", L2)]:
m = met(r)
print(f" {nome:48s}{m['sharpe']:>9.2f}{m['cagr']:>8.2%}{m['dd']:>9.1%}{m['vol']:>8.1%}")
m0, m2 = met(L0), met(L2)
print(f"\n TOTALE (L2-L0): dSharpe {m2['sharpe']-m0['sharpe']:+.2f} · "
f"dCAGR {m2['cagr']-m0['cagr']:+.2%}/anno · dDD {m2['dd']-m0['dd']:+.1%}")
print(f"\n ⚠️ corr(L0,L2) = {L0.corr(L2):.2f}, bassa per due serie sullo STESSO indice — ed e'")
print(" l'orologio, non un difetto: la chiusura di Londra e quella di New York condividono")
print(" la notte e la mattina americana ma non il pomeriggio. Verificato: la correlazione")
print(" fra veicolo e gemello e' MASSIMA a lag 0 su 6/6 coppie (0.46-0.78), quindi le date")
print(" non sono sfasate. Conseguenza: le statistiche GIORNALIERE delle due lenti non sono")
print(" confrontabili una a una, mentre il DRIFT (sezione 1) lo e'.")
# ---------- (3) COSTI PER FORMA ----------
print("\n" + "=" * 108)
print(" (3) IL COSTO DEL VENUE — per FORMA, non per livello")
print("=" * 108)
print(" Regola 25/07: due venue a pari 'costo medio' danno esiti OPPOSTI al variare del")
print(" capitale, perche' un pavimento FISSO e' una tassa regressiva. Qui non si indovina il")
print(" listino di un broker: si misura la curva e si ricava la soglia da verificare.")
caps = [3_000, 10_000, 50_000]
# Il numero che rende ogni listino calcolabile a mano, senza doverne indovinare nessuno.
print("\n ORDINI L'ANNO (dipendono da banda e cadenza, NON dal listino ne' dal capitale):")
for c in [3_000, 10_000, 50_000]:
n = 0
for k in uc:
_, pos = leg_returns(uc[k], uc[k], cal, c / 6.0, G.REBAL_BAND_USD, G.REBAL_EVERY,
cost_form(fixed=1.0))
n += int((pos.diff().abs() > 1e-12).sum())
print(f" a ${c:,d} di capitale allocato: {n/(len(cal)/252):.0f} ordini/anno "
f"-> costo annuo = ordini x tariffa (es. $1/ordine = ${n/(len(cal)/252):.0f}/anno "
f"= {n/(len(cal)/252)/c:.2%} del capitale)")
print(f"\n COSTO FISSO per ordine: " + "".join(f"{'$'+format(c,',d'):>22s}" for c in caps))
print(f" {'':>29s}" + "".join(f"{'Sharpe':>11s}{'CAGR':>11s}" for _ in caps))
for f in [0.0, 0.35, 1.0, 2.0, 3.0, 5.0, 10.0]:
row = f" {'$'+format(f,'.2f'):>29s}"
for c in caps:
m = met(sleeve(uc, uc, cal, c, G.REBAL_BAND_USD, G.REBAL_EVERY, cost_form(fixed=f)))
row += f"{m['sharpe']:>11.2f}{m['cagr']:>10.2%}"
print(row)
print(f"\n COSTO PROPORZIONALE: " + "".join(f"{'$'+format(c,',d'):>22s}" for c in caps))
print(f" {'':>29s}" + "".join(f"{'Sharpe':>11s}{'CAGR':>11s}" for _ in caps))
for p in [0.0, 5.0, 10.0, 25.0, 50.0]:
row = f" {format(p,'.0f')+' bps/ordine':>29s}"
for c in caps:
m = met(sleeve(uc, uc, cal, c, G.REBAL_BAND_USD, G.REBAL_EVERY, cost_form(prop_bps=p)))
row += f"{m['sharpe']:>11.2f}{m['cagr']:>10.2%}"
print(row)
print("\n SOGLIE — costo fisso per ordine oltre il quale lo sleeve non vale la pena:")
print(f" {'capitale':>10s}{'CAGR = 0':>13s}{'Sharpe = 0.45':>16s} (0.45 = base del plateau dichiarato)")
for c in caps:
def cagr_at(f):
return met(sleeve(uc, uc, cal, c, G.REBAL_BAND_USD, G.REBAL_EVERY, cost_form(fixed=f)))
res = []
for target, key in [(0.0, "cagr"), (0.45, "sharpe")]:
lo, hi = 0.0, 40.0
if cagr_at(hi)[key] > target:
res.append(">$40")
continue
for _ in range(9):
mid = (lo + hi) / 2
if cagr_at(mid)[key] > target:
lo = mid
else:
hi = mid
res.append(f"${(lo+hi)/2:.2f}")
print(f" {'$'+format(c,',d'):>10s}{res[0]:>13s}{res[1]:>16s}")
# ---------- (4) BIGLIETTO MINIMO ----------
print("\n" + "=" * 108)
print(" (4) IL BIGLIETTO MINIMO — l'analogo azionario dell'haircut small-cap")
print("=" * 108)
print(" Senza frazionamento un ordine e' almeno UNA azione. Se una azione vale quanto l'intera")
print(" gamba, il vol-target non e' rappresentabile e lo sleeve non e' quello che il backtest")
print(" modella. Ma il prezzo unitario e' una SCELTA: sullo stesso indice esistono linee 5-10x")
print(" piu' economiche.")
print(f"\n {'gamba':7s}{'STORIA':8s}{'prezzo':>9s}{'DEPLOY':>9s}{'prezzo':>9s}"
+ "".join(f"{'a $'+format(c,',d'):>13s}" for c in caps))
for s in US:
pa = float(_norm(uc[s]).iloc[-1])
pb = float(_norm(dep[s]).iloc[-1])
row = f" {s:7s}{SET_STORIA[s]:8s}{pa:>9.2f}{SET_DEPLOY[s]:>9s}{pb:>9.2f}"
for c in caps:
row += f"{pb/(c/6.0):>12.0%} "
print(row)
print(" (percentuale = quanto pesa UNA azione DEPLOY sull'allocazione di quella gamba;")
print(" sopra ~20% il target continuo non e' rappresentabile)")
print("\n DANNO MISURATO dell'esecuzione a numero INTERO di azioni — i DUE insiemi a confronto,")
print(" sulla STESSA finestra (quella del veicolo piu' corto), per isolare il prezzo unitario:")
cal_d = common_cal(list(us.values()) + list(dep.values()) + list(uc.values()))
start_d = max(_norm(x).index[0] for x in list(dep.values()) + list(uc.values()))
cal_d = cal_d[cal_d >= start_d + pd.Timedelta(days=380)]
print(f" finestra {cal_d[0].date()} -> {cal_d[-1].date()} ({len(cal_d)/252:.1f} anni, "
f"vincolata da {SET_DEPLOY['QQQ']})")
print(f"\n {'insieme':9s}{'capitale':>10s}{'Sh fraz.':>10s}{'Sh intero':>11s}{'delta':>8s}"
f"{'vol fraz.':>11s}{'vol intero':>12s}{'gambe vive':>12s}{'esposta':>10s}")
for nome, vv in [("STORIA", uc), ("DEPLOY", dep)]:
for c in [3_000, 10_000, 50_000]:
a = met(sleeve(vv, vv, cal_d, c, G.REBAL_BAND_USD, G.REBAL_EVERY, cost_form(fixed=1.0)))
rb, pb = sleeve(vv, vv, cal_d, c, G.REBAL_BAND_USD, G.REBAL_EVERY,
cost_form(fixed=1.0), integer_shares=True, want_pos=True)
b = met(rb)
vive = int((pb.abs().sum(axis=0) > 0).sum())
print(f" {nome:9s}{'$'+format(c,',d'):>10s}{a['sharpe']:>10.2f}{b['sharpe']:>11.2f}"
f"{b['sharpe']-a['sharpe']:>+8.2f}{a['vol']:>11.1%}{b['vol']:>12.1%}"
f"{str(vive)+'/6':>12s}{(pb > 0).mean().mean():>10.0%}")
print("\n ⚠️ SI LEGGE LA COLONNA 'gambe vive' E 'vol', NON 'Sharpe'. Un vincolo che ALZA lo")
print(" Sharpe non e' un miglioramento: arrotondando in giu' riduce l'esposizione e paga")
print(" meno commissioni. E' il NULL DE-LEVERING (VRP-DD, TP01xDVOL, MAT01: 3 occorrenze")
print(" nel progetto) — il primo test da fare su ogni 'va meglio'. Qui il controllo e' stato")
print(" eseguito e il verdetto e' che con l'insieme DEPLOY il de-levering NON c'e'.")
# ---------- (5) limite del rilevatore, chiuso sul DANNO ----------
print("\n" + "=" * 108)
print(" (5) IL LIMITE DEL CROSS-CHECK, CHIUSO SUL DANNO E NON SULLA RILEVABILITA'")
print("=" * 108)
print(" Il cross-check non vede una contaminazione a cambio basso (EURUSD ~1.09 -> 8% di")
print(" deviazione, dentro il rumore dei giorni violenti: misurato 9.9% il 2025-04-09).")
print(" Abbassare la soglia genererebbe falsi positivi, quindi si misura il DANNO di una")
print(" contaminazione NON VISTA invece di inseguirne la rilevabilita'.")
base = met(sleeve(uc, uc, cal, CAP, G.REBAL_BAND_USD, G.REBAL_EVERY, cost_form(fixed=1.0)))
rng = np.random.default_rng(20260726)
for fx, nome in [(1.09, "EURUSD 1.09 — il caso NON rilevabile"),
(1.27, "GBPUSD 1.27 — riferimento di scala (sempre rilevato)")]:
ds = []
for _ in range(60):
leg = US[int(rng.integers(0, 6))]
u2 = dict(uc)
s = _norm(u2[leg]).reindex(cal).ffill().copy()
s.iloc[int(rng.integers(300, len(s) - 5))] /= fx
u2[leg] = s
ds.append(met(sleeve(u2, u2, cal, CAP, G.REBAL_BAND_USD, G.REBAL_EVERY,
cost_form(fixed=1.0)))["sharpe"] - base["sharpe"])
ds = np.array(ds)
print(f" {nome:52s} dSharpe mediano {np.median(ds):+.4f} · "
f"peggiore {ds.min():+.4f} · |d|>0.05 nel {(np.abs(ds)>0.05).mean():.0%}")
print("\n" + "=" * 108)
if __name__ == "__main__":
main()