Files
PythagorasGoal/scripts/research/r0727_gtaa_broker.py
T
Adriano Dal Pastro d0c804ebfc research(gtaa): l'assenza di API DEGIRO vale -0.02 di Sharpe, non e' squalificante
Domanda dell'operatore: "degiro ha api?". Fatto: no, non ufficiale; Revolut nemmeno
(la sua e' per pagamenti); IB si'. I wrapper non ufficiali girano su credenziali +
seed 2FA e si rompono IN SILENZIO a ogni cambio di front-end — e il progetto ha gia'
pagato quel prezzo con fresh_5m il 26/07. Un esecutore che puo' rompersi senza dirlo
e' peggio dell'esecuzione manuale.

Misurato il costo di NON automatizzare invece di discuterlo:
  carico operativo: 15 settimane/anno con >=1 ordine (29% dei controlli, 1.4 per volta)
  ritardo 1g  -0.02 Sharpe (peggiora in 6/11 anni = moneta)
  ritardo 3g  -0.13
  ritardo 10g -0.27

Il costo non e' il ritardo tipico ma la coda: il rischio dell'operativita' manuale e'
la dimenticanza, e si copre con un allarme, non con una API (gtaa_rebalance_plan esiste
gia' in produzione ed e' nato per un esecutore).

Nota di metodo: sulla finestra UCITS di 3.2 anni la curva del ritardo NON e' monotona
(5g -0.26, 10g -0.13) -> il campione corto non risolve differenze di questa taglia,
quindi il numero si legge sulla finestra lunga a 10 anni, dove lo e'.

Book, pesi, cron, config INVARIATI.

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

249 lines
14 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")}
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()