live(feed): l'allerta funzionava, la sua CAUSA era una riga cablata
Il 29/07 il feed 5m di SKH01 e' ricaduto sul certificato in 6 giri orari su 8 (eta' 265->685 min, +60 a ogni giro = firma esatta del fallback): latenza d'uscita da ~1h a ~11h, book flat, nessuna posizione esposta. L'allerta del 26/07 ha segnalato 6/6, poi ha stampato un perche' che non aveva misurato — la nota "fetch pubblico KO" era cablata, identica in ogni caso, compreso quello in cui la coda fresca E' attaccata e il vecchio e' il certificato. E la causa vera non era recuperabile per costruzione: _fetch_recent_5m ingoia l'eccezione di pagina con un break e a prima pagina fallita ritorna un frame vuoto, indistinguibile da "il venue non ha barre". Cablato: livefeed.last_fetch_error() (registra E logga nel punto in cui l'errore viene ingoiato) -> book_report.skh_feed_errors -> allerta con la causa. Stesso buco chiuso sul ramo gemello "conto offline", che la ragione l'aveva gia' in mark_src e non la stampava mai. La causa del 29/07 resta IGNOTA e va citata cosi': una prima stesura la attribuiva a un rate limit per-IP come se fosse un fatto -> rimossa, sarebbe stato lo stesso difetto che stavo correggendo scritto meglio. Cron spostato al minuto :07 come ripiego da UNA osservazione, dichiarato tale. Test 11 -> 16 (incluso il caso a meta' paginazione: coda parziale attaccata, mancano le barre PIU' recenti). Book/pesi/config/strategia INVARIATI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+13
-2
@@ -194,16 +194,23 @@ def book_report(offline: bool = False, equity_override: float | None = None,
|
||||
cap = _cap(equity=equity, real_equity=sh.get("real_equity"), eq_fallback=sh.get("eq_fallback"))
|
||||
load5m = None
|
||||
feed_ages: dict[str, float | None] = {}
|
||||
feed_errors: dict[str, str] = {}
|
||||
if live_feed:
|
||||
from src.live.livefeed import feed_age_minutes, fresh_5m
|
||||
from src.live.livefeed import feed_age_minutes, fresh_5m, last_fetch_error
|
||||
|
||||
def load5m(a: str):
|
||||
"""Wrapper che MISURA la freschezza del feed effettivamente usato per il segnale.
|
||||
`fresh_5m` ricade sul certificato in SILENZIO se il fetch pubblico fallisce: senza
|
||||
questa misura la latenza d'uscita di SKH01 passerebbe da ~1h a ~1 giorno senza che
|
||||
nulla lo segnali (vedi feed_age_minutes)."""
|
||||
nulla lo segnali (vedi feed_age_minutes). Raccoglie anche la CAUSA del fallback:
|
||||
l'eta' dice CHE il feed e' vecchio, non PERCHE' — e senza il perche' la diagnosi
|
||||
non e' rifacibile a posteriori, perche' quando la si prova il guasto e' rientrato
|
||||
(29/07: 6 giri stantii su 8, causa mai stabilita -> vedi livefeed._LAST_ERROR)."""
|
||||
df = fresh_5m(a)
|
||||
feed_ages[a] = feed_age_minutes(df)
|
||||
err = last_fetch_error()
|
||||
if err is not None:
|
||||
feed_errors[a] = err
|
||||
return df
|
||||
skh_error = None
|
||||
try:
|
||||
@@ -247,5 +254,9 @@ def book_report(offline: bool = False, equity_override: float | None = None,
|
||||
# e' stantio, il segnale netto e' sospetto.
|
||||
skh_feed_age_min=(max((v for v in feed_ages.values() if v is not None), default=None)
|
||||
if feed_ages else None),
|
||||
# PERCHE' la coda fresca non e' stata attaccata, per asset ({} = nessun fallback).
|
||||
# Accompagna skh_feed_age_min: senza, l'allerta puo' solo TIRARE A INDOVINARE la causa
|
||||
# (ed e' esattamente cio' che faceva, con una nota cablata, fino al 29/07).
|
||||
skh_feed_errors=dict(feed_errors),
|
||||
flat=all(abs(x["net_target"]) < FLAT_USD for x in assets),
|
||||
)
|
||||
|
||||
+57
-3
@@ -14,6 +14,7 @@ TP01 è giornaliero e gira bene sul feed certificato.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import time
|
||||
|
||||
import pandas as pd
|
||||
@@ -25,6 +26,42 @@ from src.data.downloader import load_data
|
||||
DERIBIT_SYMBOL = {"BTC": "BTC/USD:BTC", "ETH": "ETH/USD:ETH"}
|
||||
SCHEMA = ["timestamp", "open", "high", "low", "close", "volume"]
|
||||
|
||||
_LOG = logging.getLogger(__name__)
|
||||
|
||||
# CAUSA dell'ultimo fallback. Il fallback resta SILENZIOSO come comportamento (mai operare a cieco
|
||||
# > mai operare, scelta del 26/07): quello che NON deve restare silenzioso e' il PERCHE'.
|
||||
#
|
||||
# Perche' esiste (2026-07-29): dopo il riavvio della VPS delle 04:11 UTC il feed e' ricaduto sul
|
||||
# certificato in 6 giri orari su 8 fra le 05:00 e le 12:00 -> fino a 685 min di eta', cioe' la
|
||||
# latenza d'uscita di SKH01 passata da ~1h a ~11h. L'allerta del 26/07 ha segnalato ogni volta
|
||||
# (ha funzionato), ma la CAUSA non era recuperabile dai log: l'eccezione di pagina viene ingoiata
|
||||
# con un `break` e `fresh_5m` ritorna il certificato senza lasciare traccia. L'unica riga sulla
|
||||
# causa era CABLATA nell'allerta ("fetch pubblico KO") = una presunzione stampata come misura.
|
||||
# ⚠️ La causa di QUEL giorno resta IGNOTA e tale deve restare scritta: le prove sono solo
|
||||
# circostanziali (i giri falliti durano quanto quelli riusciti, 17-47s -> errore immediato, non
|
||||
# timeout; nello stesso giorno il percorso del CONTO, rete diversa ma stesso venue a valle, dava
|
||||
# ReadTimeout/404/502). Ipotesi non distinguibili a posteriori: rate limit per-IP del venue,
|
||||
# contesa di rete/CPU sulla VPS al minuto tondo, degrado post-riavvio.
|
||||
# Questa variabile serve a che la PROSSIMA occorrenza sia un dato e non un'ipotesi.
|
||||
_LAST_ERROR: str | None = None
|
||||
|
||||
|
||||
def last_fetch_error() -> str | None:
|
||||
"""Perche' l'ultima `fresh_5m` non ha attaccato una coda fresca COMPLETA.
|
||||
|
||||
`None` = coda attaccata senza errori. Non-None = fallback al certificato **oppure** coda
|
||||
troncata a meta' paginazione (la paginazione va in avanti: se cade a pagina N mancano le
|
||||
barre piu' RECENTI, quindi in entrambi i casi il feed e' piu' vecchio di quanto sembri).
|
||||
Chi decide guarda `feed_age_minutes`; questa dice il perche'."""
|
||||
return _LAST_ERROR
|
||||
|
||||
|
||||
def _note_error(msg: str) -> None:
|
||||
"""Registra E logga la ragione del fallback. Non solleva: il chiamante prosegue sul certificato."""
|
||||
global _LAST_ERROR
|
||||
_LAST_ERROR = msg
|
||||
_LOG.warning("fresh_5m: coda fresca non attaccata, fallback al feed certificato — %s", msg)
|
||||
|
||||
|
||||
def _fetch_recent_5m(symbol: str, lookback_days: int) -> pd.DataFrame:
|
||||
"""Coda recente di 5m da Deribit pubblico (ccxt). Paginazione in avanti. Solo letture pubbliche."""
|
||||
@@ -38,7 +75,12 @@ def _fetch_recent_5m(symbol: str, lookback_days: int) -> pd.DataFrame:
|
||||
guard += 1
|
||||
try:
|
||||
r = ex.fetch_ohlcv(symbol, "5m", since=since, limit=1000)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
# E' QUI che il guasto diventa invisibile: l'errore di pagina viene ingoiato e la
|
||||
# funzione ritorna comunque cio' che ha raccolto finora. Se a fallire e' la PRIMA
|
||||
# pagina il risultato e' un DataFrame VUOTO, che a valle e' indistinguibile da
|
||||
# "Deribit non ha barre" — ed e' il ramo che il 29/07 ha cancellato la diagnosi.
|
||||
_note_error(f"{type(e).__name__}: {e} (pagina {guard}, {symbol})")
|
||||
break
|
||||
r = [x for x in r if int(x[0]) >= since]
|
||||
if not r:
|
||||
@@ -94,13 +136,25 @@ def fresh_5m(asset: str, lookback_days: int = 12) -> pd.DataFrame:
|
||||
|
||||
NB: il fallback e' SILENZIOSO per scelta (mai operare a cieco > mai operare). Chi esegue deve
|
||||
misurare la freschezza di cio' che riceve con `feed_age_minutes` — vedi book.book_report, che
|
||||
la espone come `skh_feed_age_min`, e book_execute, che allerta."""
|
||||
la espone come `skh_feed_age_min`, e book_execute, che allerta. La RAGIONE del fallback resta
|
||||
disponibile in `last_fetch_error()` fino alla chiamata successiva."""
|
||||
global _LAST_ERROR
|
||||
_LAST_ERROR = None
|
||||
base = load_data(asset, "5m")
|
||||
sym = DERIBIT_SYMBOL.get(asset)
|
||||
if sym is None:
|
||||
_note_error(f"nessun simbolo Deribit per l'asset {asset}")
|
||||
return base
|
||||
try:
|
||||
tail = _fetch_recent_5m(sym, lookback_days)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
_note_error(f"{type(e).__name__}: {e} ({sym})")
|
||||
return base
|
||||
if tail is None or len(tail) == 0:
|
||||
# Coda vuota senza eccezione propagata: o _fetch_recent_5m ha gia' registrato l'errore
|
||||
# di pagina, o Deribit ha davvero risposto senza barre. Sono due guasti diversi e vanno
|
||||
# detti diversi, senno' si ricade nel "non vedo = va tutto bene" gia' pagato due volte.
|
||||
if _LAST_ERROR is None:
|
||||
_note_error(f"nessuna barra restituita da {sym} (risposta vuota, nessuna eccezione)")
|
||||
return base
|
||||
return merge_tail(base, tail)
|
||||
|
||||
Reference in New Issue
Block a user