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>
This commit is contained in:
@@ -229,6 +229,68 @@ def _check_stale_feed(asset: str, df: pd.DataFrame, alerted: set[str]):
|
||||
"gap_pct": round(gap, 2), "prezzo": float(c[i])})
|
||||
|
||||
|
||||
# --- Gate feed CONGELATO (2026-06-15) -----------------------------------------
|
||||
# Distinto da STALE_FEED (osservabilita') e FEED_BOOK_GAP (divergenza esecuzione):
|
||||
# qui si AGISCE. Quando il feed di DECISIONE 1h di un asset e' CONGELATO (guasto
|
||||
# testnet: ETH-PERPETUAL inverse fermo a 1661.95 per 36h+, prezzo MAI cambiato) gli
|
||||
# sleeve CONCENTRATI che ne dipendono (single/ml/pairs) decidono entry/exit su un
|
||||
# prezzo morto -> segnali spuri (z-score pairs estremi -3/-5, SH01 short a prezzo
|
||||
# fermo) e perdite REALI (i fill avvengono al book vero ~1717, l'uscita sim al prezzo
|
||||
# congelato: SH01_ETH ha realizzato -2.83 reali vs -0.09 sim su un solo close). Si
|
||||
# SALTA il tick (entry E exit) finche' il feed non si sblocca: come un outage (i worker
|
||||
# non valutano gli exit, protezione = disaster-SL on-book), auto-guarente.
|
||||
#
|
||||
# DISTINGUERE guasto da ILLIQUIDITA': SOL/LTC/ADA stampano molte barre flat (O=H=L=C,
|
||||
# 50-95%) ma il prezzo SI muove ogni poche ore (run di close identiche CORTE, ~2-5). Il
|
||||
# guasto e' il prezzo che non cambia MAI: run di close INVARIATE >= soglia (il freeze
|
||||
# reale dura decine di barre). Un detector flat-bar ingenuo gaterebbe gli alt
|
||||
# illiquidi-ma-VIVI -> si conta la run di close invariate, NON le barre flat.
|
||||
#
|
||||
# NB POST-FLAT: si gatea DURANTE il freeze (ultima barra completa ferma). La barra di
|
||||
# RIPRESA e' non-flat -> la run si azzera -> il tick riprende SU di essa. NON e'
|
||||
# l'entry-guard post-flat (BOCCIATA: la candela-gap e' l'overshoot che la fade fada con
|
||||
# profitto, CLAUDE.md / 2026-06-05): quello salta la barra di ripresa, questo no.
|
||||
#
|
||||
# SOGLIA (misurata sul feed reale 2026-06-15): le due popolazioni sono ben separate ->
|
||||
# MORTO ETH run 64 (1 val distinto/48h), BNB 64, DOGE 42 (feed pinnato a un valore)
|
||||
# ILLIQUIDO LTC run 10, ADA 11, XRP 12, SOL 1 (5-31 val distinti/48h: SI muovono)
|
||||
# A 24 (un giorno intero di prezzo immobile) il guasto multi-giorno e' preso con ampio
|
||||
# margine senza gateare gli alt illiquidi-ma-VIVI (importante per PR_BTCLTC: BTC vivo +
|
||||
# LTC solo illiquido NON deve sospendere il pair). ETH (run 64) gatea comunque subito.
|
||||
_FREEZE_GATE_BARS = 24 # run di close INVARIATE sull'ultima barra completa = freeze
|
||||
|
||||
|
||||
def _frozen_assets(raw1h: dict[str, pd.DataFrame], threshold: int) -> set[str]:
|
||||
"""Asset col feed di decisione 1h CONGELATO: ultima barra completa flat (O=H=L=C)
|
||||
E close invariata da >= `threshold` barre complete consecutive. 0 = gate disattivo."""
|
||||
from src.live.bars import last_settled_idx
|
||||
frozen: set[str] = set()
|
||||
if threshold <= 0:
|
||||
return frozen
|
||||
for asset, df in raw1h.items():
|
||||
o, h, l, c = (df[k].values for k in ("open", "high", "low", "close"))
|
||||
i = len(c) + last_settled_idx(df["timestamp"].values)
|
||||
if i < 1 or not (o[i] == h[i] == l[i] == c[i]):
|
||||
continue # ultima barra completa non flat -> vivo
|
||||
run = 1
|
||||
while i - run >= 0 and c[i - run] == c[i]:
|
||||
run += 1
|
||||
if run >= threshold:
|
||||
frozen.add(asset)
|
||||
return frozen
|
||||
|
||||
|
||||
def _feed_gated_sids(live_specs, frozen: set[str]) -> set[str]:
|
||||
"""sid degli sleeve CONCENTRATI (single/ml/pairs) che dipendono da un asset col feed
|
||||
congelato. I multi-asset (basket/rotation/tsmom/xsec, tutti PAPER) NON sono gateati:
|
||||
diversificati su 8 asset, un singolo feed fermo non li compromette."""
|
||||
if not frozen:
|
||||
return set()
|
||||
return {s.sid for s in live_specs
|
||||
if s.kind in ("single", "ml", "pairs")
|
||||
and any(a in frozen for a in _spec_assets_tf(s)[0])}
|
||||
|
||||
|
||||
_GAP_BPS_DEFAULT = 150.0 # |close feed - mark book| oltre cui il feed non e' affidabile
|
||||
|
||||
|
||||
@@ -431,6 +493,11 @@ def run(config_path: str = "portfolios.yml"):
|
||||
# guard feed-vs-book (2026-06-12): soglia bps in overrides.feed_book_gap_bps (0 = off)
|
||||
gap_bps = float(_ov.get("feed_book_gap_bps", _GAP_BPS_DEFAULT))
|
||||
gap_alerted: set[str] = set()
|
||||
# gate feed CONGELATO (2026-06-15): salta gli sleeve concentrati su un asset col feed
|
||||
# fermo. Soglia (barre 1h di close invariata) in overrides.feed_freeze_gate_bars (0 = off).
|
||||
freeze_gate_bars = int(_ov.get("feed_freeze_gate_bars", _FREEZE_GATE_BARS))
|
||||
spec_by_id = {s.sid: s for s in live_specs}
|
||||
frozen_gated: set[str] = set() # sleeve gateati ora (dedup alert + recovery)
|
||||
# Osservabilita' outage (improvement-sweep 2026-06-06): il poll-loop intero e' in un
|
||||
# try/except → durante un outage i worker NON valutano gli exit. Alert Telegram dopo
|
||||
# _OUTAGE_POLLS poll falliti/DEGRADATI consecutivi + notifica di ripresa con durata.
|
||||
@@ -486,6 +553,23 @@ def run(config_path: str = "portfolios.yml"):
|
||||
if exec_enabled and gap_bps > 0:
|
||||
_check_feed_book_gap(client, raw1h, exec_instr, gap_bps, gap_alerted)
|
||||
|
||||
# gate feed CONGELATO: sleeve concentrati su un asset col feed fermo -> tick saltato
|
||||
frozen = _frozen_assets(raw1h, freeze_gate_bars)
|
||||
now_gated = _feed_gated_sids(live_specs, frozen)
|
||||
if now_gated != frozen_gated:
|
||||
from src.live.telegram_notifier import notify_event
|
||||
for sid in sorted(now_gated - frozen_gated):
|
||||
fa = sorted(set(_spec_assets_tf(spec_by_id[sid])[0]) & frozen)
|
||||
print(f"[runner] FEED_FROZEN_GATE {sid}: asset congelati {fa} -> tick sospeso")
|
||||
notify_event("FEED_FROZEN_GATE", {
|
||||
"sleeve": sid, "asset_congelati": ", ".join(fa), "stato": "GATED",
|
||||
"nota": "feed di decisione fermo: entry+exit sospesi finche' non si "
|
||||
"sblocca (disaster-SL on-book protegge le posizioni reali)"})
|
||||
for sid in sorted(frozen_gated - now_gated):
|
||||
print(f"[runner] FEED_FROZEN_GATE {sid}: feed ripreso -> tick riattivato")
|
||||
notify_event("FEED_FROZEN_GATE", {"sleeve": sid, "stato": "RIPRESO"})
|
||||
frozen_gated = now_gated
|
||||
|
||||
# fetch DIRETTO dei timeframe sub-orari (15m...) per (asset, tf)
|
||||
raw_sub: dict[tuple[str, str], pd.DataFrame] = {}
|
||||
for (asset, tf), days in subhourly_needs.items():
|
||||
@@ -553,6 +637,8 @@ def run(config_path: str = "portfolios.yml"):
|
||||
"nota": "exit NON valutati per questo worker"})
|
||||
|
||||
for s in live_specs:
|
||||
if s.sid in now_gated:
|
||||
continue # feed di decisione congelato -> entry+exit sospesi
|
||||
_tick_safe(s, workers[s.sid])
|
||||
# PAPER: ticcati per statistica, MAI nel ledger del portafoglio
|
||||
for s in paper_specs:
|
||||
|
||||
Reference in New Issue
Block a user