"""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")} 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 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" + "=" * 100) if __name__ == "__main__": main()