"""Test del rilevatore di split NON aggiustati (src/data/eq_splits.py). Il punto dei test non e' "trova il salto grande" — e' distinguere uno SPLIT da un CROLLO VERO. I due casi reali che hanno motivato il modulo sono entrambi coperti: * IWM/EFA 2005-06-09 -> split non aggiustati (devono essere rilevati e riparati); * SLV 2026-01-30 -> crollo vero del -28.5% con rapporto 1.3994 (a 4bps da 1.4!) e range intraday 33% (deve NON essere toccato). """ from __future__ import annotations import sys from pathlib import Path import numpy as np import pandas as pd import pytest ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT / "scripts" / "research")) from src.data.eq_splits import detect_unadjusted_splits, repair_splits # noqa: E402 def _series(closes, ranges=None, start="2005-01-03"): """OHLC daily sintetico: high/low a +/- meta' del range richiesto attorno al close.""" n = len(closes) idx = pd.bdate_range(start, periods=n, tz="UTC") c = np.asarray(closes, float) rng = np.full(n, 0.01) if ranges is None else np.asarray(ranges, float) return pd.DataFrame({"open": c, "high": c * (1 + rng / 2), "low": c * (1 - rng / 2), "close": c, "volume": np.full(n, 1e6)}, index=idx) def test_rileva_split_2a1_stile_iwm(): df = _series([94.5, 94.6, 94.1] + [47.5, 47.6, 47.4]) sp = detect_unadjusted_splits(df) assert len(sp) == 1 assert sp[0]["factor"] == pytest.approx(2.0) assert sp[0]["date"] == df.index[3] def test_rileva_split_3a1_stile_efa(): df = _series([85.9, 86.3, 85.9] + [28.8, 28.6, 28.7]) sp = detect_unadjusted_splits(df) assert len(sp) == 1 and sp[0]["factor"] == pytest.approx(3.0) def test_crollo_vero_non_e_uno_split_anche_se_il_rapporto_e_vicino(): """SLV 2026-01-30: -28.5%, rapporto 1.3994 (vicinissimo a 1.4) ma range intraday 33%. Se il rilevatore guardasse solo il rapporto, un fattore 1.4 in lista lo distruggerebbe. La guardia vera e' il range: il movimento avviene DENTRO la barra.""" df = _series([98.3, 101.6, 105.6, 105.6, 75.4, 72.4], ranges=[0.10, 0.07, 0.06, 0.12, 0.33, 0.09]) assert detect_unadjusted_splits(df) == [] def test_crollo_con_rapporto_esatto_ma_range_grande_non_e_split(): """Anche un -50% ESATTO non e' uno split se la barra ha range enorme (crollo intraday).""" df = _series([100.0, 100.0, 100.0, 50.0, 51.0], ranges=[0.01, 0.01, 0.01, 0.40, 0.05]) assert detect_unadjusted_splits(df) == [] def test_riparazione_rende_continua_la_serie(): df = _series([94.5, 94.6, 94.1] + [47.5, 47.6, 47.4]) fixed, sp = repair_splits(df) assert len(sp) == 1 r = fixed["close"].pct_change().abs() assert r.max() < 0.05, "dopo la riparazione non deve restare nessun salto anomalo" # i prezzi POST-split non si toccano; quelli PRE si dimezzano assert fixed["close"].iloc[-1] == pytest.approx(47.4) assert fixed["close"].iloc[0] == pytest.approx(94.5 / 2) # il volume va nella direzione opposta assert fixed["volume"].iloc[0] == pytest.approx(1e6 * 2) def test_split_multipli_si_compongono(): """Due split (3:1 poi 2:1): i prezzi piu' antichi vanno divisi per 6, non per 3 o 2.""" df = _series([90.0, 90.0] + [30.0, 30.0] + [15.0, 15.0]) fixed, sp = repair_splits(df) assert len(sp) == 2 assert fixed["close"].iloc[0] == pytest.approx(90.0 / 6) assert fixed["close"].iloc[2] == pytest.approx(30.0 / 2) assert fixed["close"].iloc[-1] == pytest.approx(15.0) def test_serie_pulita_resta_identica(): rng = np.random.default_rng(0) c = 100 * np.cumprod(1 + rng.normal(0, 0.01, 500)) df = _series(c) assert detect_unadjusted_splits(df) == [] fixed, sp = repair_splits(df) assert sp == [] pd.testing.assert_frame_equal(fixed, df) def test_dati_reali_iwm_efa_riparati_dai_loader(): """Verifica end-to-end sui parquet certificati, se presenti.""" eqlib = pytest.importorskip("eqlib", reason="scripts/research non nel path") for sym in ("IWM", "EFA"): try: df = eqlib.load_eq(sym) except FileNotFoundError: pytest.skip(f"eq_{sym.lower()}_1d.parquet assente") assert df["close"].pct_change().abs().max() < 0.5, f"{sym}: salto da split ancora presente"