"""Gate feed CONGELATO (2026-06-15): salta entry+exit degli sleeve concentrati (single/ml/pairs) il cui feed di DECISIONE 1h e' fermo (guasto, es. ETH-PERPETUAL a 1661.95 per 36h+). Distingue il GUASTO (close mai cambiata per decine di barre) dall'ILLIQUIDITA' (SOL/LTC/ADA: molte barre flat ma il prezzo si muove -> run corte). Auto-guarente: la barra di ripresa e' non-flat -> il gate si rilascia da solo.""" import pandas as pd from src.portfolio.runner import _frozen_assets, _feed_gated_sids def _df(completed, forming): """Serie 1h: barre complete (flat = O=H=L=C; non-flat quando il prezzo cambia) + una candela in formazione (ultima riga, esclusa dal detector).""" end_ms = int(pd.Timestamp.now(tz="UTC").floor("h").timestamp() * 1000) vals = list(completed) + [forming] n = len(vals) rows = [] for j, v in enumerate(vals): ts = end_ms - (n - 1 - j) * 3_600_000 rows.append({"timestamp": ts, "open": v, "high": v, "low": v, "close": v, "volume": 0.0}) for j in range(1, n): # bar NON-flat quando il prezzo si muove if rows[j]["close"] != rows[j - 1]["close"]: rows[j]["high"] = rows[j]["close"] * 1.001 return pd.DataFrame(rows) def test_frozen_feed_detected(): raw = {"ETH": _df([1661.95] * 40, 1661.95)} # prezzo MAI cambiato (guasto) assert _frozen_assets(raw, 12) == {"ETH"} def test_live_feed_not_frozen(): raw = {"BTC": _df([65000 + i * 5 for i in range(40)], 65200.0)} # sempre in movimento assert _frozen_assets(raw, 12) == set() def test_illiquid_but_alive_not_frozen(): # ultima barra completa FLAT ma il prezzo si e' mosso da poche barre (run < soglia): # alt illiquido (SOL/LTC/ADA), NON un guasto -> non gateato. raw = {"LTC": _df([44.0] * 30 + [45.0] * 5, 45.0)} assert _frozen_assets(raw, 12) == set() def test_resume_releases_gate(): # rilascio solo su una barra COMPLETATA non-flat (la forming non basta -> coerente # con la valutazione su barre settled, EXIT-16). NON e' l'entry-guard post-flat: la # barra di ripresa NON viene saltata, il tick riprende su di essa. raw = {"ETH": _df([1661.95] * 40 + [1700.0], 1700.0)} assert _frozen_assets(raw, 12) == set() # finche' la ripresa e' solo nella candela in formazione, resta congelato assert _frozen_assets({"ETH": _df([1661.95] * 40, 1700.0)}, 12) == {"ETH"} def test_threshold_zero_disables(): raw = {"ETH": _df([1661.95] * 40, 1661.95)} assert _frozen_assets(raw, 0) == set() def test_gated_scope_matches_eth_legs(): """Sui veri sleeve PORT06: con ETH congelato si gatano TUTTE e sole le gambe ETH (3 fade ETH + SH_ETH + 5 pairs con gamba ETH). BTC-only, BTC/LTC e i multi-asset (anche ROT02 che ha ETH nell'universo) restano attivi.""" from scripts.portfolios._defs import FADE, PAIRS, SHAPE, HONEST specs = FADE + PAIRS + SHAPE + HONEST g = _feed_gated_sids(specs, {"ETH"}) assert {"MR01_ETH", "MR02_ETH", "MR07_ETH", "SH_ETH", "PR_ETHBTC", "PR_ETHBTC_15M", "PR_LTCETH", "PR_ADAETH", "PR_ETHSOL"} <= g assert g.isdisjoint({"MR01_BTC", "MR02_BTC", "MR07_BTC", "SH_BTC", "DIP01_BTC", "PR_BTCLTC"}) assert g.isdisjoint({"TR01_basket", "ROT02_rot"}) # multi-asset mai gateati assert _feed_gated_sids(specs, set()) == set() # nessun congelato -> nessun gate