741bbc2c09
IWM ed EFA avevano uno split NON aggiustato il 2005-06-09 (IWM 2:1 = -49.5%, EFA 3:1 = -66.5%): IB ADJUSTED_LAST non li aveva aggiustati. La certificazione non li vedeva per un punto cieco STRUTTURALE: l'unica guardia sui salti era `maxret > 50% -> SPIKE?` e uno split 2:1 fa esattamente -50%, cioe' cade sul filo della soglia (IWM passava a 49.5% con status OK). IWM e' una delle 6 gambe di GTAA01, sleeve in PRODUZIONE. Impatto misurato: GTAA6 FULL Sharpe 0.61 -> 0.64, IS (<2015) 0.49 -> 0.54; OOS 2015+ e maxDD INVARIATI (l'artefatto e' nel 2005, fuori hold-out) -> il difetto SOTTOSTIMAVA lo sleeve: nessuna decisione presa va rivista. Discriminante split-vs-crollo: NON il rapporto (SLV 2026-01-30 ha rapporto 1.3994, a 4bps da 1.4, ma e' un crollo vero: GLD -10.3% lo stesso giorno) ma il RANGE INTRADAY — lo split apre gia' al nuovo livello con range normale (IWM: open 47.00, range 1.7%), il crollo si muove DENTRO la barra (SLV: range 33%). - src/data/eq_splits.py: detect_unadjusted_splits() a 3 condizioni congiunte (|ret|>20% AND rapporto ~ fattore comune AND range intraday <5%) + repair_splits() con split multipli componibili; - riparazione in LETTURA in src/portfolio/gtaa.py::_close (produzione) e scripts/research/eqlib.py::load_eq (ricerca); - fetch_ib_equities.certify(): nuovo status SPLIT-NON-AGG + elenco split rilevati; - tests/test_eq_splits.py: 8 casi, inclusi il falso positivo SLV e un crollo -50% esatto con range grande. Regola nuova: ogni soglia di certificazione tarata su un valore tondo va controllata contro il difetto che genera esattamente quel valore. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
72 lines
3.2 KiB
Python
72 lines
3.2 KiB
Python
"""EQLIB — harness di ricerca EQUITY/ETF (branch research/equities-ib).
|
|
|
|
Legge la CACHE su disco (data/raw/eq_*.parquet, ADJUSTED_LAST, scritta una volta da
|
|
fetch_ib_equities.py) — MAI da IB. `lru_cache` -> ogni parquet si legge una sola volta per processo.
|
|
"Memorizza i dati per non rileggerli ogni volta": la persistenza e' il parquet su disco + questa cache.
|
|
|
|
Espone: universi (SECTORS/BROAD), load_eq(sym), panel(universe) allineato, e riusa lo scorer
|
|
indurito di altlib (_sh, _dd_ret, _to_daily, marginal_vs_tp01) per giudicare i candidati con la
|
|
stessa disciplina del lato crypto.
|
|
"""
|
|
import sys
|
|
from functools import lru_cache
|
|
from pathlib import Path
|
|
import numpy as np, pandas as pd
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(ROOT))
|
|
from src.data.eq_splits import repair_splits # noqa: E402
|
|
RAW = ROOT / "data" / "raw"
|
|
|
|
# 11 SPDR settoriali. I 9 "classici" (sotto SECTORS_CLASSIC) partono 1998; XLRE 2015, XLC 2018.
|
|
SECTORS = ["XLK", "XLF", "XLE", "XLV", "XLI", "XLP", "XLY", "XLU", "XLB", "XLRE", "XLC"]
|
|
SECTORS_CLASSIC = ["XLK", "XLF", "XLE", "XLV", "XLI", "XLP", "XLY", "XLU", "XLB"] # storia lunga (1998+)
|
|
BROAD = ["SPY", "QQQ", "IWM", "TLT", "GLD", "HYG"]
|
|
|
|
|
|
@lru_cache(maxsize=64)
|
|
def load_eq(sym: str) -> pd.DataFrame:
|
|
"""OHLCV aggiustato (dividendi+split) per `sym`, indicizzato datetime UTC. Cache su disco -> RAM."""
|
|
p = RAW / f"eq_{sym.lower()}_1d.parquet"
|
|
if not p.exists():
|
|
raise FileNotFoundError(f"{p} assente — gira: uv run --with ib_async python scripts/research/fetch_ib_equities.py")
|
|
d = pd.read_parquet(p).copy()
|
|
d.index = pd.to_datetime(d["timestamp"], unit="ms", utc=True)
|
|
d = d.sort_index()
|
|
# split NON aggiustati da IB (IWM/EFA 2005-06-09) — vedi src/data/eq_splits.py
|
|
d, _ = repair_splits(d)
|
|
return d[["open", "high", "low", "close", "volume"]]
|
|
|
|
|
|
@lru_cache(maxsize=16)
|
|
def _close_panel(universe: tuple) -> pd.DataFrame:
|
|
cols = {s: load_eq(s)["close"].astype(float) for s in universe}
|
|
return pd.concat(cols, axis=1).sort_index()
|
|
|
|
|
|
def panel(universe=tuple(SECTORS), how: str = "inner") -> pd.DataFrame:
|
|
"""Prezzi close aggiustati [date x asset]. how='inner' = date comuni a TUTTI (start = ETF piu' giovane);
|
|
'outer' = unione (NaN dove un ETF non esiste ancora)."""
|
|
P = _close_panel(tuple(universe))
|
|
return P.dropna(how="any") if how == "inner" else P
|
|
|
|
|
|
def describe(universe=None):
|
|
universe = universe or (SECTORS + BROAD)
|
|
print(f" {'sym':6} {'barre':>6} {'da':>11} {'a':>11} {'anni':>5}")
|
|
for s in universe:
|
|
try:
|
|
d = load_eq(s)
|
|
print(f" {s:6} {len(d):>6} {str(d.index[0].date()):>11} {str(d.index[-1].date()):>11} "
|
|
f"{(d.index[-1]-d.index[0]).days/365.25:>5.1f}")
|
|
except FileNotFoundError:
|
|
print(f" {s:6} (assente)")
|
|
Pc = panel(SECTORS_CLASSIC); Pa = panel(SECTORS)
|
|
print(f"\n panel 9 settori CLASSICI: {Pc.shape[1]}x{len(Pc)} start comune {Pc.index[0].date()}")
|
|
print(f" panel 11 settori : {Pa.shape[1]}x{len(Pa)} start comune {Pa.index[0].date()}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
print("=" * 70); print(" EQLIB — cache equity su disco (nessun IB)"); print("=" * 70)
|
|
describe()
|