research(gtaa): ZPRR e' R2US (stessa ISIN) + correzione sul verso del costo FX

L'operatore ha verificato su Revolut: R2US assente, ZPRR e SPY4 presenti.

(1) ZPRR (Xetra EUR) == R2US (Londra USD): stessa ISIN IE00BJ38QD84, stessa classe di
quote, stesso NAV. La gamba small cap e' risolta. Regola: si cerca per ISIN, non per
ticker — lo stesso fondo ha ticker diversi su borse diverse.

(2) SPY4 IE00B4YBJ215 NON e' small cap ne' S&P 500: e' SPDR S&P 400 MID cap (l'S&P 500
e' SPY5). Misurato come ripiego su 6.5 anni: Sharpe 0.86 vs 0.86, corr fra i due sleeve
0.991, peggiore in 3/7 anni = moneta. Sulla finestra corta di 3.2 anni sembrava -0.15:
era rumore. Ripiego accettabile per ragione meccanica (small e mid USA correlano ~0.95
giornaliero), NON validato — e' proprio perche' non si distinguono che la scelta non
conta.

(3) CORREZIONE a un'indicazione data ieri. Avevo scritto "una linea in EUR aggiunge la
conversione per ordine". Vero solo per un conto in USD: per un conto in EURO vale
l'opposto. L'esposizione economica e' identica (il fondo detiene attivi USD, nessuna
delle due linee e' coperta) — la valuta di quotazione non copre nulla, decide solo se
serve una conversione. Regola giusta: prendere la linea nella valuta del proprio saldo.
Misurato: turnover lordo $13.655/anno su $10k -> 25bps = -0.07 Sharpe = $34/anno = ~$1.6
per ordine, contro una soglia di $18.90.

Regola generale: un costo di conversione non e' una proprieta' dello strumento ma della
coppia strumento-CONTO.

