From 842b0e865b4e94070309494c535dc70931dfaca5 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Mon, 27 Jul 2026 06:01:01 +0000 Subject: [PATCH] =?UTF-8?q?research(gtaa):=20il=20numero=20di=20ordini=20e?= =?UTF-8?q?'=20un=20parametro=20=E2=80=94=20il=20costo=20smette=20di=20ess?= =?UTF-8?q?ere=20binding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_01XBbYmiXqbUNuGpK9sfbsGp --- CLAUDE.md | 50 +++++-- docs/diary/2026-07-26-gtaa-ucits.md | 118 ++++++++++++++++ scripts/research/r0727_gtaa_broker.py | 193 ++++++++++++++++++++++++++ tests/test_gtaa_broker.py | 120 ++++++++++++++++ 4 files changed, 473 insertions(+), 8 deletions(-) create mode 100644 scripts/research/r0727_gtaa_broker.py create mode 100644 tests/test_gtaa_broker.py diff --git a/CLAUDE.md b/CLAUDE.md index c2370c4..1002832 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1078,14 +1078,48 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis ⚠️ Letto sulla colonna **gambe vive + vol**, non su Sharpe: il vincolo intero *alza* lo Sharpe a capitale piccolo perche' arrotonda in giu' e paga meno commissioni = **null de-levering, 4ª occorrenza** (VRP-DD, TP01×DVOL, MAT01). - (3) **Cosa resta da verificare: UNA cosa sola.** Lo sleeve fa **77 ordini/anno a $3k, 102 a $10k, - 149 a $50k**; soglia oltre cui non vale la pena (Sharpe<0.45) = **$0.90/ordine a $3k, $2.23 a - $10k, $7.62 a $50k**. Listino **proporzionale** ≈ indifferente al capitale (Sh 0.75-0.89 a ogni - taglia), listino **fisso** = tassa regressiva (regola 25/07 riconfermata su altro venue). - **Raccomandazione: restare su IB** — conto gia' aperto, stesso gateway dei dati del segnale, - forma di costo gia' modellata. ⚠️ **Revolut/Degiro NON sbloccano gli ETF USA**: il PRIIPs vale - per ogni intermediario UE, IB e' anzi fra i piu' permissivi; un terzo conto aggiungerebbe rischio - di venue senza guadagno. Nessuna azione oggi (decisione venue: 100% Deribit fino a $20k). + (3) **Il costo per ordine.** Col canonico lo sleeve fa **77 ordini/anno a $3k, 102 a $10k, 149 a + $50k**; soglia oltre cui non vale la pena (Sharpe<0.45) = **$0.90/ordine a $3k, $2.23 a $10k, + $7.62 a $50k**. Listino **proporzionale** ≈ indifferente al capitale, listino **fisso** = tassa + regressiva (regola 25/07 riconfermata su altro venue). ⚠️ **Revolut/Degiro NON sbloccano gli ETF + USA**: il PRIIPs vale per ogni intermediario UE, IB e' anzi fra i piu' permissivi. +- ✅ **ADDENDUM 2026-07-27 — il numero di ordini e' un PARAMETRO, e con la banda giusta il costo + smette di essere binding su qualunque broker UE.** Nato dalla correzione dell'operatore *"io ho + gia' Revolut e Degiro e li uso da anni, IB sono solo iscritto"*: la raccomandazione del 26/07 + ("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. + Script `r0727_gtaa_broker.py`, test `tests/test_gtaa_broker.py` (9). **Book/pesi/cron/config + INVARIATI.** + (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) mentre gli ordini vanno + 84 → 23. Rallentare la CADENZA invece costa (1.27 → 1.12 mensile). Meccanismo: `_exposure` e' la + media di 4 indicatori binari, si muove a scatti di 0.25 ≈ $417 su una gamba da $1.667 → **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 passa 0.98 → **0.96** mentre gli ordini calano + del **74%** (133 → 35) — non e' un artefatto della finestra corta UCITS (3.2a). + (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%. Lo Sharpe resta 0.71 — accettabile — **su una strategia che ha smesso di seguire il proprio + target**. E' il **null de-levering in veste nuova**: non travestito da "meno drawdown" ma da + "meno costi". **REGOLA: quando un parametro di esecuzione e' espresso in valuta assoluta il suo + effetto dipende dal capitale, e 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** + (invariante), esposizione 66% ovunque; soglia per ordine **$5.65 a $3k / $18.90 a $10k / $47 a + $25k** contro i **$2.08 a $3k** del canonico. E' la differenza fra "serve un broker economico" e + "va bene qualunque broker europeo". ⚠️ **Proposta, non cambio di produzione**: tarata su questa + finestra, non passata per `study_family_honest` ne' per un deflated-Sharpe — e GTAA01 non e' + deployabile prima dei $20k, quindi c'e' tempo per validarla. + (d) **Cosa cercare sul proprio conto — per ISIN, non per ticker** (da contract details IB; tutti + domiciliati IE, quotati a **Londra in USD**): **VUAA** `IE00BFMXXD54` ($143.80) · **XNAS** + `IE00BMFKG444` ($65.74) · **R2US** `IE00BJ38QD84` ($86.35) · **IDTL** `IE00BSKRJZ44` ($3.09) · + **IGLN** `IE00B4ND3602` ($79.06) · **IHYU** `IE00B4PY7Y77` ($94.12). ⚠️ Le stesse ISIN esistono + in EUR su altre borse: la conversione per ordine a 25bps costa **~0.15 di Sharpe**, piu' del + costo per ordine di qualunque broker a questa cadenza → **la valuta della linea conta piu' del + broker**. Nessuna azione oggi (decisione venue: 100% Deribit fino a $20k). + **LEZIONE: una raccomandazione poggiata su un fatto non verificato sul conto reale vale quanto + quel fatto** — due volte in due giorni un'assunzione sul conto ha cambiato la conclusione (prima + la negoziabilita' PRIIPs, poi quale conto e' davvero operativo). - ⚠️ **IL FEED EQUITY NON AVEVA UN CROSS-CHECK — buco trovato e chiuso (2026-07-26).** `src/data/eq_crosscheck.py`. Nel crypto la certificazione incrocia sempre piu' venue (`certify_feed.py` vs Coinbase USD); il feed equity aveva **solo controlli locali** (integrita', diff --git a/docs/diary/2026-07-26-gtaa-ucits.md b/docs/diary/2026-07-26-gtaa-ucits.md index a7f3d0c..c4132f0 100644 --- a/docs/diary/2026-07-26-gtaa-ucits.md +++ b/docs/diary/2026-07-26-gtaa-ucits.md @@ -276,3 +276,121 @@ contro $2.23 (a $10.000) o $0.90 (a $3.000). 6. **Il grado d'accordo si conta, non si dichiara.** La prima stesura stampava "stesso segno su 6/6 gambe"; i dati dicevano 4/6. Scriverlo a mano nel `print` è il modo più facile di pubblicare una conclusione che i propri dati smentiscono — 3ª occorrenza in questa sessione. + +--- + +# Addendum 2026-07-27 — "io ho già Revolut e Degiro, IB sono solo iscritto" + +L'informazione ribalta la raccomandazione di ieri. L'argomento principale per IB era **"il conto +esiste già"**, e non è vero: i conti reali sono Revolut e Degiro, IB è una registrazione. + +Cosa **non** cambia: il gateway IB serve per i **dati** del segnale (ADJUSTED_LAST sui 6 ETF USA), +e per quello basta il paper — non serve eseguire lì. La scelta del broker di **esecuzione** è +quindi libera, e si gioca su una sola dimensione misurabile: il **costo per ordine**. + +Script: `scripts/research/r0727_gtaa_broker.py`. Test: `tests/test_gtaa_broker.py` (9). +**Book, pesi, cron, config: INVARIATI.** + +## Il numero di ordini è un parametro, non un dato + +I 77-149 ordini/anno del 26/07 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**. + +| cadenza | banda | ord/anno | Sh @ $0 | Sh @ $1 | Sh @ $2 | +|---|---|---|---|---|---| +| settimanale | $25 | 102 | 1.28 | 1.08 | 0.87 | +| settimanale | **$50** (canonico) | 84 | 1.27 | 1.10 | 0.94 | +| settimanale | $400 | **23** | **1.27** | 1.22 | 1.18 | +| mensile | $50 | 33 | **1.12** | 1.06 | 1.00 | +| bimestrale | $400 | 10 | 1.10 | 1.08 | 1.06 | + +**Si legge `Sh @ $0` per prima**: è il prezzo della banda *in assenza di commissioni*. Allargare la +banda a cadenza settimanale **non lo tocca** (1.27 → 1.27), rallentare la cadenza sì (1.27 → 1.12). + +Il meccanismo è interpretabile: `_exposure` è la media di 4 indicatori binari, quindi si muove a +scatti di 0.25 ≈ $417 su una gamba da $1.667. **La banda filtra la deriva del vol-target, non il +segnale di trend.** + +### Controllo su finestra lunga e veicoli diversi + +Sui **veicoli USA**, finestra comune ai 6 (10 anni, limitata dalla cache TLT dal 2016): + +| banda | ord/anno | Sh @ $0 | Sh @ $1 | +|---|---|---|---| +| $25 | 133 | 0.98 | 0.72 | +| $400 | **35** | **0.96** | **0.89** | + +✅ Lo Sharpe a costo zero perde **0.02** mentre gli ordini calano del **74%**. Non è un artefatto +della finestra corta. + +## ⚠️ Ma la banda in dollari assoluti è la parametrizzazione sbagliata + +| capitale | banda | % della gamba | ord/anno | **esposta** | Sharpe | | +|---|---|---|---|---|---|---| +| $3.000 | $400 | **80%** | **3** | **45%** | 0.71 | ❌ congelato | +| $10.000 | $400 | 24% | 23 | 66% | 1.22 | OK | +| $50.000 | $400 | 5% | 75 | 66% | 1.25 | OK | + +A $3.000 la banda da $400 è l'80% della gamba: **3 ordini l'anno**, a mercato il **45%** del tempo +invece del 66%. Lo Sharpe resta 0.71 — accettabile — **su una strategia che ha smesso di seguire il +proprio target**. È il null de-levering in una veste nuova: qui non travestito da "meno drawdown" +ma da "meno costi". + +> **Regola: quando un parametro di esecuzione è espresso in valuta assoluta, il suo effetto dipende +> dal capitale — e il controllo non è lo Sharpe ma la QUOTA DI TEMPO A MERCATO.** + +## La configurazione: banda = 25% della gamba + +| capitale | banda | ord/anno | esposta | soglia $/ordine | budget/anno | +|---|---|---|---|---|---| +| $3.000 | $125 | **21** | 66% | **$5.65** | $120 | +| $6.000 | $250 | **21** | 66% | $11.34 | $240 | +| $10.000 | $417 | **21** | 66% | $18.90 | $400 | +| $25.000 | $1.042 | **21** | 66% | $47.26 | $1.000 | +| $50.000 | $2.083 | **21** | 66% | >$60 | — | + +**21 ordini l'anno a ogni capitale**, esposizione 66% ovunque. Contro il canonico: a $3.000 la +soglia passa da **$2.08 a $5.65 per ordine** — cioè dalla differenza fra *"serve un broker +economico"* e *"va bene qualunque broker europeo"*. + +⚠️ **Non è un cambio di produzione.** È una configurazione **proposta**, tarata su questa finestra, +non passata per `study_family_honest` né per un deflated-Sharpe. Regge il controllo su 10 anni e +veicoli diversi, ma GTAA01 non è deployabile prima dei $20k (decisione venue 26/07): c'è tempo per +validarla come si deve. + +## Cosa cercare sul proprio conto — per ISIN, non per ticker + +| gamba | ticker | ISIN | prezzo | descrizione | +|---|---|---|---|---| +| SPY | VUAA | `IE00BFMXXD54` | $143.80 | Vanguard S&P 500 UCITS (Acc, USD) | +| QQQ | XNAS | `IE00BMFKG444` | $65.74 | Xtrackers Nasdaq 100 UCITS 1C | +| IWM | R2US | `IE00BJ38QD84` | $86.35 | SPDR Russell 2000 US Small Cap UCITS | +| TLT | IDTL | `IE00BSKRJZ44` | $3.09 | iShares $ Treasury Bond 20+yr UCITS | +| GLD | IGLN | `IE00B4ND3602` | $79.06 | iShares Physical Gold ETC | +| HYG | IHYU | `IE00B4PY7Y77` | $94.12 | iShares $ High Yield Corp Bond UCITS | + +*(ISIN da contract details IB: i ticker cambiano da borsa a borsa, le ISIN no. Tutti domiciliati in +Irlanda — è il motivo per cui sono acquistabili nell'UE.)* + +Tutti quotati a **Londra in USD**. ⚠️ Le stesse ISIN esistono su altre borse e in altre valute: una +linea in **EUR** aggiunge la conversione per ordine, che a 25bps costa **~0.15 di Sharpe** — più del +costo per ordine di qualunque broker a questa cadenza. **La valuta della linea conta più del +broker.** + +## Verdetto + +Con 21 ordini l'anno e una tolleranza di $5.65/ordine già a $3.000, **il vincolo di costo smette di +essere binding su qualunque broker retail europeo**. La domanda "Revolut o Degiro" perde quindi la +sua parte quantitativa e resta solo la parte di disponibilità: quale dei due quota le sei ISIN sopra +sulla linea in USD. + +Restano due criteri non misurabili da qui, entrambi a favore di Degiro su Revolut per questo uso +(universo ETF europeo tipicamente più ampio, e accesso diretto a LSE), **ma vanno verificati +guardando i due conti** — non sono cose che posso misurare da questo lato. + +## Lezione + +**Una raccomandazione poggiata su un fatto non verificato sul conto reale vale quanto quel fatto.** +Ieri ho consigliato IB in parte perché "il conto esiste già": era un'assunzione, e per la seconda +volta in due giorni un'assunzione sul conto reale ha cambiato la conclusione. La prima volta era la +negoziabilità (PRIIPs), questa volta quale conto è davvero operativo. diff --git a/scripts/research/r0727_gtaa_broker.py b/scripts/research/r0727_gtaa_broker.py new file mode 100644 index 0000000..60e5e17 --- /dev/null +++ b/scripts/research/r0727_gtaa_broker.py @@ -0,0 +1,193 @@ +"""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() diff --git a/tests/test_gtaa_broker.py b/tests/test_gtaa_broker.py new file mode 100644 index 0000000..29f71e2 --- /dev/null +++ b/tests/test_gtaa_broker.py @@ -0,0 +1,120 @@ +"""Test della configurazione di esecuzione di GTAA01 su un broker a costo fisso +(scripts/research/r0727_gtaa_broker.py). + +Il finding da bloccare e' UNO: una banda in dollari ASSOLUTI non scala col capitale, e a capitale +piccolo congela lo sleeve invece di renderlo frugale. Lo Sharpe non lo dice — resta accettabile — +lo dice la QUOTA DI TEMPO A MERCATO. E' la stessa firma del null de-levering che il progetto ha +gia' incontrato quattro volte, in una veste nuova. +""" +from __future__ import annotations + +import re +import sys +from pathlib import Path + +import numpy as np +import pytest + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) +sys.path.insert(0, str(ROOT / "scripts" / "research")) + +import r0726_gtaa_ucits as U # noqa: E402 +import r0727_gtaa_broker as B # noqa: E402 + +RAW = ROOT / "data" / "raw" + + +@pytest.fixture(scope="module") +def ctx(): + for s in list(U.US) + list(U.SET_DEPLOY.values()): + pre = "eq" if s in U.US else "eqx" + if not (RAW / f"{pre}_{s.lower()}_1d.parquet").exists(): + pytest.skip("data/raw incompleto (gitignored)") + 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()) + return dep, cal[cal >= start + np.timedelta64(380, "D")] + + +def _esposta(dep, cal, cap, band, every=5) -> float: + _, P = U.sleeve(dep, dep, cal, cap, band, every, U.cost_form(fixed=1.0), want_pos=True) + return float((P > 0).mean().mean()) + + +# =========================================================================== +# il finding +# =========================================================================== +def test_banda_assoluta_larga_congela_lo_sleeve_a_capitale_piccolo(ctx): + """IL test. A $3.000 una banda da $400 e' l'80% della gamba: lo sleeve smette di seguire il + target. Lo Sharpe resta accettabile, quindi il controllo NON puo' essere sullo Sharpe.""" + dep, cal = ctx + assert _esposta(dep, cal, 3_000.0, 400.0) < B.ESPOSTA_SANA + assert B.ordini_anno(dep, cal, 3_000.0, 400.0, 5) < 10 + + +def test_la_stessa_banda_a_capitale_grande_e_sana(ctx): + """Il confronto che rende il test precedente informativo: non e' la banda a essere sbagliata, + e' il fatto che sia espressa in dollari assoluti.""" + dep, cal = ctx + assert _esposta(dep, cal, 10_000.0, 400.0) >= B.ESPOSTA_SANA + + +def test_la_banda_proporzionale_resta_sana_a_ogni_capitale(ctx): + dep, cal = ctx + for cap in (3_000.0, 10_000.0, 50_000.0): + assert _esposta(dep, cal, cap, B.BAND_FRAC * cap / 6.0) >= B.ESPOSTA_SANA, \ + f"congelato a ${cap:,.0f}" + + +def test_la_banda_proporzionale_rende_gli_ordini_invarianti_al_capitale(ctx): + """E' la proprieta' che serve: il costo annuo scala col capitale invece che restare fisso.""" + dep, cal = ctx + n = [B.ordini_anno(dep, cal, c, B.BAND_FRAC * c / 6.0, 5) for c in (3_000., 10_000., 50_000.)] + assert max(n) - min(n) <= 2, f"ordini/anno non invarianti: {n}" + + +def test_una_banda_piu_larga_non_puo_generare_piu_ordini(ctx): + dep, cal = ctx + n = [B.ordini_anno(dep, cal, 10_000.0, b, 5) for b in (25.0, 100.0, 400.0)] + assert n == sorted(n, reverse=True), f"monotonia violata: {n}" + + +# =========================================================================== +# la soglia per il broker +# =========================================================================== +def test_la_soglia_per_ordine_cresce_col_capitale(ctx): + """Con banda proporzionale gli ordini sono costanti ma il capitale cresce: la tariffa + tollerabile deve crescere in proporzione.""" + dep, cal = ctx + v = [] + for cap in (3_000.0, 10_000.0): + s = B.soglia_per_ordine(dep, cal, cap, B.BAND_FRAC * cap / 6.0, 5) + v.append(60.0 if s.startswith(">") else float(s[1:])) + assert v[1] > 2 * v[0], f"la soglia non scala col capitale: {v}" + + +def test_la_banda_proporzionale_alza_la_soglia_rispetto_al_canonico(ctx): + """La ragione operativa di tutto il filone: a $3.000 si passa da ~$2 a ~$5 per ordine.""" + dep, cal = ctx + can = B.soglia_per_ordine(dep, cal, 3_000.0, 50.0, 5) + prop = B.soglia_per_ordine(dep, cal, 3_000.0, B.BAND_FRAC * 3_000.0 / 6.0, 5) + assert float(prop[1:]) > 2 * float(can[1:]) + + +# =========================================================================== +# cio' che l'operatore copia e incolla +# =========================================================================== +def test_le_isin_sono_ben_formate_e_coprono_le_sei_gambe(): + """Le ISIN finiscono in un campo di ricerca su un conto reale: un refuso qui compra un'altra + cosa. Formato ISO 6166 e domicilio irlandese (e' il motivo per cui sono acquistabili nell'UE).""" + assert set(B.ISIN) == set(U.SET_DEPLOY.values()) + for t, (isin, desc) in B.ISIN.items(): + assert re.fullmatch(r"IE00[A-Z0-9]{8}", isin), f"{t}: ISIN malformata {isin}" + assert desc.strip() + + +def test_le_soglie_di_lettura_sono_dichiarate(): + assert B.BAND_FRAC == 0.25 + assert B.ESPOSTA_SANA == 0.60