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
+23 -4
View File
@@ -1113,10 +1113,29 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
(d) **Cosa cercare sul proprio conto — per ISIN, non per ticker** (da contract details IB; tutti
domiciliati IE, quotati a **Londra in USD**): **VUAA** `IE00BFMXXD54` ($143.80) · **XNAS**
`IE00BMFKG444` ($65.74) · **R2US** `IE00BJ38QD84` ($86.35) · **IDTL** `IE00BSKRJZ44` ($3.09) ·
**IGLN** `IE00B4ND3602` ($79.06) · **IHYU** `IE00B4PY7Y77` ($94.12). ⚠️ Le stesse ISIN esistono
in EUR su altre borse: la conversione per ordine a 25bps costa **~0.15 di Sharpe**, piu' del
costo per ordine di qualunque broker a questa cadenza → **la valuta della linea conta piu' del
broker**. Nessuna azione oggi (decisione venue: 100% Deribit fino a $20k).
**IGLN** `IE00B4ND3602` ($79.06) · **IHYU** `IE00B4PY7Y77` ($94.12).
Nessuna azione oggi (decisione venue: 100% Deribit fino a $20k).
⚠️ **CORREZIONE 27/07 A QUESTO PUNTO.** Avevo scritto "una linea in EUR aggiunge la conversione
per ordine → prendere quella in USD". E' vero **solo per un conto in USD**. Per un conto in EURO
(retail italiano su Revolut/Degiro) vale l'**OPPOSTO**: la linea USD costringe a convertire a ogni
ordine, quella EUR no. L'esposizione economica e' identica (il fondo detiene attivi USD e
**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, 21 ordini): 15bps 0.04 Sh/$20 a., 25bps
**0.07 Sh/$34 a.**, 50bps 0.14/$68 → a 25bps equivale a **~$1.6 in piu' per ordine**, piccolo
rispetto alla soglia di $18.90. **REGOLA: un costo di conversione non e' una proprieta' dello
strumento ma della coppia strumento-CONTO.**
(f) **Equivalenze e ripieghi, verificati su contract details IB (27/07).**
**ZPRR (Xetra EUR) == R2US (Londra USD): stessa ISIN `IE00BJ38QD84`, stesso fondo, stesso NAV**
se un broker non quota la linea di Londra, quella di Xetra e' identica (e su conto in euro,
migliore). **REGOLA: si cerca per ISIN, non per ticker** — lo stesso fondo ha ticker diversi su
borse diverse. ⚠️ **SPY4 `IE00B4YBJ215` NON e' small cap ne' S&P 500**: e' SPDR **S&P 400 MID
cap** (l'S&P 500 e' SPY5 `IE00B6YX5C33`). Misurato come ripiego della gamba small: su 6.5 anni
Sharpe **0.86 vs 0.86**, CAGR 4.26% vs 4.20%, **corr fra i due sleeve 0.991**, peggiore in 3/7
anni = moneta (sulla finestra corta di 3.2a sembrava 0.15: rumore) → **ripiego accettabile**, per
ragione meccanica (small e mid USA correlano ~0.95 giornaliero, un TSMOM si accende negli stessi
giorni). Non e' "validato": 6.5 anni non distinguono due gambe cosi' simili, ed e' esattamente
perche' la scelta non conta. Se c'e' una linea Russell 2000, si usa quella.
**LEZIONE: una raccomandazione poggiata su un fatto non verificato sul conto reale vale quanto
quel fatto** — due volte in due giorni un'assunzione sul conto ha cambiato la conclusione (prima
la negoziabilita' PRIIPs, poi quale conto e' davvero operativo).
+67
View File
@@ -458,3 +458,70 @@ punto in cui la si riaprirebbe comunque.**
**Una capacità mancante si valuta sul costo di non averla, non sulla sua assenza.** Qui l'assenza di
API sembra squalificante e vale 0.02 di Sharpe se si esegue entro un giorno; ciò che vale davvero
(0.27) è un rischio operativo che si copre con una notifica.
---
# Addendum 2026-07-27 (3) — "r2us in revolut non c'è, ma ho zprr e spy4"
## ZPRR **è** R2US
| ticker | borsa | valuta | ISIN |
|---|---|---|---|
| R2US | Londra | USD | `IE00BJ38QD84` |
| **ZPRR** | **Xetra** | **EUR** | **`IE00BJ38QD84`** |
Stessa ISIN = stessa classe di quote = **stesso fondo, stesso NAV**. Cambia solo dove e in che
valuta si negozia. La gamba small cap è risolta: **compra ZPRR**, non si perde niente.
> **Regola: si cerca per ISIN, non per ticker.** Lo stesso fondo ha ticker diversi su borse diverse,
> e un ticker copiato da una lista può essere un altro prodotto.
## ⚠️ SPY4 non è quello che sembra
`IE00B4YBJ215` = SPDR **S&P 400 US Mid Cap**. Non è small cap, e non è l'S&P 500 (quello è SPY5,
`IE00B6YX5C33`). Non serve, dato che ZPRR c'è — ma misurato come ripiego, su 6.5 anni:
| gamba small cap | Sharpe | CAGR | maxDD |
|---|---|---|---|
| R2US (small) | 0.86 | 4.20% | 8.2% |
| SPY4 (mid) | 0.86 | 4.26% | 8.3% |
Correlazione dei due sleeve **0.991**, SPY4 peggiore in **3/7 anni** = moneta. *(Sulla finestra
corta di 3.2 anni sembrava 0.15 di Sharpe: era rumore.)*
**Ripiego accettabile**, e il motivo è meccanico: small e mid cap USA hanno correlazione giornaliera
~0.95, e un TSMOM su due serie così vicine si accende quasi negli stessi giorni. ⚠️ Non è
"validato" — 6.5 anni non bastano a distinguere due gambe così simili, ed è **esattamente per
questo** che la scelta fra le due non conta.
## ⚠️ Correzione a un'indicazione data ieri: sulla valuta avevo ragionato al contrario
Ieri ho scritto che *"una linea in EUR aggiunge la conversione per ordine"*. È vero **solo per un
conto in USD**. Per un conto in **euro** — cioè un retail italiano su Revolut o Degiro — vale
l'opposto: comprare la linea in USD costringe a convertire a ogni ordine, la linea in EUR no.
L'esposizione economica è identica nei due casi: il fondo detiene attivi in USD e **nessuna delle
due linee è coperta**. La valuta di quotazione non copre niente — decide solo se serve una
conversione.
> **Regola giusta: prendere la linea nella valuta del proprio saldo.**
Quanto vale, misurato (turnover lordo **$13.655/anno** su $10.000, 21 ordini):
| bps FX per ordine | ΔSharpe | costo/anno |
|---|---|---|
| 15 bps | 0.04 | $20 |
| 25 bps | 0.07 | $34 |
| 50 bps | 0.14 | $68 |
A 25bps la conversione equivale a ~**$1.6 in più su ciascuno dei 21 ordini**: reale, ma piccolo
rispetto alla soglia di $18.90/ordine a $10.000. Quindi **ZPRR in EUR su un conto in euro è la
scelta migliore**, non un compromesso.
## Lezione
**Un costo di conversione non è una proprietà dello strumento ma della coppia strumento-conto.**
Avevo dedotto "linea in EUR = costo in più" da un caso (conto in USD) e l'avevo scritta come regola
generale; per il conto che l'operatore usa davvero il segno è invertito. È la terza volta in due
giorni che un'assunzione non verificata sul conto reale cambia la conclusione — dopo la
negoziabilità PRIIPs e quale conto è operativo.
+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)
+39
View File
@@ -118,3 +118,42 @@ def test_le_isin_sono_ben_formate_e_coprono_le_sei_gambe():
def test_le_soglie_di_lettura_sono_dichiarate():
assert B.BAND_FRAC == 0.25
assert B.ESPOSTA_SANA == 0.60
# ===========================================================================
# equivalenze fra listini — la stessa ISIN su borse diverse e' lo STESSO fondo
# ===========================================================================
def test_le_equivalenze_dichiarate_condividono_la_isin_del_veicolo_scelto():
"""ZPRR (Xetra EUR) e R2US (Londra USD) sono la stessa classe di quote: se le ISIN non
coincidono l'equivalenza e' falsa e l'operatore comprerebbe un altro fondo."""
for a, b, isin, _ in B.EQUIVALENZE:
assert a in B.ISIN, f"{a} non e' un veicolo del set DEPLOY"
assert B.ISIN[a][0] == isin, f"{a}: la ISIN dell'equivalenza non e' quella del veicolo"
assert re.fullmatch(r"IE00[A-Z0-9]{8}", isin)
assert b not in B.ISIN, f"{b} e' un ALIAS di {a}, non un veicolo distinto"
def test_il_ripiego_smallcap_e_dichiarato_come_indice_DIVERSO():
"""SPY4 e' S&P 400 MID cap: e' un ripiego misurato, non un equivalente. Il test esiste
perche' la differenza fra 'equivalente' e 'ripiego accettabile' e' proprio cio' che si
perde quando si copiano ticker da una lista."""
tic, isin, desc = B.RIPIEGO_SMALLCAP
assert re.fullmatch(r"IE00[A-Z0-9]{8}", isin)
assert isin != B.ISIN["R2US"][0], "un ripiego con la stessa ISIN sarebbe un'equivalenza"
assert "Mid" in desc
def test_il_ripiego_smallcap_e_indistinguibile_dalla_gamba_scelta(ctx):
"""Il finding della sezione (6): sostituire mid a small lascia lo sleeve al 99% uguale.
Se un giorno smettesse di essere vero, la raccomandazione 'usa SPY4 se non trovi R2US'
andrebbe ritirata."""
dep, cal = ctx
us = {s: U.close_us(s) for s in U.US}
mid = dict(dep)
mid["IWM"], _ = U.close_uc(B.RIPIEGO_SMALLCAP[0], us["IWM"])
cl = U.common_cal(list(dep.values()) + [mid["IWM"]])
cl = cl[cl >= cal[0]]
band = B.BAND_FRAC * 10_000.0 / 6.0
a = U.sleeve(dep, dep, cl, 10_000.0, band, 5, U.cost_form(fixed=1.0))
b = U.sleeve(mid, mid, cl, 10_000.0, band, 5, U.cost_form(fixed=1.0))
assert a.corr(b) > 0.95, f"il ripiego non e' piu' equivalente: corr {a.corr(b):.3f}"