Files
PythagorasGoal/tests/test_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

121 lines
5.0 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