be4690375f
Domanda dell'operatore: "usiamo revolut o degiro". Risposta misurata: cambiare broker non sblocca nulla (il PRIIPs e' una norma, non una politica di IB); cambiare VEICOLO si', e costa ~zero. CORREZIONE A UNA MIA AFFERMAZIONE. La nota in gtaa.py diceva che gli UCITS fanno perdere la validazione a 30 anni. Falso: il PRIIPs vieta di COMPRARE, non di GUARDARE — i prezzi dei 6 ETF USA restano leggibili, quindi il segnale gira sui 30 anni per sempre e cambia solo il veicolo su cui si incassa. Misure (3 lenti, un grado di liberta' per volta, 6.3 anni comuni): L0 segnale USA + rend. USA Sh 0.81 / CAGR 3.96% L2 segnale UCITS + rend. UCITS Sh 0.84 / CAGR 4.08% drag del veicolo +0.10%/anno EW, coerente coi TER; ritenuta USA ~35bps a FAVORE dell'UCITS e non inclusa nel drag. Lo stimatore ovvio sbagliava: la media delle differenze giornaliere dava -0.47%/anno su CSPX contro -0.06% vero (SE ~7%/anno = 15x la quantita' stimata, piu' drag di varianza). La deviazione fra veicoli sullo stesso indice si misura sul RAPPORTO CUMULATO. Il vincolo non e' il broker ma il prezzo di UNA azione, che e' una scelta: CSPX $802 vs VUAA $144 sullo stesso S&P 500. A $3.000 con azioni intere l'insieme STORIA tiene 4/6 gambe (a mercato il 33%), l'insieme DEPLOY 6/6 (65%) -> il frazionamento non serve. Letto su gambe-vive+vol, non su Sharpe: il vincolo intero ALZA lo Sharpe perche' de-leveraggia (null de-levering, 4a occorrenza). Resta da verificare una cosa sola: 77-149 ordini/anno contro soglie $0.90 ($3k) / $2.23 ($10k) / $7.62 ($50k) per ordine. Raccomandazione: restare su IB. Feed equity: aggiunto il CROSS-CHECK che mancava (src/data/eq_crosscheck.py). Il primo veicolo estero ha trovato subito CSPX 2012-01-13 con open/high in USD e low/close in EUR (fattore 1.2797 = EURUSD del giorno), invisibile alla guardia maxret>50% — stesso schema dello split 2:1 del 25/07. Soglia non tarabile sulla deviazione (rumore 9.90%, margine 2.2x): cambiata statistica in |dev|/movimento del gemello -> margine 5.3x. Limite EURUSD 1.09 dichiarato e chiuso sul DANNO (dSharpe mediano -0.003), congelato in un test. Book, pesi, cron, config INVARIATI. 435 test verdi (+24). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XBbYmiXqbUNuGpK9sfbsGp
144 lines
6.8 KiB
Python
144 lines
6.8 KiB
Python
"""FETCH + CERTIFY equivalenti UCITS dei 6 ETF di GTAA01 -> data/raw/eqx_<sym>_1d.parquet.
|
|
|
|
PERCHE' ESISTE. Il 2026-07-26 il conto reale ha RIFIUTATO l'ordine sui 6 ETF USA di GTAA01
|
|
(blocco PRIIPs: ETF domiciliati USA, nessun KID, vietati al retail UE). Lo sleeve, cosi' com'e',
|
|
non e' deployabile. L'unica via d'uscita e' negoziare gli equivalenti UCITS — e per sapere se
|
|
quella via regge servono i DATI dei veicoli, non le loro schede prodotto.
|
|
|
|
SCELTE DICHIARATE
|
|
* Tutti i candidati sono presi su **LSEETF in USD**. Non e' una comodita': e' cio' che elimina
|
|
un livello di costo (conversione valutaria per ordine) che su un CAGR del 3.65% non sarebbe
|
|
trascurabile. Le linee Xetra in EUR (SXR8/SXRV/ZPRR) NON risolvono su questo gateway e
|
|
comunque introdurrebbero l'FX.
|
|
* Namespace **eqx_** separato da eq_: i veicoli USA restano su disco perche' sono la base della
|
|
VALIDAZIONE (30 anni), che nessun UCITS puo' fornire. Il progetto valida sull'indice/veicolo
|
|
lungo e negozia il veicolo corto; sovrascrivere eq_ cancellerebbe la meta' lunga.
|
|
* Piu' candidati per gamba dove l'equivalenza NON e' ovvia (small cap: RTWO e' un fattore
|
|
*quality*, CSUSS e' *ESG* -> non sono IWM). La scelta si fa dopo, sui dati.
|
|
|
|
CERTIFICAZIONE: riusa `certify()` di fetch_ib_equities.py — stesse regole, split non aggiustati
|
|
inclusi (la lezione IWM/EFA del 25/07 vale identica su questi veicoli).
|
|
|
|
uv run --with ib_async python scripts/research/fetch_ib_ucits.py
|
|
uv run --with ib_async python scripts/research/fetch_ib_ucits.py --force
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import pandas as pd
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(ROOT))
|
|
sys.path.insert(0, str(ROOT / "scripts" / "research"))
|
|
|
|
from fetch_ib_equities import certify # noqa: E402
|
|
|
|
RAW = ROOT / "data" / "raw"
|
|
RAW.mkdir(parents=True, exist_ok=True)
|
|
|
|
EXCH, CUR = "LSEETF", "USD"
|
|
|
|
# gamba USA -> candidati UCITS, in ordine di preferenza a priori (equivalenza dell'INDICE prima
|
|
# della storia: un fattore 'quality'/'ESG' non e' la stessa esposizione anche se ha piu' barre).
|
|
#
|
|
# ⚠️ SI SCARICANO DUE INSIEMI, e la ragione e' il BIGLIETTO MINIMO. Il prezzo di UNA azione e' una
|
|
# scelta dell'emittente, non una proprieta' dell'indice: CSPX costa $802 e VUAA $144 sullo STESSO
|
|
# S&P 500. Senza frazionamento un ordine e' almeno una azione, quindi a capitale piccolo il prezzo
|
|
# unitario decide se la gamba e' rappresentabile. Ma i veicoli economici sono anche i piu' RECENTI
|
|
# (XNAS: 4.3 anni contro i 7.5 di EQQQ) -> non si puo' scegliere una cosa sola:
|
|
# * insieme STORIA -> finestra comune piu' lunga, serve a MISURARE la deviazione del veicolo;
|
|
# * insieme DEPLOY -> prezzo unitario basso, serve a sapere se si puo' ESEGUIRE.
|
|
CANDIDATI: dict[str, tuple[str, ...]] = {
|
|
"SPY": ("CSPX", "VUAA"), # S&P 500: CSPX $802 (storia) · VUAA $144 (deploy)
|
|
"QQQ": ("CNDX", "EQQQ", "XNAS"), # Nasdaq-100: EQQQ $693 (storia) · XNAS $66 (deploy)
|
|
"IWM": ("XRSU", "R2US", "IDP6"), # Russell 2000: XRSU $438 · R2US $86 (piu' economico E piu' lungo)
|
|
"TLT": ("IDTL", "DTLA"), # US Treasury 20+ — indice identico, IDTL $3.09
|
|
"GLD": ("IGLN",), # oro fisico (ETC, non UCITS: ha comunque il KID)
|
|
"HYG": ("IHYU",), # USD High Yield corp — indice equivalente
|
|
}
|
|
UNIVERSE = [s for v in CANDIDATI.values() for s in v]
|
|
|
|
|
|
def main() -> int:
|
|
try:
|
|
from ib_async import IB, Stock
|
|
except Exception:
|
|
print("ib_async assente. Esegui con: uv run --with ib_async python scripts/research/fetch_ib_ucits.py")
|
|
return 2
|
|
|
|
ib = IB()
|
|
try:
|
|
ib.connect("127.0.0.1", 4002, clientId=91, timeout=20)
|
|
except Exception as e:
|
|
print(f"[CONNESSIONE FALLITA] 127.0.0.1:4002 -> {repr(e)[:120]}\n Avvia: docker compose up -d ib-gateway")
|
|
return 1
|
|
|
|
print("=" * 108)
|
|
print(f" FETCH + CERTIFY UCITS ({EXCH}/{CUR}, ADJUSTED_LAST) -> data/raw/eqx_* | acct {ib.managedAccounts()}")
|
|
print("=" * 108)
|
|
force = "--force" in sys.argv[1:]
|
|
ok, rep = [], []
|
|
for sym in UNIVERSE:
|
|
out_path = RAW / f"eqx_{sym.lower()}_1d.parquet"
|
|
if out_path.exists() and not force:
|
|
print(f" {sym:6} GIA' SU DISCO -> skip (--force per riscaricare)")
|
|
ok.append(sym)
|
|
continue
|
|
con = Stock(sym, EXCH, CUR)
|
|
try:
|
|
det = ib.reqContractDetails(con)
|
|
except Exception as e:
|
|
det = None
|
|
print(f" {sym:6} ERR contratto {type(e).__name__} {str(e)[:50]}")
|
|
if not det:
|
|
print(f" {sym:6} CONTRATTO ASSENTE su {EXCH}/{CUR}")
|
|
rep.append({"sym": sym, "status": "ASSENTE"})
|
|
time.sleep(1.2)
|
|
continue
|
|
try:
|
|
bars = ib.reqHistoricalData(det[0].contract, endDateTime="", durationStr="30 Y",
|
|
barSizeSetting="1 day", whatToShow="ADJUSTED_LAST",
|
|
useRTH=True, formatDate=1, timeout=120)
|
|
except Exception as e:
|
|
print(f" {sym:6} ERR storico {repr(e)[:70]}")
|
|
rep.append({"sym": sym, "status": "ERR"})
|
|
time.sleep(1.2)
|
|
continue
|
|
if not bars:
|
|
print(f" {sym:6} 0 barre (market data non sottoscritto?)")
|
|
rep.append({"sym": sym, "n": 0, "status": "VUOTO"})
|
|
time.sleep(1.2)
|
|
continue
|
|
|
|
df = pd.DataFrame([(pd.Timestamp(str(b.date)), b.open, b.high, b.low, b.close, b.volume)
|
|
for b in bars],
|
|
columns=["ts", "open", "high", "low", "close", "volume"]).set_index("ts").sort_index()
|
|
c = certify(sym, df)
|
|
rep.append(c)
|
|
out = df.copy()
|
|
out["timestamp"] = out.index.astype("datetime64[ms]").astype("int64")
|
|
out.reset_index(drop=True).to_parquet(out_path)
|
|
if c["status"] == "OK":
|
|
ok.append(sym)
|
|
print(f" {sym:6} n={c.get('n',0):>5} {str(c.get('primo','')):>10}->{str(c.get('ultimo',''))} "
|
|
f"{c.get('anni','?')}y flat={c.get('flat','?')} maxret={c.get('maxret%','?')}% "
|
|
f"miss_bd={c.get('miss_bd','?')} gapL={c.get('gap_lunghi','?')} "
|
|
f"adj={c.get('adj_first/last','?')} [{c['status']}]"
|
|
+ (f" split={c['splits']}" if c.get("splits") else ""))
|
|
time.sleep(1.2)
|
|
|
|
print("-" * 108)
|
|
print(f" CERTIFICATI OK ({len(ok)}/{len(UNIVERSE)}): {ok}")
|
|
# Il vincolo che decide tutto: la finestra COMUNE alle 6 gambe scelte.
|
|
print("\n ⚠️ La profondita' per gamba conta meno della FINESTRA COMUNE: il candidato piu' corto")
|
|
print(" determina il campione su cui si potra' misurare qualunque cosa.")
|
|
ib.disconnect()
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|