Book, pesi, cron, config INVARIATI. 447 test verdi (+3).

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-27 11:25:20 +00:00
parent d0c804ebfc
commit 378f448450
5 changed files with 208 additions and 8 deletions
+4 -1
View File
@@ -54,7 +54,10 @@ EXCH, CUR = "LSEETF", "USD"
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)
"IWM": ("XRSU", "R2US", "IDP6", "SPY4"), # Russell 2000: XRSU $438 · R2US $86 (piu' economico E lungo)
# SPY4 = S&P 400 MID cap: indice DIVERSO, tenuto solo
# per misurare il costo di una sostituzione forzata
# (e' cio' che alcuni broker offrono al posto dello small cap)
"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
+75 -3
View File
@@ -46,6 +46,15 @@ ISIN = {"VUAA": ("IE00BFMXXD54", "Vanguard S&P 500 UCITS (Acc, USD)"),
"IGLN": ("IE00B4ND3602", "iShares Physical Gold ETC"),
"IHYU": ("IE00B4PY7Y77", "iShares $ High Yield Corp Bond UCITS")}
# Stesso fondo, ticker diverso su borse diverse. Verificato su contract details IB: se le ISIN
# coincidono e' la STESSA classe di quote, quindi lo stesso NAV — cambia solo dove si negozia e in
# che valuta. Serve perche' un broker puo' non quotare la linea di Londra e quotare quella di Xetra.
EQUIVALENZE = [("R2US", "ZPRR", "IE00BJ38QD84", "Xetra EUR")]
# Ripiego misurato: se un broker non ha NESSUNA linea Russell 2000, l'S&P 400 mid cap e' l'unica
# alternativa comune. NON e' lo stesso indice (mid, non small) — vedi sezione (6) per il costo.
RIPIEGO_SMALLCAP = ("SPY4", "IE00B4YBJ215", "SPDR S&P 400 US Mid Cap UCITS")
def ordini_anno(veic, cal, capital, band, every) -> float:
n = 0
@@ -238,9 +247,72 @@ def main() -> None:
t = U.SET_DEPLOY[s]
isin, d = ISIN[t]
print(f" {s:7s}{t:8s}{isin:16s}{float(U._norm(dep[s]).iloc[-1]):>9.2f} {d}")
print("\n Tutti su LONDRA in USD. Le stesse ISIN esistono su altre borse e in altre valute:")
print(" una linea in EUR aggiunge la conversione per ordine, che a 25bps costa ~0.15 di")
print(" Sharpe — piu' del costo per ordine di qualunque broker a questa cadenza.")
print("\n ⚠️ SI CERCA PER ISIN, NON PER TICKER — e il motivo non e' la pignoleria: lo STESSO")
print(" fondo ha ticker diversi su borse diverse. Verificato su contract details IB:")
for a, b, isin, nota in EQUIVALENZE:
print(f" {a:6s} (Londra USD) == {b:6s} ({nota:12s}) ISIN {isin}")
print(" Sono la stessa classe di quote: NAV identico, cambia solo dove e in che valuta si")
print(" negozia. Cercando per ISIN si trova qualunque linea il proprio broker offra.")
print("\n ⚠️ CORREZIONE (2026-07-27) A UN'INDICAZIONE DATA IERI. Avevo scritto che 'una linea in")
print(" EUR aggiunge la conversione per ordine'. E' vero solo per un conto in USD. Per un conto")
print(" in EURO — che e' il caso di un retail italiano su Revolut/Degiro — vale l'OPPOSTO:")
print(" comprare la linea in USD costringe a convertire a ogni ordine, la linea in EUR no.")
print(" L'esposizione economica e' identica in entrambi i casi (il fondo detiene attivi in USD")
print(" e nessuna delle due linee e' coperta): la valuta di quotazione non copre nulla, decide")
print(" solo se serve una conversione. **La regola giusta e': prendere la linea nella valuta")
print(" del proprio saldo.**")
turnover = 0.0
for k in dep:
_, pos = U.leg_returns(dep[k], dep[k], cal, CAP / 6.0, band_c, can_every,
U.cost_form(fixed=1.0))
turnover += float(pos.diff().abs().sum()) * CAP / 6.0
turnover /= (len(cal) / 252.0)
print(f"\n Quanto vale la conversione, misurato: turnover lordo ${turnover:,.0f}/anno su "
f"${CAP:,.0f}")
print(f" {'bps FX/ordine':>15s}{'Sharpe':>9s}{'dSharpe':>10s}{'costo/anno':>13s}")
b0 = None
for p in (0.0, 15.0, 25.0, 50.0):
m = U.met(U.sleeve(dep, dep, cal, CAP, band_c, can_every, U.cost_form(prop_bps=p)))
b0 = m["sharpe"] if b0 is None else b0
print(f" {format(p,'.0f')+' bps':>15s}{m['sharpe']:>9.2f}{m['sharpe']-b0:>+10.2f}"
f"{'$'+format(turnover*p/1e4,',.0f'):>13s}")
print(" Cioe' a 25bps la conversione equivale a ~$1.6 in piu' su ciascuno dei 21 ordini:")
print(" reale, ma piccolo rispetto alla soglia (${:.2f}/ordine a ${:,.0f}).".format(
float(soglia_per_ordine(dep, cal, CAP, band_c, can_every)[1:]), int(CAP)))
# ---------- (6) ripiego sulla gamba small cap ----------
print("\n" + "=" * 100)
print(" (6) SE IL BROKER NON HA NESSUNA LINEA RUSSELL 2000")
print("=" * 100)
tic, isin_r, desc_r = RIPIEGO_SMALLCAP
print(f" Ripiego comune: {tic} {isin_r}{desc_r}. NON e' lo stesso indice (MID cap, non")
print(" small): va misurato, non assunto equivalente.")
alt = dict(dep)
alt["IWM"], _ = U.close_uc(tic, us["IWM"])
# finestra lunga: veicoli STORIA sulle altre 5 gambe, cosi' il campione non e' limitato da XNAS
st_set = {s: U.close_uc(u, us[s])[0] for s, u in U.SET_STORIA.items()}
small = dict(st_set)
small["IWM"], _ = U.close_uc("R2US", us["IWM"])
mid = dict(st_set)
mid["IWM"] = alt["IWM"]
cl = U.common_cal(list(small.values()) + [mid["IWM"]])
st = max(U._norm(x).index[0] for x in list(small.values()) + [mid["IWM"]])
cl = cl[cl >= st + np.timedelta64(380, "D")]
print(f"\n finestra {cl[0].date()} -> {cl[-1].date()} ({len(cl)/252:.1f} anni)")
s1 = U.sleeve(small, small, cl, CAP, BAND_FRAC * CAP / 6.0, can_every, U.cost_form(fixed=1.0))
s2 = U.sleeve(mid, mid, cl, CAP, BAND_FRAC * CAP / 6.0, can_every, U.cost_form(fixed=1.0))
for nome, s in [("R2US small cap", s1), ("SPY4 mid cap ", s2)]:
m = U.met(s)
print(f" {nome} Sharpe {m['sharpe']:.2f} CAGR {m['cagr']:.2%} maxDD {m['dd']:.1%}")
peggio = sum(1 for y, g in s2.groupby(s2.index.year)
if len(g) > 100 and U.met(g)["sharpe"] < U.met(s1[s1.index.year == y])["sharpe"])
tot = sum(1 for y, g in s2.groupby(s2.index.year) if len(g) > 100)
print(f" correlazione dei due sleeve {s1.corr(s2):.4f} · SPY4 peggiore in {peggio}/{tot} anni")
print("\n -> RIPIEGO ACCETTABILE, e il motivo e' meccanico: small e mid cap USA hanno")
print(" correlazione giornaliera ~0.95, e un segnale TSMOM su due serie cosi' vicine si")
print(" accende quasi negli stessi giorni. ⚠️ NON e' 'validato': 6.5 anni non bastano a")
print(" distinguere due gambe cosi' simili, ed e' esattamente per questo che la scelta fra")
print(" le due non e' importante. Se c'e' una linea Russell 2000, si usa quella.")
print("\n" + "=" * 100)