Files
PythagorasGoal/scripts/research/r0727_gtaa_broker.py
T
Adriano Dal Pastro 3f0812c7fc research(gtaa): DEGIRO ha tutte e sei le gambe — verificato sul conto reale
Screenshot della watchlist "Pythagoras" su flatexDEGIRO: le 6 gambe ci sono tutte, coi
ticker esatti raccomandati. Era Revolut a non avere R2US.

VERIFICA INDIPENDENTE che le due linee EUR (VUAA, XNAS su Tradegate) siano gli stessi
fondi, dai dati e non dallo screenshot: il rapporto prezzo-IB/prezzo-Degiro dev'essere un
solo cambio -> 1.1341 e 1.1315, scarto 0.23%. Le altre 4 stanno a 0.993-0.998 (gia' USD).
Due fondi diversi non darebbero lo stesso cambio.

ERRORE MIO, dello stesso tipo che avevo codificato come regola un messaggio prima: per
cercare le linee in euro delle altre 4 gambe ho interrogato IB per TICKER sulle borse
tedesche, ottenendo "nessuna linea EUR" su 4/4. Falso. Cercando per ISIN ognuna ce l'ha:
ZPRR (=R2US), IS04 (=IDTL), EGLN (=IGLN, Londra EUR), IS0R (=IHYU). Un "assente" da una
ricerca per ticker su una borsa dove quel ticker non esiste non significa "non esiste".

TURNOVER PER GAMBA a $10k (21 ordini/anno): IDTL 9 ordini / $6.257 = 46% del totale,
poi VUAA 17%, IGLN 12%, XNAS 12%, IHYU 7%, R2US 6%. Il 71% del turnover e' su gambe in
USD ($9.752/anno) -> conversione $24/anno a 25bps. Spostare la sola IDTL sulla linea in
euro (IS04) copre il 64% del turnover convertibile.

Ma non e' un risparmio netto (spread Xetra piu' larghi delle linee primarie di Londra,
tariffa di connettivita' per borsa) e su $10k si parla di decine di dollari l'anno in
entrambe le direzioni: non e' una decisione importante, ed e' piu' utile dirlo che
costruire una precisione finta.

Il prompt W-8BEN di Degiro non riguarda queste sei: sono fondi irlandesi, non titoli USA.

Stato: preparazione, non azione (saldo Degiro EUR 328,92; GTAA01 richiede >=$3k e la
decisione venue tiene tutto su Deribit fino a $20k).

Book, pesi, cron, config INVARIATI. 448 test verdi (+1).

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

349 lines
21 KiB
Python

"""GTAA01 su un broker a COSTO FISSO — il numero di ordini e' un PARAMETRO, non un dato.
CONTESTO. Il 26/07 la via UCITS e' risultata praticabile (drag del veicolo ~0, biglietto minimo
risolto scegliendo linee a prezzo unitario basso) e il solo vincolo residuo e' il **costo per
ordine**: 77-149 ordini l'anno con banda e cadenza canoniche, contro soglie di $0.90-$7.62.
Ma quei 77-149 ordini sono la conseguenza di `REBAL_EVERY=5` e `REBAL_BAND_USD=50`, scelti il
25/07 sul plateau **per il listino IB su azioni USA e a una taglia sola**. La domanda giusta non
e' "quale broker costa meno" ma "quanti ordini servono davvero".
⚠️ IL COMPROMESSO HA DUE LATI. Allargare la banda risparmia commissioni MA aumenta il tracking
error. Guardare solo lo Sharpe netto confonde le due cose: qui si riporta anche lo Sharpe a
**costo zero**, che isola il prezzo della banda. Se lo Sharpe a costo zero tiene, il risparmio e'
reale; se crolla, si sta solo smettendo di seguire il target.
⚠️ E LA BANDA IN DOLLARI ASSOLUTI NON SCALA. A $10.000 allocati una banda da $400 e' il 24% della
gamba (sana, 23 ordini/anno); a $3.000 e' l'**80%** e lo sleeve si congela — 3 ordini l'anno, a
mercato il 45% del tempo invece del 66%. Uno Sharpe accettabile su una strategia congelata non e'
un successo: e' il null de-levering. La banda va espressa come FRAZIONE della gamba.
uv run python scripts/research/r0727_gtaa_broker.py
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "scripts" / "research"))
import r0726_gtaa_ucits as U # noqa: E402
CADENZE = [(5, "settimanale"), (10, "bisettimanale"), (21, "mensile"), (42, "bimestrale")]
BANDE = [25.0, 50.0, 100.0, 200.0, 400.0]
BAND_FRAC = 0.25 # limite superiore della regione sana, misurato in (3)
ESPOSTA_SANA = 0.60 # sotto questa quota di tempo a mercato lo sleeve e' congelato, non frugale
# ISIN da contract details IB (fonte autorevole: i ticker cambiano da borsa a borsa, le ISIN no).
ISIN = {"VUAA": ("IE00BFMXXD54", "Vanguard S&P 500 UCITS (Acc, USD)"),
"XNAS": ("IE00BMFKG444", "Xtrackers Nasdaq 100 UCITS 1C"),
"R2US": ("IE00BJ38QD84", "SPDR Russell 2000 US Small Cap UCITS"),
"IDTL": ("IE00BSKRJZ44", "iShares $ Treasury Bond 20+yr UCITS"),
"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 cercando **per
# ISIN**: 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' (a) un broker puo' non quotare la linea di Londra,
# (b) su un conto in EURO la linea in euro evita la conversione a ogni ordine.
#
# ⚠️ Queste righe vanno cercate PER ISIN e non per ticker, ed e' un errore che ho fatto io stesso
# costruendo la tabella: la prima sonda cercava il ticker "R2US" sulle borse tedesche e non trovava
# nulla, concludendo "nessuna linea EUR". La linea c'e' e si chiama ZPRR. Cercare un ticker su una
# borsa dove quel ticker non esiste da' sempre "assente", che NON significa "non esiste".
EQUIVALENZE = [("R2US", "ZPRR", "IE00BJ38QD84", "Xetra EUR"),
("IDTL", "IS04", "IE00BSKRJZ44", "Xetra EUR"),
("IGLN", "EGLN", "IE00B4ND3602", "Londra EUR"),
("IHYU", "IS0R", "IE00B4PY7Y77", "Xetra EUR"),
("VUAA", "VUAA", "IE00BFMXXD54", "Xetra/BIt EUR"),
("XNAS", "XNAS", "IE00BMFKG444", "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
for k in veic:
_, pos = U.leg_returns(veic[k], veic[k], cal, capital / 6.0, band, every,
U.cost_form(fixed=1.0))
n += int((pos.diff().abs() > 1e-12).sum())
return n / (len(cal) / 252.0)
def soglia_per_ordine(veic, cal, capital, band, every, target=0.45) -> str:
def sh(f):
return U.met(U.sleeve(veic, veic, cal, capital, band, every,
U.cost_form(fixed=f)))["sharpe"]
lo, hi = 0.0, 60.0
if sh(hi) > target:
return ">$60"
for _ in range(10):
mid = (lo + hi) / 2
lo, hi = (mid, hi) if sh(mid) > target else (lo, mid)
return f"${(lo + hi) / 2:.2f}"
def main() -> None:
us = {s: U.close_us(s) for s in U.US}
dep = {s: U.close_uc(u, us[s])[0] for s, u in U.SET_DEPLOY.items()}
cal = U.common_cal(list(us.values()) + list(dep.values()))
start = max(U._norm(x).index[0] for x in dep.values())
cal = cal[cal >= start + np.timedelta64(380, "D")]
cal_us = U.common_cal(list(us.values()))
print("=" * 100)
print(" GTAA01 SU UN BROKER A COSTO FISSO — quanti ordini servono davvero")
print("=" * 100)
print(f" veicoli DEPLOY {list(U.SET_DEPLOY.values())}")
print(f" finestra UCITS {cal[0].date()} -> {cal[-1].date()} ({len(cal)/252:.1f} anni)")
print(" ⚠️ Finestra corta e tutta di mercato favorevole: gli Sharpe assoluti qui (1.1-1.3) NON")
print(" sono l'aspettativa dello sleeve (~0.6 sulla storia lunga). Servono a confrontare")
print(" configurazioni FRA LORO sulla stessa finestra, non a stimare il rendimento.")
CAP = 10_000.0
can_every, can_band = 5, 50.0
# ---------- (1) griglia ----------
print("\n" + "=" * 100)
print(" (1) GRIGLIA cadenza x banda — capitale allocato $10.000")
print("=" * 100)
print(f"\n {'cadenza':15s}{'banda':>8s}{'ord/anno':>10s}{'Sh @ $0':>10s}{'Sh @ $1':>10s}"
f"{'Sh @ $2':>10s}{'Sh @ $5':>10s}")
griglia = []
for every, nome in CADENZE:
for band in BANDE:
n = ordini_anno(dep, cal, CAP, band, every)
sh = [U.met(U.sleeve(dep, dep, cal, CAP, band, every,
U.cost_form(fixed=f)))["sharpe"] for f in (0.0, 1.0, 2.0, 5.0)]
griglia.append((every, nome, band, n, sh))
print(f" {nome:15s}{'$'+format(band,'.0f'):>8s}{n:>10.0f}"
+ "".join(f"{x:>10.2f}" for x in sh))
base_z = next(sh[0] for e, _, b, _, sh in griglia if (e, b) == (can_every, can_band))
print(f"\n Sharpe a COSTO ZERO del canonico (settimanale/$50): {base_z:.2f}")
print(" Allargare la banda a cadenza settimanale non lo tocca (il segnale si muove a scatti")
print(" di 0.25 di esposizione = ~$417 su una gamba da $1.667: la banda filtra la deriva del")
print(" vol-target, non il segnale di trend). Rallentare la CADENZA invece costa davvero.")
# ---------- (2) il plateau regge sulla finestra lunga? ----------
print("\n" + "=" * 100)
print(" (2) CONTROLLO — la banda larga regge fuori dalla finestra corta?")
print("=" * 100)
print(f" Stessa griglia di banda sui VEICOLI USA, finestra comune ai 6: "
f"{cal_us[0].date()} -> {cal_us[-1].date()} ({len(cal_us)/252:.0f} anni).")
print(" (e' l'intersezione delle 6 serie, limitata dalla cache TLT che parte dal 2016; lo")
print(" sleeve in produzione gira da prima con meno gambe, qui serve un campione appaiato)")
print(f"\n {'banda':>8s}{'ord/anno':>10s}{'Sh @ $0':>10s}{'Sh @ $1':>10s}{'CAGR @ $0':>12s}")
for band in BANDE:
n = ordini_anno(us, cal_us, CAP, band, can_every)
m0 = U.met(U.sleeve(us, us, cal_us, CAP, band, can_every, U.cost_form(fixed=0.0)))
m1 = U.met(U.sleeve(us, us, cal_us, CAP, band, can_every, U.cost_form(fixed=1.0)))
print(f" {'$'+format(band,'.0f'):>8s}{n:>10.0f}{m0['sharpe']:>10.2f}"
f"{m1['sharpe']:>10.2f}{m0['cagr']:>11.2%}")
print("\n ✅ Il plateau si estende a $400 anche su 10 anni e su veicoli diversi: lo Sharpe a")
print(" costo zero perde 0.02 mentre gli ordini calano del 74%. Non e' un artefatto della")
print(" finestra corta.")
# ---------- (3) la degenerazione ----------
print("\n" + "=" * 100)
print(" (3) PERCHE' LA BANDA IN DOLLARI ASSOLUTI E' SBAGLIATA")
print("=" * 100)
print(f"\n {'capitale':>9s}{'banda':>8s}{'% gamba':>10s}{'ord/anno':>10s}{'esposta':>9s}"
f"{'Sharpe':>9s} verdetto")
for cap in (3_000.0, 10_000.0, 50_000.0):
for band in (50.0, 400.0):
n = ordini_anno(dep, cal, cap, band, can_every)
r, P = U.sleeve(dep, dep, cal, cap, band, can_every, U.cost_form(fixed=1.0),
want_pos=True)
esp = float((P > 0).mean().mean())
v = "OK" if esp >= ESPOSTA_SANA else "❌ CONGELATO"
print(f" {'$'+format(int(cap),',d'):>9s}{'$'+format(band,'.0f'):>8s}"
f"{band/(cap/6):>9.0%}{n:>10.0f}{esp:>9.0%}"
f"{U.met(r)['sharpe']:>9.2f} {v}")
print("\n ⚠️ A $3.000 la banda da $400 e' l'80% della gamba: 3 ordini l'anno e a mercato il 45%")
print(" del tempo invece del 66%. Lo Sharpe resta 0.71 — accettabile — ma su una strategia")
print(" che ha smesso di seguire il proprio target. SI LEGGE 'esposta', NON 'Sharpe'.")
# ---------- (4) la configurazione capital-aware ----------
print("\n" + "=" * 100)
print(f" (4) BANDA = {BAND_FRAC:.0%} DELLA GAMBA — e il numero di ordini diventa invariante")
print("=" * 100)
print(f"\n {'capitale':>9s}{'banda':>9s}{'ord/anno':>10s}{'esposta':>9s}{'Sharpe':>8s}"
f"{'soglia $/ordine':>17s}{'= budget/anno':>15s}")
for cap in (3_000.0, 6_000.0, 10_000.0, 25_000.0, 50_000.0):
band = BAND_FRAC * cap / 6.0
n = ordini_anno(dep, cal, cap, band, can_every)
r, P = U.sleeve(dep, dep, cal, cap, band, can_every, U.cost_form(fixed=1.0), want_pos=True)
thr = soglia_per_ordine(dep, cal, cap, band, can_every)
budget = "-" if thr.startswith(">") else f"${n*float(thr[1:]):,.0f}"
print(f" {'$'+format(int(cap),',d'):>9s}{'$'+format(band,'.0f'):>9s}{n:>10.0f}"
f"{float((P > 0).mean().mean()):>9.0%}{U.met(r)['sharpe']:>8.2f}{thr:>17s}{budget:>15s}")
n_can = ordini_anno(dep, cal, CAP, can_band, can_every)
print(f"\n Contro il canonico ($50 fissi): {n_can:.0f} ordini/anno a $10.000 e "
f"{ordini_anno(dep, cal, 3_000.0, can_band, can_every):.0f} a $3.000, con soglia "
f"{soglia_per_ordine(dep, cal, 3_000.0, can_band, can_every)}/ordine a $3.000.")
print(" La banda proporzionale porta la soglia a $3.000 da ~$2 a ~$5.7 per ordine: e' la")
print(" differenza fra 'serve un broker economico' e 'va bene qualunque broker europeo'.")
print("\n ⚠️ NON e' un cambio di produzione. E' una configurazione PROPOSTA, tarata su questa")
print(" finestra e non passata per study_family_honest ne' per un deflated-Sharpe. Regge il")
print(" controllo (2) su 10 anni e veicoli diversi, ma GTAA01 non e' deployabile prima dei")
print(" $20k (decisione venue 26/07): c'e' tempo per validarla come si deve.")
# ---------- (5-bis) l'esecuzione MANUALE e' sostenibile? ----------
print("\n" + "=" * 100)
print(" (5-bis) SE IL BROKER NON HA UNA API — quanto costa eseguire a mano")
print("=" * 100)
print(" DEGIRO non espone una API di trading ufficiale; Revolut nemmeno (la sua API e' per")
print(" pagamenti). Esistono wrapper non ufficiali degli endpoint interni, ma girano su")
print(" credenziali + seed 2FA e si rompono in SILENZIO a ogni cambio di front-end — e questo")
print(" progetto ha gia' pagato una volta il prezzo di un guasto silenzioso (fresh_5m, 26/07).")
print(" Quindi la domanda giusta non e' 'si puo' automatizzare' ma 'quanto costa NON farlo'.")
per_bar = None
band_c = BAND_FRAC * CAP / 6.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))
o = (pos.diff().abs() > 1e-12).astype(int)
per_bar = o if per_bar is None else per_bar + o
anni = len(cal) / 252.0
att = int((per_bar > 0).sum())
print(f"\n Carico operativo a ${CAP:,.0f} (banda {BAND_FRAC:.0%}, controllo settimanale):")
print(f" controlli/anno {len(cal)/5/anni:.0f}")
print(f" settimane con >=1 ordine/anno {att/anni:.0f} ({att/(len(cal)/5):.0%} dei controlli)")
print(f" ordini per settimana attiva {per_bar[per_bar > 0].mean():.1f} "
f"(max {int(per_bar.max())})")
print("\n Costo del RITARDO di esecuzione — veicoli USA, finestra lunga "
f"({len(cal_us)/252:.0f} anni), perche' su 3.2 anni la curva non risulta monotona")
print(" (cioe' il campione corto non risolve differenze di questa taglia):")
print(f"\n {'ritardo':>10s}{'Sharpe':>9s}{'CAGR':>9s}{'dSharpe':>10s} stabilita' per anno")
base_sh, base_s = None, None
for d in (0, 1, 2, 3, 5, 10):
cols = {}
for k in us:
_, pos = U.leg_returns(us[k], us[k], cal_us, CAP / 6.0, band_c, can_every,
U.cost_form(fixed=1.0))
px = U._norm(us[k]).reindex(cal_us).ffill().values.astype(float)
ret = np.zeros(len(px))
ret[1:] = px[1:] / px[:-1] - 1.0
p = pos.shift(d).fillna(0.0).values
c = (pos.diff().abs() > 1e-12).astype(float).shift(d).fillna(0.0).values / (CAP / 6.0)
cols[k] = __import__("pandas").Series(p * ret - c, index=cal_us)
s = __import__("pandas").concat(cols, axis=1).mean(axis=1)
m = U.met(s)
if base_sh is None:
base_sh, base_s = m["sharpe"], s.copy()
peggio = sum(1 for y, g in s.groupby(s.index.year)
if len(g) > 100 and U.met(g)["sharpe"] < U.met(base_s[base_s.index.year == y])["sharpe"])
tot = sum(1 for y, g in s.groupby(s.index.year) if len(g) > 100)
print(f" {str(d)+' giorni':>10s}{m['sharpe']:>9.2f}{m['cagr']:>8.2%}"
f"{m['sharpe']-base_sh:>+10.2f} peggiora in {peggio}/{tot} anni")
print("\n LETTURA: eseguire il giorno stesso o il giorno dopo costa ~nulla (-0.02, e peggiora")
print(" in 6/11 anni = moneta). Il costo NON e' il ritardo tipico ma la CODA: dieci giorni")
print(" valgono -0.27, cioe' quasi un terzo dello sleeve. Il rischio dell'operativita' manuale")
print(" non e' la lentezza, e' la DIMENTICANZA — e quella si compra con un ALLARME, non con")
print(" una API: `gtaa_rebalance_plan()` esiste gia' in produzione ed e' nato per un esecutore.")
# ---------- (5) cosa cercare sul proprio conto ----------
print("\n" + "=" * 100)
print(" (5) COSA CERCARE SUL PROPRIO BROKER — per ISIN, non per ticker")
print("=" * 100)
print(f"\n {'gamba':7s}{'ticker':8s}{'ISIN':16s}{'prezzo':>9s} descrizione")
for s in U.US:
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 ⚠️ 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)))
print("\n ⚠️ E LA CONVERSIONE NON SI DISTRIBUISCE IN MODO UNIFORME: il turnover e' concentrato")
print(" in poche gambe, quindi conviene sapere QUALI conviene spostare su una linea in euro.")
print(f"\n {'gamba':7s}{'ticker':8s}{'ordini/anno':>13s}{'turnover/anno':>16s}{'quota':>8s}")
tv = {}
for s in U.US:
_, pos = U.leg_returns(dep[s], dep[s], cal, CAP / 6.0, band_c, can_every,
U.cost_form(fixed=1.0))
tv[s] = (int((pos.diff().abs() > 1e-12).sum()) / (len(cal) / 252.0),
float(pos.diff().abs().sum()) * CAP / 6.0 / (len(cal) / 252.0))
tot = sum(v for _, v in tv.values())
for s, (n, v) in sorted(tv.items(), key=lambda kv: -kv[1][1]):
print(f" {s:7s}{U.SET_DEPLOY[s]:8s}{n:>13.0f}{'$'+format(v,',.0f'):>16s}{v/tot:>8.0%}")
top = max(tv.items(), key=lambda kv: kv[1][1])
print(f"\n -> {U.SET_DEPLOY[top[0]]} da sola vale il {top[1][1]/tot:.0%} del turnover e "
f"{top[1][0]:.0f} dei {sum(n for n, _ in tv.values()):.0f} ordini: se si vuole ridurre la")
print(" conversione senza cambiare sei strumenti, si sposta QUELLA su una linea in euro.")
# ---------- (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)
if __name__ == "__main__":
main()