Files
PythagorasGoal/tests/portfolio/test_freeze_gate.py
T
Adriano Dal Pastro 4c184d5cbc feat(runner): gate feed CONGELATO — sospende gli sleeve ETH-leg finche' il feed non si sblocca
Audit "stato ordini": il feed ETH-PERPETUAL congelato a 1661.95 (36h+) generava
perdite REALI (SH01_ETH -2.83 reali vs -0.09 sim su un close + riapertura; 4 pairs
con gamba ETH entrati su z-score spuri -3/-5/+5.6 = artefatto del log-ratio con ETH
pinnato mentre gli alt si muovono).

_frozen_assets + _feed_gated_sids: quando il feed di decisione 1h di un asset e'
congelato, gli sleeve concentrati (single/ml/pairs) che ne dipendono saltano il tick
(entry+exit) finche' non si sblocca (come outage; disaster-SL on-book = coda).
Auto-guarente: rilascio alla prima barra COMPLETA non-flat (NON l'entry-guard
post-flat bocciata). Detector guasto-vs-illiquido: conta la run di close INVARIATE
(ETH/BNB/DOGE run 40-64, 1-4 val/48h = morti; SOL/LTC/ADA run <=12, 5-31 val = vivi).
Soglia feed_freeze_gate_bars=24 -> gatea le 9 gambe ETH esatte, PR_BTCLTC e i
multi-asset restano attivi. Alert Telegram FEED_FROZEN_GATE GATED/RIPRESO.

Test test_freeze_gate.py (6, detector+scope+rilascio). Suite portfolio 140/140.
Cerotto testnet: il fix vero e' mainnet. Diario 2026-06-15-frozen-feed-gate.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 08:44:39 +00:00

71 lines
3.3 KiB
Python

"""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