"""Test della misura sugli INGRESSI da barra 230m parziale (r0726_skh_partial_entry.py). Il rischio di questa misura non e' sbagliare un numero: e' **misurare la propria aritmetica invece della strategia**. E' gia' successo in sessione — il primo self-check dava "BTC 80/80 OK" mentre la ricostruzione era rotta, perche' campionava bin a caso e gli ingressi sono ~2% dei bin: confrontava zeri con zeri. Questi test blindano le due cose che rendono la misura credibile: 1. la ricostruzione riproduce `skyhook_entries` sui bin che HANNO un ingresso (non su bin a caso); 2. il confine del bin e' gestito bene (un'osservazione ALLA chiusura appartiene al bin che chiude). """ from __future__ import annotations import sys from pathlib import Path import numpy as np import pytest ROOT = Path(__file__).resolve().parents[1] for p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt"): sys.path.insert(0, str(p)) pytest.importorskip("pandas") import r0702_anchor_skh01 as r02 # noqa: E402 import r0726_skh_partial_entry as pe # noqa: E402 # --------------------------------------------------------------- confini del bin def test_osservazione_alla_chiusura_appartiene_al_bin_che_chiude(): """IL bug della prima stesura: `obs // MS_LTF` alla chiusura salta al bin SUCCESSIVO, che e' ancora vuoto -> signal_at ritorna None -> il self-check confronta zeri e passa a vuoto.""" start = 1_700_000_000_000 // pe.MS_LTF * pe.MS_LTF close = start + pe.MS_LTF assert ((close - 1) // pe.MS_LTF) * pe.MS_LTF == start assert (close // pe.MS_LTF) * pe.MS_LTF == start + pe.MS_LTF # il bug, documentato def test_le_osservazioni_stanno_dentro_il_bin_e_non_includono_la_chiusura(): """Il cron gira a ore piene: le osservazioni INTRA-bin sono quelle strettamente interne. La chiusura non e' un'osservazione intra-bin (li' il backtest e il live coincidono).""" start = 1_700_000_000_000 // pe.MS_LTF * pe.MS_LTF obs = pe.observations(start) assert obs, "un bin da 230 min contiene almeno un confine orario" assert all(start < t < start + pe.MS_LTF for t in obs) assert all(t % pe.MSH == 0 for t in obs), "il cron gira a ore piene" assert len(obs) in (3, 4), len(obs) # 230 min contiene 3 o 4 confini orari # --------------------------------------------------------------- fedelta' della ricostruzione @pytest.mark.parametrize("asset", ["BTC", "ETH"]) def test_ricostruzione_fedele_sui_bin_CON_ingresso(asset): """Il test che conta. Se fallisce, ogni numero della misura e' privo di significato.""" r = pe.selfcheck(asset, n_signal=40, n_flat=60) assert r["sig_tot"] >= 20, "campione troppo piccolo per dire qualcosa" assert r["sig_ok"] == r["sig_tot"], (asset, r) def test_il_selfcheck_non_e_vacuo(): """Deve testare bin che HANNO un ingresso, altrimenti passa anche da rotto.""" r = pe.selfcheck("BTC", n_signal=40, n_flat=10) assert r["sig_tot"] >= 20 assert r["flat_tot"] <= 10 def test_segnale_a_fine_bin_coincide_col_backtest_su_un_caso_reale(): """Verifica end-to-end su un bin con ingresso vero: direzione E prezzo di riferimento.""" _, _, ltf, ent = r02.run_asset("BTC", 0) df5 = r02.get5m("BTC") ts5 = df5["timestamp"].values.astype(np.int64) o5, h5 = df5["open"].values.astype(float), df5["high"].values.astype(float) l5, c5 = df5["low"].values.astype(float), df5["close"].values.astype(float) v5 = df5["volume"].values.astype(float) htf = r02.resample_off(df5, pe.HTF_MIN, 0) tsl = ltf["timestamp"].values.astype(np.int64) ks = [k for k in range(pe.WARM_HTF * 3, len(ltf) - 1) if ent[k]] assert ks, "nessun ingresso nello storico: il test non direbbe nulla" k = ks[-1] s = pe.signal_at(int(tsl[k]) + pe.MS_LTF, ltf, htf, ts5, o5, h5, l5, c5, v5) assert s is not None assert s["dir"] == ent[k]["dir"] assert s["close"] == pytest.approx(float(ltf["close"].values[k]), rel=1e-9) # --------------------------------------------------------------- assenza di look-ahead @pytest.mark.parametrize("asset", ["BTC", "ETH"]) def test_nessun_lookahead_intra_bin(asset): """IL test che rende credibile il risultato della misura. Il self-check sui bin CON ingresso valida la ricostruzione **a chiusura di bin**: li' la barra e' completa, quindi un look-ahead che esistesse SOLO intra-bin gli sarebbe invisibile per costruzione. E' proprio intra-bin che il live vive, ed e' proprio li' che la misura trova il vantaggio -> senza questo test il vantaggio potrebbe essere futuro travestito. Controllo decisivo: se il futuro contasse, troncare i 5m alle sole barre GIA' CHIUSE a obs_ms cambierebbe la risposta. Deve essere identica, e il prezzo d'ingresso deve coincidere con l'ultimo close 5m disponibile in quell'istante. """ _, _, ltf, ent = r02.run_asset(asset, 0) df5 = r02.get5m(asset) ts5 = df5["timestamp"].values.astype(np.int64) o5, h5 = df5["open"].values.astype(float), df5["high"].values.astype(float) l5, c5 = df5["low"].values.astype(float), df5["close"].values.astype(float) v5 = df5["volume"].values.astype(float) htf = r02.resample_off(df5, pe.HTF_MIN, 0) tsl = ltf["timestamp"].values.astype(np.int64) ks = [k for k in range(pe.WARM_HTF * 3, len(ltf) - 1) if ent[k]][-8:] assert ks, "nessun ingresso nello storico: il test non direbbe nulla" n = 0 for k in ks: for obs in pe.observations(int(tsl[k])): full = pe.signal_at(obs, ltf, htf, ts5, o5, h5, l5, c5, v5) j = int(np.searchsorted(ts5 + pe.MS5, obs, side="right")) # solo barre chiuse trunc = pe.signal_at(obs, ltf, htf, ts5[:j], o5[:j], h5[:j], l5[:j], c5[:j], v5[:j]) n += 1 assert (full or {}).get("dir", 0) == (trunc or {}).get("dir", 0), (asset, obs) assert (full or {}).get("close") == (trunc or {}).get("close"), (asset, obs) if full: assert full["close"] == pytest.approx(float(c5[j - 1]), rel=1e-9), ( "il prezzo d'ingresso deve essere gia' noto a obs_ms") assert n >= 8, f"campione intra-bin troppo piccolo ({n})" # --------------------------------------------------------------- livelli congelati def test_livelli_coerenti_col_meccanismo_congelato(): """SKH01_V2_DD e' pct asimmetrico: long sl4%/tp10%, short sl2%/tp8%.""" sl_l, tp_l, mb_l = pe.levels(+1, 100.0, 1.0) sl_s, tp_s, mb_s = pe.levels(-1, 100.0, 1.0) assert sl_l == pytest.approx(96.0) and tp_l == pytest.approx(110.0) assert sl_s == pytest.approx(102.0) and tp_s == pytest.approx(92.0) assert (mb_l, mb_s) == (24, 16) assert (tp_s - 100.0) > (92.0 - 100.0) - 1e-9 # short: TP sotto l'ingresso def test_finestra_di_warmup_copre_le_memorie_degli_indicatori(): """La ricostruzione a finestra e' lecita solo se la finestra supera la memoria piu' lunga.""" assert pe.WARM_HTF > max(pe.P.ptn_n, pe.P.n_vola, pe.P.n_volume, pe.P.atr_win) * 3