fix(live): 4 hardening da code-review — REAL_DIVERGENCE, outage su feed vuoto, bars.py condiviso, DSL cancel retry
Review multi-agente 7-angoli su 8c4e1cd..HEAD + check trades live:
1. Alert Telegram REAL_DIVERGENCE quando |slippage sim/reale| >= 100bps a
open/close. Causa scatenante: spike print testnet 10:37 (candela 10:00
H=65618, O/C ~62400) -> 3 fade BTC short su close fantasma 65266.5, reale
fillato a 62395 (-440bps), sim +2.26 mai esistiti — passato in silenzio.
2. FEED_OUTAGE anche su feed degradato SENZA eccezione (HTTP 200 + candles
vuote: i worker saltavano il tick in silenzio, streak a 0). Helper unico
_outage_tick; chiavi payload uniformate (minuti su start e RIPRESO).
3. src/live/bars.py: detection forming-bar unificata (bar_ms_of /
last_bar_is_forming / last_settled_idx) — era copiata in 4 punti
(strategy_worker, basket, pairs, _check_stale_feed hardcoded 1h).
E' l'invariante di sicurezza EXIT-16: ora una sola implementazione testata.
4. DSL cancel hardening in _real_close: retry su errore transitorio + alert
REAL_DSL_CANCEL_FAIL se lo stop resta forse orfano sul book (prima l'id
veniva dimenticato in silenzio); order_not_found = probabile trigger in
outage -> solo log (il close a valle esce gia' verified=False).
Refutato il finding top dei finder ("stop_market senza trigger"): cerbero-mcp
traduce price->trigger_price+mark, e in produzione 2 DSL armati + 1 ciclo
completo pulito (MR07_BTC).
Test: 83/83 (9 nuovi: bars helper + DSL/divergence con executor finto).
Smoke testnet 4 scenari verdi, conto flat, zero falsi allarmi.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+38
-24
@@ -150,13 +150,13 @@ def _check_stale_feed(asset: str, df: pd.DataFrame, alerted: set[str]):
|
||||
risveglio (con il gap % del primo prezzo reale). Una notifica per episodio.
|
||||
NB: SOLO osservabilita' — saltare gli ingressi post-flat PEGGIORA l'edge
|
||||
(testato: la candela-gap e' l'overshoot che la fade fada con profitto)."""
|
||||
import time as _t
|
||||
from src.live.bars import last_settled_idx
|
||||
from src.live.telegram_notifier import notify_event
|
||||
if len(df) < _STALE_BARS + 2:
|
||||
return
|
||||
o, h, l, c = (df[k].values for k in ("open", "high", "low", "close"))
|
||||
# ultima barra COMPLETA: la riga -1 e' la candela in corso finche' non e' trascorsa
|
||||
k = -1 if int(_t.time() * 1000) >= int(df["timestamp"].iloc[-1]) + 3_600_000 else -2
|
||||
# ultima barra COMPLETA (detection condivisa src.live.bars; prima hardcodava 1h)
|
||||
k = last_settled_idx(df["timestamp"].values)
|
||||
i = len(c) + k
|
||||
if i < 1:
|
||||
return
|
||||
@@ -262,10 +262,35 @@ def run(config_path: str = "portfolios.yml"):
|
||||
stale_alerted: set[str] = set() # asset con alert STALE_FEED attivo (dedup per episodio)
|
||||
# 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 consecutivi (con l'elenco delle posizioni REALI aperte,
|
||||
# protette solo dal disaster-bracket on-book) + notifica di ripresa con la durata.
|
||||
# _OUTAGE_POLLS poll falliti/DEGRADATI consecutivi + notifica di ripresa con durata.
|
||||
# "Degradato" include il caso HTTP-200-con-candles-vuote (code review 2026-06-07):
|
||||
# non solleva eccezione ma i worker dell'asset mancante saltano il tick in silenzio.
|
||||
_OUTAGE_POLLS = 5
|
||||
fail_streak = 0
|
||||
|
||||
def _outage_tick(failed: bool, streak: int, detail: str = "") -> int:
|
||||
"""Aggiorna lo streak e gestisce gli alert FEED_OUTAGE (start a soglia, una
|
||||
volta per episodio; RIPRESO al primo poll pulito). Ritorna il nuovo streak."""
|
||||
from src.live.telegram_notifier import notify_event
|
||||
if failed:
|
||||
streak += 1
|
||||
if streak == _OUTAGE_POLLS:
|
||||
real_open = sorted(sid for sid, wk in workers.items()
|
||||
if getattr(wk, "real_in_position", False))
|
||||
notify_event("FEED_OUTAGE", {
|
||||
"poll_falliti": streak,
|
||||
"minuti": round(streak * poll / 60),
|
||||
"dettaglio": detail,
|
||||
"posizioni_reali_aperte": ", ".join(real_open) or "nessuna",
|
||||
"nota": "exit NON valutati durante l'outage; "
|
||||
"protezione = disaster-SL on-book sui fade reali"})
|
||||
return streak
|
||||
if streak >= _OUTAGE_POLLS:
|
||||
notify_event("FEED_OUTAGE", {"status": "RIPRESO",
|
||||
"poll_falliti": streak,
|
||||
"minuti": round(streak * poll / 60)})
|
||||
return 0
|
||||
|
||||
while True:
|
||||
try:
|
||||
# fetch 1h per asset al lookback massimo richiesto
|
||||
@@ -312,30 +337,19 @@ def run(config_path: str = "portfolios.yml"):
|
||||
rebalance_allocations(ledger, workers, weights)
|
||||
last_day = today
|
||||
ledger.save()
|
||||
if fail_streak >= _OUTAGE_POLLS:
|
||||
from src.live.telegram_notifier import notify_event
|
||||
notify_event("FEED_OUTAGE", {
|
||||
"status": "RIPRESO",
|
||||
"poll_falliti": fail_streak,
|
||||
"durata_min": round(fail_streak * poll / 60)})
|
||||
fail_streak = 0
|
||||
# feed degradato senza eccezione: asset richiesti ma senza candele
|
||||
missing = sorted(a for a in asset_days if a not in raw1h)
|
||||
if missing:
|
||||
print(f"[runner] feed incompleto: mancano {missing} (streak {fail_streak + 1})")
|
||||
fail_streak = _outage_tick(bool(missing), fail_streak,
|
||||
detail=f"feed senza candele per: {', '.join(missing)}")
|
||||
except KeyboardInterrupt:
|
||||
ledger.save()
|
||||
print("shutdown")
|
||||
break
|
||||
except Exception as e:
|
||||
fail_streak += 1
|
||||
print(f"[runner] errore: {e} (streak {fail_streak})")
|
||||
if fail_streak == _OUTAGE_POLLS:
|
||||
from src.live.telegram_notifier import notify_event
|
||||
real_open = sorted(sid for sid, wk in workers.items()
|
||||
if getattr(wk, "real_in_position", False))
|
||||
notify_event("FEED_OUTAGE", {
|
||||
"poll_falliti": fail_streak,
|
||||
"minuti": round(fail_streak * poll / 60),
|
||||
"posizioni_reali_aperte": ", ".join(real_open) or "nessuna",
|
||||
"nota": "exit NON valutati durante l'outage; "
|
||||
"protezione = disaster-SL on-book sui fade reali"})
|
||||
print(f"[runner] errore: {e} (streak {fail_streak + 1})")
|
||||
fail_streak = _outage_tick(True, fail_streak, detail=f"eccezione: {e}")
|
||||
time.sleep(poll)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user