Files
Adriano Dal Pastro 3f0812c7fc research(gtaa): DEGIRO ha tutte e sei le gambe — verificato sul conto reale
Screenshot della watchlist "Pythagoras" su flatexDEGIRO: le 6 gambe ci sono tutte, coi
ticker esatti raccomandati. Era Revolut a non avere R2US.

VERIFICA INDIPENDENTE che le due linee EUR (VUAA, XNAS su Tradegate) siano gli stessi
fondi, dai dati e non dallo screenshot: il rapporto prezzo-IB/prezzo-Degiro dev'essere un
solo cambio -> 1.1341 e 1.1315, scarto 0.23%. Le altre 4 stanno a 0.993-0.998 (gia' USD).
Due fondi diversi non darebbero lo stesso cambio.

ERRORE MIO, dello stesso tipo che avevo codificato come regola un messaggio prima: per
cercare le linee in euro delle altre 4 gambe ho interrogato IB per TICKER sulle borse
tedesche, ottenendo "nessuna linea EUR" su 4/4. Falso. Cercando per ISIN ognuna ce l'ha:
ZPRR (=R2US), IS04 (=IDTL), EGLN (=IGLN, Londra EUR), IS0R (=IHYU). Un "assente" da una
ricerca per ticker su una borsa dove quel ticker non esiste non significa "non esiste".

TURNOVER PER GAMBA a $10k (21 ordini/anno): IDTL 9 ordini / $6.257 = 46% del totale,
poi VUAA 17%, IGLN 12%, XNAS 12%, IHYU 7%, R2US 6%. Il 71% del turnover e' su gambe in
USD ($9.752/anno) -> conversione $24/anno a 25bps. Spostare la sola IDTL sulla linea in
euro (IS04) copre il 64% del turnover convertibile.

Ma non e' un risparmio netto (spread Xetra piu' larghi delle linee primarie di Londra,
tariffa di connettivita' per borsa) e su $10k si parla di decine di dollari l'anno in
entrambe le direzioni: non e' una decisione importante, ed e' piu' utile dirlo che
costruire una precisione finta.

Il prompt W-8BEN di Degiro non riguarda queste sei: sono fondi irlandesi, non titoli USA.

Stato: preparazione, non azione (saldo Degiro EUR 328,92; GTAA01 richiede >=$3k e la
decisione venue tiene tutto su Deribit fino a $20k).

Book, pesi, cron, config INVARIATI. 448 test verdi (+1).

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

171 lines
7.7 KiB
Python

"""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
# ===========================================================================
# equivalenze fra listini — la stessa ISIN su borse diverse e' lo STESSO fondo
# ===========================================================================
def test_le_equivalenze_dichiarate_condividono_la_isin_del_veicolo_scelto():
"""ZPRR (Xetra EUR) e R2US (Londra USD) sono la stessa classe di quote: se le ISIN non
coincidono l'equivalenza e' falsa e l'operatore comprerebbe un altro fondo."""
assert {a for a, _, _, _ in B.EQUIVALENZE} == set(B.ISIN), \
"ogni gamba deve avere una linea in euro dichiarata: su un conto in EUR e' cio' che evita la conversione"
for a, b, isin, _ in B.EQUIVALENZE:
assert a in B.ISIN, f"{a} non e' un veicolo del set DEPLOY"
assert B.ISIN[a][0] == isin, f"{a}: la ISIN dell'equivalenza non e' quella del veicolo"
assert re.fullmatch(r"IE00[A-Z0-9]{8}", isin)
if b != a:
assert b not in B.ISIN, f"{b} e' un ALIAS di {a}, non un veicolo distinto"
def test_ogni_isin_compare_una_volta_sola_fra_le_equivalenze():
"""Due gambe con la stessa ISIN sarebbero lo stesso fondo comprato due volte: il vol-target
girerebbe su cinque esposizioni invece di sei senza che nulla lo segnali."""
isins = [i for _, _, i, _ in B.EQUIVALENZE]
assert len(set(isins)) == len(isins)
assert len(set(B.ISIN[t][0] for t in B.ISIN)) == 6
def test_il_ripiego_smallcap_e_dichiarato_come_indice_DIVERSO():
"""SPY4 e' S&P 400 MID cap: e' un ripiego misurato, non un equivalente. Il test esiste
perche' la differenza fra 'equivalente' e 'ripiego accettabile' e' proprio cio' che si
perde quando si copiano ticker da una lista."""
tic, isin, desc = B.RIPIEGO_SMALLCAP
assert re.fullmatch(r"IE00[A-Z0-9]{8}", isin)
assert isin != B.ISIN["R2US"][0], "un ripiego con la stessa ISIN sarebbe un'equivalenza"
assert "Mid" in desc
def test_il_ripiego_smallcap_e_indistinguibile_dalla_gamba_scelta(ctx):
"""Il finding della sezione (6): sostituire mid a small lascia lo sleeve al 99% uguale.
Se un giorno smettesse di essere vero, la raccomandazione 'usa SPY4 se non trovi R2US'
andrebbe ritirata."""
dep, cal = ctx
us = {s: U.close_us(s) for s in U.US}
mid = dict(dep)
mid["IWM"], _ = U.close_uc(B.RIPIEGO_SMALLCAP[0], us["IWM"])
cl = U.common_cal(list(dep.values()) + [mid["IWM"]])
cl = cl[cl >= cal[0]]
band = B.BAND_FRAC * 10_000.0 / 6.0
a = U.sleeve(dep, dep, cl, 10_000.0, band, 5, U.cost_form(fixed=1.0))
b = U.sleeve(mid, mid, cl, 10_000.0, band, 5, U.cost_form(fixed=1.0))
assert a.corr(b) > 0.95, f"il ripiego non e' piu' equivalente: corr {a.corr(b):.3f}"