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
This commit is contained in:
Adriano Dal Pastro
2026-07-26 23:01:54 +00:00
parent 6664575e1c
commit be4690375f
8 changed files with 1481 additions and 16 deletions
+143
View File
@@ -0,0 +1,143 @@
"""FETCH + CERTIFY equivalenti UCITS dei 6 ETF di GTAA01 -> data/raw/eqx_<sym>_1d.parquet.
PERCHE' ESISTE. Il 2026-07-26 il conto reale ha RIFIUTATO l'ordine sui 6 ETF USA di GTAA01
(blocco PRIIPs: ETF domiciliati USA, nessun KID, vietati al retail UE). Lo sleeve, cosi' com'e',
non e' deployabile. L'unica via d'uscita e' negoziare gli equivalenti UCITS — e per sapere se
quella via regge servono i DATI dei veicoli, non le loro schede prodotto.
SCELTE DICHIARATE
* Tutti i candidati sono presi su **LSEETF in USD**. Non e' una comodita': e' cio' che elimina
un livello di costo (conversione valutaria per ordine) che su un CAGR del 3.65% non sarebbe
trascurabile. Le linee Xetra in EUR (SXR8/SXRV/ZPRR) NON risolvono su questo gateway e
comunque introdurrebbero l'FX.
* Namespace **eqx_** separato da eq_: i veicoli USA restano su disco perche' sono la base della
VALIDAZIONE (30 anni), che nessun UCITS puo' fornire. Il progetto valida sull'indice/veicolo
lungo e negozia il veicolo corto; sovrascrivere eq_ cancellerebbe la meta' lunga.
* Piu' candidati per gamba dove l'equivalenza NON e' ovvia (small cap: RTWO e' un fattore
*quality*, CSUSS e' *ESG* -> non sono IWM). La scelta si fa dopo, sui dati.
CERTIFICAZIONE: riusa `certify()` di fetch_ib_equities.py — stesse regole, split non aggiustati
inclusi (la lezione IWM/EFA del 25/07 vale identica su questi veicoli).
uv run --with ib_async python scripts/research/fetch_ib_ucits.py
uv run --with ib_async python scripts/research/fetch_ib_ucits.py --force
"""
from __future__ import annotations
import sys
import time
from pathlib import Path
import pandas as pd
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "scripts" / "research"))
from fetch_ib_equities import certify # noqa: E402
RAW = ROOT / "data" / "raw"
RAW.mkdir(parents=True, exist_ok=True)
EXCH, CUR = "LSEETF", "USD"
# gamba USA -> candidati UCITS, in ordine di preferenza a priori (equivalenza dell'INDICE prima
# della storia: un fattore 'quality'/'ESG' non e' la stessa esposizione anche se ha piu' barre).
#
# ⚠️ SI SCARICANO DUE INSIEMI, 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. Senza frazionamento un ordine e' almeno una azione, quindi a capitale piccolo il prezzo
# unitario decide se la gamba e' rappresentabile. Ma i veicoli economici sono anche i piu' RECENTI
# (XNAS: 4.3 anni contro i 7.5 di EQQQ) -> non si puo' scegliere una cosa sola:
# * insieme STORIA -> finestra comune piu' lunga, serve a MISURARE la deviazione del veicolo;
# * insieme DEPLOY -> prezzo unitario basso, serve a sapere se si puo' ESEGUIRE.
CANDIDATI: dict[str, tuple[str, ...]] = {
"SPY": ("CSPX", "VUAA"), # S&P 500: CSPX $802 (storia) · VUAA $144 (deploy)
"QQQ": ("CNDX", "EQQQ", "XNAS"), # Nasdaq-100: EQQQ $693 (storia) · XNAS $66 (deploy)
"IWM": ("XRSU", "R2US", "IDP6"), # Russell 2000: XRSU $438 · R2US $86 (piu' economico E piu' lungo)
"TLT": ("IDTL", "DTLA"), # US Treasury 20+ — indice identico, IDTL $3.09
"GLD": ("IGLN",), # oro fisico (ETC, non UCITS: ha comunque il KID)
"HYG": ("IHYU",), # USD High Yield corp — indice equivalente
}
UNIVERSE = [s for v in CANDIDATI.values() for s in v]
def main() -> int:
try:
from ib_async import IB, Stock
except Exception:
print("ib_async assente. Esegui con: uv run --with ib_async python scripts/research/fetch_ib_ucits.py")
return 2
ib = IB()
try:
ib.connect("127.0.0.1", 4002, clientId=91, timeout=20)
except Exception as e:
print(f"[CONNESSIONE FALLITA] 127.0.0.1:4002 -> {repr(e)[:120]}\n Avvia: docker compose up -d ib-gateway")
return 1
print("=" * 108)
print(f" FETCH + CERTIFY UCITS ({EXCH}/{CUR}, ADJUSTED_LAST) -> data/raw/eqx_* | acct {ib.managedAccounts()}")
print("=" * 108)
force = "--force" in sys.argv[1:]
ok, rep = [], []
for sym in UNIVERSE:
out_path = RAW / f"eqx_{sym.lower()}_1d.parquet"
if out_path.exists() and not force:
print(f" {sym:6} GIA' SU DISCO -> skip (--force per riscaricare)")
ok.append(sym)
continue
con = Stock(sym, EXCH, CUR)
try:
det = ib.reqContractDetails(con)
except Exception as e:
det = None
print(f" {sym:6} ERR contratto {type(e).__name__} {str(e)[:50]}")
if not det:
print(f" {sym:6} CONTRATTO ASSENTE su {EXCH}/{CUR}")
rep.append({"sym": sym, "status": "ASSENTE"})
time.sleep(1.2)
continue
try:
bars = ib.reqHistoricalData(det[0].contract, endDateTime="", durationStr="30 Y",
barSizeSetting="1 day", whatToShow="ADJUSTED_LAST",
useRTH=True, formatDate=1, timeout=120)
except Exception as e:
print(f" {sym:6} ERR storico {repr(e)[:70]}")
rep.append({"sym": sym, "status": "ERR"})
time.sleep(1.2)
continue
if not bars:
print(f" {sym:6} 0 barre (market data non sottoscritto?)")
rep.append({"sym": sym, "n": 0, "status": "VUOTO"})
time.sleep(1.2)
continue
df = pd.DataFrame([(pd.Timestamp(str(b.date)), b.open, b.high, b.low, b.close, b.volume)
for b in bars],
columns=["ts", "open", "high", "low", "close", "volume"]).set_index("ts").sort_index()
c = certify(sym, df)
rep.append(c)
out = df.copy()
out["timestamp"] = out.index.astype("datetime64[ms]").astype("int64")
out.reset_index(drop=True).to_parquet(out_path)
if c["status"] == "OK":
ok.append(sym)
print(f" {sym:6} n={c.get('n',0):>5} {str(c.get('primo','')):>10}->{str(c.get('ultimo',''))} "
f"{c.get('anni','?')}y flat={c.get('flat','?')} maxret={c.get('maxret%','?')}% "
f"miss_bd={c.get('miss_bd','?')} gapL={c.get('gap_lunghi','?')} "
f"adj={c.get('adj_first/last','?')} [{c['status']}]"
+ (f" split={c['splits']}" if c.get("splits") else ""))
time.sleep(1.2)
print("-" * 108)
print(f" CERTIFICATI OK ({len(ok)}/{len(UNIVERSE)}): {ok}")
# Il vincolo che decide tutto: la finestra COMUNE alle 6 gambe scelte.
print("\n ⚠️ La profondita' per gamba conta meno della FINESTRA COMUNE: il candidato piu' corto")
print(" determina il campione su cui si potra' misurare qualunque cosa.")
ib.disconnect()
return 0
if __name__ == "__main__":
raise SystemExit(main())
+449
View File
@@ -0,0 +1,449 @@
"""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()