Files
PythagorasGoal/scripts/research/r0727_gtaa_broker.py
T
Adriano Dal Pastro 842b0e865b research(gtaa): il numero di ordini e' un parametro — il costo smette di essere binding
Correzione dell'operatore: "io ho gia' Revolut e Degiro e li uso da anni, IB sono solo
iscritto". La raccomandazione di ieri (restare su IB) poggiava su "il conto esiste
gia'", che era falso. Il gateway IB serve solo per i DATI del segnale (basta il paper),
quindi il broker di esecuzione e' libero e la scelta si gioca solo sul costo per ordine.

(a) I 77-149 ordini/anno sono la conseguenza di REBAL_EVERY=5 / REBAL_BAND_USD=50,
scelti il 25/07 per il listino IB su azioni USA e a una taglia sola. A cadenza
settimanale allargare la banda NON costa: Sharpe a costo zero 1.27 ($50) -> 1.27 ($400)
con ordini 84 -> 23. Rallentare la cadenza invece costa (1.27 -> 1.12 mensile). La banda
filtra la deriva del vol-target, non il segnale di trend. Controllo fuori finestra: sui
veicoli USA su 10 anni lo Sharpe a costo zero perde 0.02 mentre gli ordini calano del
74% -> non e' un artefatto della finestra UCITS corta.

(b) MA la banda in dollari assoluti e' la parametrizzazione sbagliata: a $3.000 una
banda da $400 e' l'80% della gamba -> 3 ordini/anno, a mercato il 45% del tempo invece
del 66%, con Sharpe 0.71 ancora "accettabile". Null de-levering in veste nuova: non
travestito da meno drawdown ma da meno costi. Il controllo non e' lo Sharpe ma la quota
di tempo a mercato.

(c) Configurazione proposta: banda = 25% della gamba -> 21 ordini/anno a OGNI capitale,
esposizione 66% ovunque, soglia $5.65/ordine a $3k (contro $2.08 del canonico). Proposta,
non cambio di produzione: non passata per study_family_honest ne' deflated-Sharpe, e
GTAA01 non e' deployabile prima dei $20k.

(d) ISIN da contract details IB per la ricerca sul conto reale (VUAA/XNAS/R2US/IDTL/
IGLN/IHYU, tutti IE, Londra in USD). La valuta della linea conta piu' del broker: una
linea in EUR aggiunge ~25bps per ordine = ~0.15 di Sharpe.

Book, pesi, cron, config INVARIATI. 444 test verdi (+9).

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

194 lines
10 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) 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()