842b0e865b
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
194 lines
10 KiB
Python
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()
|