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