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
450 lines
23 KiB
Python
450 lines
23 KiB
Python
"""GTAA01 sulla versione REALMENTE ACQUISTABILE (UCITS) — quanto dello sleeve resta.
|
|
|
|
CONTESTO. Il 2026-07-26 il conto reale ha rifiutato l'ordine sui 6 ETF USA di GTAA01 (PRIIPs:
|
|
domiciliati USA, nessun KID, vietati al retail UE). Lo sleeve non e' deployabile cosi' com'e'.
|
|
Questo script misura la sola via d'uscita: negoziare gli equivalenti UCITS.
|
|
|
|
⚠️ IL FATTO CHE RIDIMENSIONA IL PROBLEMA, E CHE LA NOTA IN gtaa.py NON AVEVA VISTO.
|
|
La nota diceva: "storia piu' CORTA -> si perde la validazione a 30 anni che e' cio' che rendeva
|
|
credibile questo sleeve". E' FALSO, per un motivo strutturale: **il PRIIPs vieta di COMPRARE, non
|
|
di GUARDARE**. I prezzi di SPY/QQQ/IWM/TLT/GLD/HYG restano leggibili (l'operatore li ha visti sul
|
|
terminale mentre l'ordine veniva rifiutato). Il SEGNALE di GTAA01 puo' continuare a girare sui 30
|
|
anni di serie USA per sempre; cambia solo il VEICOLO su cui si incassa il rendimento. La storia
|
|
corta degli UCITS non serve a validare la strategia — serve a misurare la DEVIAZIONE DEL VEICOLO,
|
|
che e' un drift lento e si stima bene anche su pochi anni.
|
|
|
|
METODO — tre lenti, UN GRADO DI LIBERTA' PER VOLTA (come per il path live di SKH01, 26/07):
|
|
L0 segnale USA + rendimento USA = GTAA01 come pubblicato (non deployabile)
|
|
L1 segnale USA + rendimento UCITS = isola il VEICOLO
|
|
L2 segnale UCITS + rendimento UCITS = aggiunge l'OROLOGIO (LSE chiude alle 11:30 di New York)
|
|
Le tre lenti girano sullo STESSO calendario e sulla STESSA finestra: le differenze sono appaiate.
|
|
|
|
⚠️ DUE INSIEMI DI VEICOLI, 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. Ma i veicoli economici sono i piu' recenti. Quindi:
|
|
STORIA (CSPX/EQQQ/XRSU/IDTL/IGLN/IHYU) -> finestra lunga, serve a MISURARE;
|
|
DEPLOY (VUAA/XNAS/R2US/IDTL/IGLN/IHYU) -> prezzo unitario basso, serve a ESEGUIRE.
|
|
|
|
uv run python scripts/research/r0726_gtaa_ucits.py
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from src.data.eq_crosscheck import repair_twin_outliers # noqa: E402
|
|
from src.data.eq_splits import repair_splits # noqa: E402
|
|
from src.portfolio import gtaa as G # noqa: E402
|
|
|
|
RAW = ROOT / "data" / "raw"
|
|
ANN = np.sqrt(252.0)
|
|
US = ("SPY", "QQQ", "IWM", "TLT", "GLD", "HYG")
|
|
|
|
# Scartati di proposito: RTWO (Russell 2000 *quality* = fattore), CSUSS (MSCI USA Small Cap *ESG*),
|
|
# IDP6 (S&P 600: small cap ma indice diverso da Russell 2000), CNDX (market data non sottoscritto).
|
|
SET_STORIA = {"SPY": "CSPX", "QQQ": "EQQQ", "IWM": "XRSU", "TLT": "IDTL", "GLD": "IGLN", "HYG": "IHYU"}
|
|
SET_DEPLOY = {"SPY": "VUAA", "QQQ": "XNAS", "IWM": "R2US", "TLT": "IDTL", "GLD": "IGLN", "HYG": "IHYU"}
|
|
|
|
# TER indicativi (%/anno). ⚠️ FONTE SECONDARIA, non letta dai KID: servono solo come CONTROLLO di
|
|
# plausibilita' del drag misurato, non entrano in nessun calcolo. Regola 26/07: dichiarare la
|
|
# qualita' della fonte.
|
|
TER = {"SPY": 0.09, "QQQ": 0.20, "IWM": 0.19, "TLT": 0.15, "GLD": 0.40, "HYG": 0.49,
|
|
"CSPX": 0.07, "EQQQ": 0.30, "XRSU": 0.30, "IDTL": 0.07, "IGLN": 0.12, "IHYU": 0.50,
|
|
"VUAA": 0.07, "XNAS": 0.20, "R2US": 0.30}
|
|
# Ritenuta USA sui dividendi (15% da trattato) x rendimento da dividendo indicativo -> punti base
|
|
# che un residente italiano paga comprando il veicolo USA e NON paga comprando l'UCITS irlandese.
|
|
RITENUTA_BPS = {"SPY": 18, "QQQ": 7, "IWM": 18, "TLT": 60, "GLD": 0, "HYG": 90}
|
|
|
|
|
|
# ===========================================================================
|
|
# dato
|
|
# ===========================================================================
|
|
def _norm(s: pd.Series) -> pd.Series:
|
|
s = s[~s.index.duplicated()].copy()
|
|
idx = pd.to_datetime(s.index)
|
|
if getattr(idx, "tz", None) is not None:
|
|
idx = idx.tz_localize(None)
|
|
s.index = idx.normalize()
|
|
return s.sort_index()
|
|
|
|
|
|
def _load(prefix: str, sym: str) -> pd.DataFrame:
|
|
d = pd.read_parquet(RAW / f"{prefix}_{sym.lower()}_1d.parquet")
|
|
d.index = pd.to_datetime(d["timestamp"], unit="ms", utc=True)
|
|
d, _ = repair_splits(d.sort_index())
|
|
return d
|
|
|
|
|
|
def close_us(sym: str) -> pd.Series:
|
|
d = _load("eq", sym)
|
|
return pd.Series(d["close"].astype(float).values, index=d.index)
|
|
|
|
|
|
def close_uc(sym: str, twin: pd.Series) -> tuple[pd.Series, list[dict]]:
|
|
"""Chiusure UCITS RIPARATE col cross-check sul gemello USA (src/data/eq_crosscheck.py)."""
|
|
d, bad = repair_twin_outliers(_load("eqx", sym), twin)
|
|
return pd.Series(d["close"].astype(float).values, index=d.index), bad
|
|
|
|
|
|
# ===========================================================================
|
|
# motore — generalizzazione di gtaa._gated_returns con segnale e veicolo SEPARATI
|
|
# ===========================================================================
|
|
def leg_returns(sig_close: pd.Series, exe_close: pd.Series, cal, cap_leg: float,
|
|
band_usd: float, every: int, cost, *, integer_shares: bool = False):
|
|
"""Rendimenti netti di UNA gamba sul calendario `cal`, piu' la posizione tenuta.
|
|
|
|
`sig_close` decide l'esposizione, `exe_close` la incassa: separarli isola il veicolo
|
|
dall'orologio. Identico a gtaa._gated_returns quando le due coincidono e cal e' l'indice
|
|
nativo. `integer_shares`: l'ordine si arrotonda a un numero INTERO di azioni.
|
|
"""
|
|
sig = _norm(sig_close).reindex(cal).ffill()
|
|
exe = _norm(exe_close).reindex(cal).ffill()
|
|
ex = np.nan_to_num(np.asarray(G._exposure(sig).values, float))
|
|
px = exe.values.astype(float)
|
|
ret = np.zeros(len(px))
|
|
ret[1:] = px[1:] / px[:-1] - 1.0
|
|
|
|
held = np.empty(len(ex))
|
|
comm = np.zeros(len(ex))
|
|
cur = 0.0
|
|
for i in range(len(ex)):
|
|
if i % every == 0:
|
|
tgt = ex[i]
|
|
if integer_shares:
|
|
tgt = np.floor(tgt * cap_leg / px[i]) * px[i] / cap_leg
|
|
notional = abs(tgt - cur) * cap_leg
|
|
if notional >= max(band_usd, G.IB_MIN_TRADE_USD):
|
|
comm[i] = cost(notional, px[i]) / cap_leg
|
|
cur = tgt
|
|
held[i] = cur
|
|
pos = np.zeros(len(held))
|
|
pos[1:] = held[:-1]
|
|
net = pos * ret - comm
|
|
net[0] = 0.0
|
|
return pd.Series(net, index=cal), pd.Series(pos, index=cal)
|
|
|
|
|
|
def sleeve(sig: dict, exe: dict, cal, capital: float, band: float, every: int, cost,
|
|
integer_shares: bool = False, want_pos: bool = False):
|
|
cap_leg = float(capital) / len(sig)
|
|
out = {k: leg_returns(sig[k], exe[k], cal, cap_leg, band, every, cost,
|
|
integer_shares=integer_shares) for k in sig}
|
|
r = pd.concat({k: v[0] for k, v in out.items()}, axis=1).mean(axis=1, skipna=True)
|
|
if want_pos:
|
|
return r, pd.concat({k: v[1] for k, v in out.items()}, axis=1)
|
|
return r
|
|
|
|
|
|
def sleeve_native(sig: dict, exe: dict, capital: float, band: float, every: int, cost) -> pd.Series:
|
|
"""Replica ESATTA di gtaa.gtaa_returns: ogni gamba sul PROPRIO indice, outer-join, media.
|
|
Serve solo al sanity check — le lenti girano su calendario comune, che e' cio' che rende
|
|
appaiate le differenze."""
|
|
cap_leg = float(capital) / len(sig)
|
|
cols = {}
|
|
for k in sig:
|
|
cal = _norm(exe[k]).index
|
|
cols[k] = leg_returns(sig[k], exe[k], cal, cap_leg, band, every, cost)[0]
|
|
return pd.concat(cols, axis=1, sort=True).sort_index().mean(axis=1, skipna=True).dropna()
|
|
|
|
|
|
def met(r: pd.Series) -> dict:
|
|
r = r.dropna()
|
|
if len(r) < 50 or r.std() == 0:
|
|
return dict(sharpe=np.nan, cagr=np.nan, dd=np.nan, vol=np.nan)
|
|
eq = (1 + r).cumprod()
|
|
return dict(sharpe=float(r.mean() / r.std() * ANN),
|
|
cagr=float(eq.iloc[-1] ** (252.0 / len(r)) - 1),
|
|
dd=float((eq / eq.cummax() - 1).min()), vol=float(r.std() * ANN))
|
|
|
|
|
|
def cost_ib_us(notional, price):
|
|
return G.ib_commission(notional, price)
|
|
|
|
|
|
def cost_form(fixed=0.0, prop_bps=0.0, min_fee=0.0, max_frac=1.0):
|
|
def f(notional, price):
|
|
return min(max(fixed + notional * prop_bps / 1e4, min_fee), max_frac * notional)
|
|
return f
|
|
|
|
|
|
def common_cal(series_list) -> pd.DatetimeIndex:
|
|
cal = None
|
|
for s in series_list:
|
|
i = _norm(s).index
|
|
cal = i if cal is None else cal.intersection(i)
|
|
return cal
|
|
|
|
|
|
def drag_cumulato(a: pd.Series, b: pd.Series, cal) -> float:
|
|
"""Deviazione annualizzata del veicolo b rispetto al gemello a, misurata sul RAPPORTO.
|
|
|
|
⚠️ NON si usa la media delle differenze giornaliere. Le due serie seguono lo STESSO indice ma
|
|
sono campionate a orari diversi (LSE chiude 4h30 prima di New York): la differenza giornaliera
|
|
e' dominata da uno sfasamento che si INVERTE il giorno dopo, quindi ha una volatilita' enorme
|
|
(17-22% annualizzata) e un errore standard di ~7%/anno sulla stima del drag — cioe' 15 volte
|
|
piu' grande del drag stesso. Il rapporto b/a invece e' stazionario: il suo drift E' il drag.
|
|
"""
|
|
x, y = _norm(a).reindex(cal).ffill(), _norm(b).reindex(cal).ffill()
|
|
r = (y / x).dropna()
|
|
return float((r.iloc[-1] / r.iloc[0]) ** (252.0 / len(r)) - 1)
|
|
|
|
|
|
# ===========================================================================
|
|
def main() -> None:
|
|
print("=" * 108)
|
|
print(" GTAA01 SUGLI EQUIVALENTI UCITS — quanto dello sleeve resta quando lo si puo' comprare")
|
|
print("=" * 108)
|
|
|
|
us = {s: close_us(s) for s in US}
|
|
uc, difetti = {}, {}
|
|
for s, u in SET_STORIA.items():
|
|
uc[s], bad = close_uc(u, us[s])
|
|
if bad:
|
|
difetti[u] = bad
|
|
dep = {}
|
|
for s, u in SET_DEPLOY.items():
|
|
dep[s], bad = close_uc(u, us[s])
|
|
if bad:
|
|
difetti[u] = bad
|
|
|
|
# ---------- (0) DATO ----------
|
|
print("\n" + "=" * 108)
|
|
print(" (0) IL DATO — cross-check di ogni veicolo contro il suo gemello USA")
|
|
print("=" * 108)
|
|
print(" Il feed equity aveva SOLO controlli locali (integrita', gap, spike, split): nessun")
|
|
print(" secondo parere, mentre nel crypto la certificazione incrocia sempre piu' venue.")
|
|
print(f"\n {'gamba':7s}{'STORIA':9s}{'barre':>7s}{'dal':>12s} {'DEPLOY':9s}{'barre':>7s}{'dal':>12s}")
|
|
for s in US:
|
|
a, b = SET_STORIA[s], SET_DEPLOY[s]
|
|
print(f" {s:7s}{a:9s}{len(uc[s]):>7d}{str(_norm(uc[s]).index[0].date()):>12s} "
|
|
f"{b:9s}{len(dep[s]):>7d}{str(_norm(dep[s]).index[0].date()):>12s}")
|
|
print("\n BARRE SCARTATE DAL CROSS-CHECK:")
|
|
if not difetti:
|
|
print(" nessuna")
|
|
for u, bad in difetti.items():
|
|
for b in bad:
|
|
print(f" {u} {pd.Timestamp(b['date']).date()}: deviazione dal gemello {b['dev']:+.1%} "
|
|
f"mentre il gemello faceva {b['twin_move']:+.2%} "
|
|
f"-> non spiegato x{b['unexplained']:.0f}")
|
|
print("\n ⚠️ CSPX 2012-01-13: open/high 112.740 in USD, low/close 88.010 in EUR (fattore dai")
|
|
print(" close adiacenti 1.2797 = EURUSD di quel giorno). La certificazione ESISTENTE non lo")
|
|
print(" vedeva: la guardia era 'maxret > 50% -> SPIKE?' e una contaminazione EUR/USD vale")
|
|
print(" ~22-28%. Stesso schema dello split 2:1 del 25/07 (che valeva ESATTAMENTE -50%).")
|
|
|
|
# ---------- SANITY ----------
|
|
print("\n" + "=" * 108)
|
|
print(" SANITY — il motore generalizzato riproduce gtaa.gtaa_returns?")
|
|
print("=" * 108)
|
|
CAP = G.GTAA_DEFAULT_CAPITAL
|
|
mine = sleeve_native(us, us, CAP, G.REBAL_BAND_USD, G.REBAL_EVERY, cost_ib_us)
|
|
prod = _norm(G.gtaa_returns(capital=CAP))
|
|
both = pd.concat([mine, prod], axis=1).dropna()
|
|
dmax = float((both.iloc[:, 0] - both.iloc[:, 1]).abs().max())
|
|
print(f" max|differenza| su {len(both)} barre = {dmax:.3e} "
|
|
f"{'OK (bit-exact)' if dmax < 1e-12 else '❌ IL MOTORE NON E LA PRODUZIONE'}")
|
|
|
|
# ---------- (1) DEVIAZIONE DEL VEICOLO ----------
|
|
cal = common_cal(list(us.values()) + list(uc.values()))
|
|
start_uc = max(_norm(x).index[0] for x in uc.values())
|
|
cal = cal[cal >= start_uc + pd.Timedelta(days=380)]
|
|
print("\n" + "=" * 108)
|
|
print(" (1) LA DEVIAZIONE DEL VEICOLO — misurata sul RAPPORTO, non sulle differenze")
|
|
print("=" * 108)
|
|
print(f" finestra: {cal[0].date()} -> {cal[-1].date()} ({len(cal)} barre, {len(cal)/252:.1f} anni)")
|
|
print(" (+1 anno sul veicolo piu' corto: in L2 il segnale nasce dagli UCITS e HORIZONS arriva")
|
|
print(" a 252 barre — senza il margine le tre lenti non sarebbero appaiate)")
|
|
print(f"\n {'gamba':7s}{'UCITS':7s}{'drag misurato':>15s}{'atteso da TER':>15s}"
|
|
f"{'+ ritenuta USA':>16s}{'stabilita per anno':>20s}")
|
|
tot_mis, tot_att = [], []
|
|
for s in US:
|
|
u = SET_STORIA[s]
|
|
d = drag_cumulato(us[s], uc[s], cal)
|
|
att = (TER[s] - TER[u]) / 100.0
|
|
anni = []
|
|
for y in sorted(set(cal.year))[1:-1]:
|
|
c = cal[cal.year == y]
|
|
if len(c) > 100:
|
|
anni.append(drag_cumulato(us[s], uc[s], c))
|
|
tot_mis.append(d)
|
|
tot_att.append(att)
|
|
print(f" {s:7s}{u:7s}{d:>14.2%}{att:>14.2%}{att + RITENUTA_BPS[s]/1e4:>15.2%}"
|
|
f" [{min(anni):+.2%},{max(anni):+.2%}]" if anni else "")
|
|
print(f"\n MEDIA EQUAL-WEIGHT: misurato {np.mean(tot_mis):+.2%}/anno · "
|
|
f"atteso da TER {np.mean(tot_att):+.2%} · "
|
|
f"con ritenuta {np.mean(tot_att) + np.mean(list(RITENUTA_BPS.values()))/1e4:+.2%}")
|
|
# Il grado d'accordo si CONTA, non si dichiara: scriverlo a mano nel print e' il modo piu'
|
|
# facile di pubblicare una conclusione che i propri dati smentiscono.
|
|
conc = sum(1 for a, b in zip(tot_mis, tot_att) if np.sign(a) == np.sign(b))
|
|
vicini = sum(1 for a, b in zip(tot_mis, tot_att) if abs(a - b) < 0.003)
|
|
print(f"\n CONTROLLO DI PLAUSIBILITA': segno concorde in {conc}/6 gambe, "
|
|
f"scarto < 30bps in {vicini}/6.")
|
|
print(" Il caso piu' netto e' l'oro: GLD 0.40% vs IGLN 0.12% -> atteso +0.28%, misurato")
|
|
print(" +0.36%. Le due gambe discordi (CSPX, IHYU) restano entro 30bps: su una banda annua")
|
|
print(" larga [-0.9%,+1.9%] il segno di uno scarto cosi' piccolo non e' informativo.")
|
|
print(" Cio' che il controllo stabilisce e' la SCALA — decine di bps, non punti.")
|
|
print("\n ⚠️ La colonna 'ritenuta' e' A FAVORE dell'UCITS e NON e' nel drag misurato:")
|
|
print(" ADJUSTED_LAST sulle serie USA e' al LORDO della ritenuta del 15% che un residente")
|
|
print(" italiano paga davvero sui dividendi USA. (Ipotesi fiscale dichiarata, fonte")
|
|
print(" secondaria, da confermare col commercialista — non e' un parere fiscale.)")
|
|
|
|
# ---------- (2) TRE LENTI ----------
|
|
print("\n" + "=" * 108)
|
|
print(" (2) TRE LENTI, UN GRADO DI LIBERTA' PER VOLTA")
|
|
print("=" * 108)
|
|
L0 = sleeve(us, us, cal, CAP, G.REBAL_BAND_USD, G.REBAL_EVERY, cost_ib_us)
|
|
L1 = sleeve(us, uc, cal, CAP, G.REBAL_BAND_USD, G.REBAL_EVERY, cost_ib_us)
|
|
L2 = sleeve(uc, uc, cal, CAP, G.REBAL_BAND_USD, G.REBAL_EVERY, cost_ib_us)
|
|
print(f"\n {'lente':48s}{'Sharpe':>9s}{'CAGR':>9s}{'maxDD':>9s}{'vol':>8s}")
|
|
for nome, r in [("L0 segnale USA + rend. USA (pubblicato)", L0),
|
|
("L1 segnale USA + rend. UCITS (veicolo)", L1),
|
|
("L2 segnale UCITS + rend. UCITS (+orologio)", L2)]:
|
|
m = met(r)
|
|
print(f" {nome:48s}{m['sharpe']:>9.2f}{m['cagr']:>8.2%}{m['dd']:>9.1%}{m['vol']:>8.1%}")
|
|
m0, m2 = met(L0), met(L2)
|
|
print(f"\n TOTALE (L2-L0): dSharpe {m2['sharpe']-m0['sharpe']:+.2f} · "
|
|
f"dCAGR {m2['cagr']-m0['cagr']:+.2%}/anno · dDD {m2['dd']-m0['dd']:+.1%}")
|
|
print(f"\n ⚠️ corr(L0,L2) = {L0.corr(L2):.2f}, bassa per due serie sullo STESSO indice — ed e'")
|
|
print(" l'orologio, non un difetto: la chiusura di Londra e quella di New York condividono")
|
|
print(" la notte e la mattina americana ma non il pomeriggio. Verificato: la correlazione")
|
|
print(" fra veicolo e gemello e' MASSIMA a lag 0 su 6/6 coppie (0.46-0.78), quindi le date")
|
|
print(" non sono sfasate. Conseguenza: le statistiche GIORNALIERE delle due lenti non sono")
|
|
print(" confrontabili una a una, mentre il DRIFT (sezione 1) lo e'.")
|
|
|
|
# ---------- (3) COSTI PER FORMA ----------
|
|
print("\n" + "=" * 108)
|
|
print(" (3) IL COSTO DEL VENUE — per FORMA, non per livello")
|
|
print("=" * 108)
|
|
print(" Regola 25/07: due venue a pari 'costo medio' danno esiti OPPOSTI al variare del")
|
|
print(" capitale, perche' un pavimento FISSO e' una tassa regressiva. Qui non si indovina il")
|
|
print(" listino di un broker: si misura la curva e si ricava la soglia da verificare.")
|
|
caps = [3_000, 10_000, 50_000]
|
|
|
|
# Il numero che rende ogni listino calcolabile a mano, senza doverne indovinare nessuno.
|
|
print("\n ORDINI L'ANNO (dipendono da banda e cadenza, NON dal listino ne' dal capitale):")
|
|
for c in [3_000, 10_000, 50_000]:
|
|
n = 0
|
|
for k in uc:
|
|
_, pos = leg_returns(uc[k], uc[k], cal, c / 6.0, G.REBAL_BAND_USD, G.REBAL_EVERY,
|
|
cost_form(fixed=1.0))
|
|
n += int((pos.diff().abs() > 1e-12).sum())
|
|
print(f" a ${c:,d} di capitale allocato: {n/(len(cal)/252):.0f} ordini/anno "
|
|
f"-> costo annuo = ordini x tariffa (es. $1/ordine = ${n/(len(cal)/252):.0f}/anno "
|
|
f"= {n/(len(cal)/252)/c:.2%} del capitale)")
|
|
print(f"\n COSTO FISSO per ordine: " + "".join(f"{'$'+format(c,',d'):>22s}" for c in caps))
|
|
print(f" {'':>29s}" + "".join(f"{'Sharpe':>11s}{'CAGR':>11s}" for _ in caps))
|
|
for f in [0.0, 0.35, 1.0, 2.0, 3.0, 5.0, 10.0]:
|
|
row = f" {'$'+format(f,'.2f'):>29s}"
|
|
for c in caps:
|
|
m = met(sleeve(uc, uc, cal, c, G.REBAL_BAND_USD, G.REBAL_EVERY, cost_form(fixed=f)))
|
|
row += f"{m['sharpe']:>11.2f}{m['cagr']:>10.2%}"
|
|
print(row)
|
|
print(f"\n COSTO PROPORZIONALE: " + "".join(f"{'$'+format(c,',d'):>22s}" for c in caps))
|
|
print(f" {'':>29s}" + "".join(f"{'Sharpe':>11s}{'CAGR':>11s}" for _ in caps))
|
|
for p in [0.0, 5.0, 10.0, 25.0, 50.0]:
|
|
row = f" {format(p,'.0f')+' bps/ordine':>29s}"
|
|
for c in caps:
|
|
m = met(sleeve(uc, uc, cal, c, G.REBAL_BAND_USD, G.REBAL_EVERY, cost_form(prop_bps=p)))
|
|
row += f"{m['sharpe']:>11.2f}{m['cagr']:>10.2%}"
|
|
print(row)
|
|
|
|
print("\n SOGLIE — costo fisso per ordine oltre il quale lo sleeve non vale la pena:")
|
|
print(f" {'capitale':>10s}{'CAGR = 0':>13s}{'Sharpe = 0.45':>16s} (0.45 = base del plateau dichiarato)")
|
|
for c in caps:
|
|
def cagr_at(f):
|
|
return met(sleeve(uc, uc, cal, c, G.REBAL_BAND_USD, G.REBAL_EVERY, cost_form(fixed=f)))
|
|
res = []
|
|
for target, key in [(0.0, "cagr"), (0.45, "sharpe")]:
|
|
lo, hi = 0.0, 40.0
|
|
if cagr_at(hi)[key] > target:
|
|
res.append(">$40")
|
|
continue
|
|
for _ in range(9):
|
|
mid = (lo + hi) / 2
|
|
if cagr_at(mid)[key] > target:
|
|
lo = mid
|
|
else:
|
|
hi = mid
|
|
res.append(f"${(lo+hi)/2:.2f}")
|
|
print(f" {'$'+format(c,',d'):>10s}{res[0]:>13s}{res[1]:>16s}")
|
|
|
|
# ---------- (4) BIGLIETTO MINIMO ----------
|
|
print("\n" + "=" * 108)
|
|
print(" (4) IL BIGLIETTO MINIMO — l'analogo azionario dell'haircut small-cap")
|
|
print("=" * 108)
|
|
print(" Senza frazionamento un ordine e' almeno UNA azione. Se una azione vale quanto l'intera")
|
|
print(" gamba, il vol-target non e' rappresentabile e lo sleeve non e' quello che il backtest")
|
|
print(" modella. Ma il prezzo unitario e' una SCELTA: sullo stesso indice esistono linee 5-10x")
|
|
print(" piu' economiche.")
|
|
print(f"\n {'gamba':7s}{'STORIA':8s}{'prezzo':>9s}{'DEPLOY':>9s}{'prezzo':>9s}"
|
|
+ "".join(f"{'a $'+format(c,',d'):>13s}" for c in caps))
|
|
for s in US:
|
|
pa = float(_norm(uc[s]).iloc[-1])
|
|
pb = float(_norm(dep[s]).iloc[-1])
|
|
row = f" {s:7s}{SET_STORIA[s]:8s}{pa:>9.2f}{SET_DEPLOY[s]:>9s}{pb:>9.2f}"
|
|
for c in caps:
|
|
row += f"{pb/(c/6.0):>12.0%} "
|
|
print(row)
|
|
print(" (percentuale = quanto pesa UNA azione DEPLOY sull'allocazione di quella gamba;")
|
|
print(" sopra ~20% il target continuo non e' rappresentabile)")
|
|
|
|
print("\n DANNO MISURATO dell'esecuzione a numero INTERO di azioni — i DUE insiemi a confronto,")
|
|
print(" sulla STESSA finestra (quella del veicolo piu' corto), per isolare il prezzo unitario:")
|
|
cal_d = common_cal(list(us.values()) + list(dep.values()) + list(uc.values()))
|
|
start_d = max(_norm(x).index[0] for x in list(dep.values()) + list(uc.values()))
|
|
cal_d = cal_d[cal_d >= start_d + pd.Timedelta(days=380)]
|
|
print(f" finestra {cal_d[0].date()} -> {cal_d[-1].date()} ({len(cal_d)/252:.1f} anni, "
|
|
f"vincolata da {SET_DEPLOY['QQQ']})")
|
|
print(f"\n {'insieme':9s}{'capitale':>10s}{'Sh fraz.':>10s}{'Sh intero':>11s}{'delta':>8s}"
|
|
f"{'vol fraz.':>11s}{'vol intero':>12s}{'gambe vive':>12s}{'esposta':>10s}")
|
|
for nome, vv in [("STORIA", uc), ("DEPLOY", dep)]:
|
|
for c in [3_000, 10_000, 50_000]:
|
|
a = met(sleeve(vv, vv, cal_d, c, G.REBAL_BAND_USD, G.REBAL_EVERY, cost_form(fixed=1.0)))
|
|
rb, pb = sleeve(vv, vv, cal_d, c, G.REBAL_BAND_USD, G.REBAL_EVERY,
|
|
cost_form(fixed=1.0), integer_shares=True, want_pos=True)
|
|
b = met(rb)
|
|
vive = int((pb.abs().sum(axis=0) > 0).sum())
|
|
print(f" {nome:9s}{'$'+format(c,',d'):>10s}{a['sharpe']:>10.2f}{b['sharpe']:>11.2f}"
|
|
f"{b['sharpe']-a['sharpe']:>+8.2f}{a['vol']:>11.1%}{b['vol']:>12.1%}"
|
|
f"{str(vive)+'/6':>12s}{(pb > 0).mean().mean():>10.0%}")
|
|
print("\n ⚠️ SI LEGGE LA COLONNA 'gambe vive' E 'vol', NON 'Sharpe'. Un vincolo che ALZA lo")
|
|
print(" Sharpe non e' un miglioramento: arrotondando in giu' riduce l'esposizione e paga")
|
|
print(" meno commissioni. E' il NULL DE-LEVERING (VRP-DD, TP01xDVOL, MAT01: 3 occorrenze")
|
|
print(" nel progetto) — il primo test da fare su ogni 'va meglio'. Qui il controllo e' stato")
|
|
print(" eseguito e il verdetto e' che con l'insieme DEPLOY il de-levering NON c'e'.")
|
|
|
|
# ---------- (5) limite del rilevatore, chiuso sul DANNO ----------
|
|
print("\n" + "=" * 108)
|
|
print(" (5) IL LIMITE DEL CROSS-CHECK, CHIUSO SUL DANNO E NON SULLA RILEVABILITA'")
|
|
print("=" * 108)
|
|
print(" Il cross-check non vede una contaminazione a cambio basso (EURUSD ~1.09 -> 8% di")
|
|
print(" deviazione, dentro il rumore dei giorni violenti: misurato 9.9% il 2025-04-09).")
|
|
print(" Abbassare la soglia genererebbe falsi positivi, quindi si misura il DANNO di una")
|
|
print(" contaminazione NON VISTA invece di inseguirne la rilevabilita'.")
|
|
base = met(sleeve(uc, uc, cal, CAP, G.REBAL_BAND_USD, G.REBAL_EVERY, cost_form(fixed=1.0)))
|
|
rng = np.random.default_rng(20260726)
|
|
for fx, nome in [(1.09, "EURUSD 1.09 — il caso NON rilevabile"),
|
|
(1.27, "GBPUSD 1.27 — riferimento di scala (sempre rilevato)")]:
|
|
ds = []
|
|
for _ in range(60):
|
|
leg = US[int(rng.integers(0, 6))]
|
|
u2 = dict(uc)
|
|
s = _norm(u2[leg]).reindex(cal).ffill().copy()
|
|
s.iloc[int(rng.integers(300, len(s) - 5))] /= fx
|
|
u2[leg] = s
|
|
ds.append(met(sleeve(u2, u2, cal, CAP, G.REBAL_BAND_USD, G.REBAL_EVERY,
|
|
cost_form(fixed=1.0)))["sharpe"] - base["sharpe"])
|
|
ds = np.array(ds)
|
|
print(f" {nome:52s} dSharpe mediano {np.median(ds):+.4f} · "
|
|
f"peggiore {ds.min():+.4f} · |d|>0.05 nel {(np.abs(ds)>0.05).mean():.0%}")
|
|
print("\n" + "=" * 108)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|