ops(live): staleness-gate bloccante + report Telegram giornaliero
Presa in carico operativa del conto Deribit (delega dell'utente). Nessun cambio a
strategie, pesi o sizing: il libro resta TP01 0.75 + SKH01 0.25.
1) STALENESS-GATE (protezione di capitale, era un follow-up mai cablato)
Il 2026-07-14 alle 14:00 UTC il book ha COMPRATO ETH $75 con l'ultima barra del
feed certificato ferma al 2026-07-08: feed congelato da 6 giorni (diario
2026-07-15-feed-freeze). I due gate esistenti non potevano vederlo — il conto ERA
online e la posizione ERA leggibile: proteggono dai problemi di CONTO, non da un
feed morto. Il diario raccomandava un alert; qui il gate e' BLOCCANTE, coerente
con gli altri due ("non opero a cieco"), col disaster-SL on-book come rete su
eventuali posizioni gia' aperte.
- config/live.json: max_data_age_days = 2;
- book_execute: _data_age_days() + blocco PRIMA di costruire DeribitTrader
(nessuna sessione autenticata aperta su dati morti) + alert Telegram con il
comando di sblocco; data illeggibile => trattata come stantia;
- letto con cfg.get(default): una config priva della chiave ricade sulla soglia
sicura invece di sollevare KeyError dentro il percorso con soldi veri;
- tests/test_book_staleness_gate.py: 8 casi, incluso il funzionale che riproduce
la situazione del 14/07 (online + posizione leggibile + ordine pronto) e
verifica che DeribitTrader NON venga costruito.
- tests/test_book_live.py: i 3 report finti avevano last_data="2026-07-01"
hardcoded -> ora data fresca calcolata. Quei test riguardano skh_error /
pos_error / eq_fallback, non la staleness: con la data fissa sarebbero marciti
al superamento della soglia.
2) REPORT TELEGRAM GIORNALIERO (scripts/live/telegram_daily.py, in cron_daily.sh)
Gli alert esistenti scattano solo su ordine o errore: con il libro flat — stato
normale e corretto col trend giu' — significava silenzio per settimane,
indistinguibile da un sistema morto. Il report dice ogni giorno dove sta il conto,
perche' non opera e quanto manca perche' operi (con la convenzione TP01 corretta:
media dei SEGNI, quindi servono 2 orizzonti su 3, non basta il piu' breve).
Sola lettura: un test verifica che il modulo non possa inviare ordini.
Stato al commit: equity $596.92, flat e a target, disaster-SL -30% verificato
(placed @ $1,308.7 sull'ultima posizione). TP01 0.00 su entrambi: accensione a
BTC $78.680 (+22,7%) / ETH $2.370 (+27,2%).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -43,9 +43,23 @@ def load_config() -> dict:
|
||||
cfg.setdefault("max_notional_per_asset_usd", 300.0)
|
||||
cfg.setdefault("min_order_usd", 5.0)
|
||||
cfg.setdefault("disaster_sl_pct", 0.30)
|
||||
cfg.setdefault("max_data_age_days", 2.0)
|
||||
return cfg
|
||||
|
||||
|
||||
def _data_age_days(last_data) -> float | None:
|
||||
"""Eta' in giorni dell'ultima barra del feed certificato. None se non interpretabile
|
||||
(trattata come stantia: meglio non operare che operare su una data che non so leggere)."""
|
||||
if last_data in (None, ""):
|
||||
return None
|
||||
try:
|
||||
ts = pd.Timestamp(last_data)
|
||||
ts = ts.tz_localize("UTC") if ts.tz is None else ts.tz_convert("UTC")
|
||||
return float((pd.Timestamp.now(tz="UTC") - ts).total_seconds() / 86400.0)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def log_event(rec: dict):
|
||||
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
||||
with open(LOG, "a") as f:
|
||||
@@ -91,6 +105,27 @@ def _run():
|
||||
"nota": "salto l'esecuzione, non opero a cieco"})
|
||||
return
|
||||
|
||||
stale_days = _data_age_days(r.get("last_data"))
|
||||
# .get col default: un chiamante che passa una config senza la chiave deve ricadere sulla
|
||||
# soglia sicura, non sollevare KeyError dentro il percorso d'esecuzione con soldi veri.
|
||||
max_age = float(cfg.get("max_data_age_days", 2.0))
|
||||
if stale_days is None or stale_days > max_age:
|
||||
# FEED STANTIO -> non eseguo. Il 2026-07-14 il book ha comprato ETH con l'ultima barra
|
||||
# ferma al 07-08 (feed congelato 6 giorni, diario 2026-07-15-feed-freeze): il conto era
|
||||
# online e la posizione leggibile, quindi i due gate esistenti NON scattavano. Il segnale
|
||||
# TP01 viene dal feed su disco: se e' vecchio, si opera alla cieca su dati morti.
|
||||
# Il disaster-SL on-book resta la rete di sicurezza su eventuali posizioni aperte.
|
||||
eta = "ignota" if stale_days is None else f"{stale_days:.0f}g"
|
||||
print(f" 🛑 FEED STANTIO (ultima barra {r.get('last_data')}, eta' {eta} > {max_age:.0f}g)"
|
||||
" -> NON eseguo su dati morti.")
|
||||
print(" Sbloccare con: uv run python scripts/analysis/rebuild_history.py --asset BTC ETH")
|
||||
if do_execute:
|
||||
notify("🛑 BOOK LIVE — FEED STANTIO, esecuzione saltata",
|
||||
{"ultima_barra": str(r.get("last_data")), "eta": eta,
|
||||
"soglia": f"{max_age:.0f}g",
|
||||
"azione": "rebuild_history.py --asset BTC ETH"})
|
||||
return
|
||||
|
||||
if r.get("eq_fallback"): # equity reale non leggibile -> sizing su paper_cap
|
||||
print(f" ⚠️ EQUITY FALLBACK (sizing su paper_cap, NON blocco): {r['eq_fallback']}")
|
||||
if do_execute: # solo diagnostica: l'hard-cap $/asset limita il downside
|
||||
|
||||
Reference in New Issue
Block a user