ops(live): sorveglianza freschezza feed SKH + correzione dei docstring sulla latenza d'uscita

T1 (TP di SKH01 come limit resting on-book) NON e' stato implementato, per misura.
Leggendo il codice di produzione: TP01 e SKH01 tradano lo stesso strumento con una
sola posizione netta Deribit, quindi un ordine on-book al livello di SKH chiuderebbe
anche quota TP01. Misurati i due ostacoli: (A) segno compatibile nel 97% dei trade
che escono in TP = risolvibile; (B) divergenza modello/live apparentemente bloccante.

Ma (B) non esiste: resample_5m NON scarta il bin 230m in corso (21 barre 5m su 46
nell'ultimo bin) e _skyhook_positions ci itera dentro -> il live rileva gia' SL/TP
intra-barra, entro ~1h dal tocco. I docstring che dicevano "usa solo barre chiuse" e
"latenza fino alla chiusura della barra 230m" erano FALSI e hanno guidato tre analisi
(02/07, 24/07, T1). Corretti sul posto.

Conseguenza: la lente `hourly` sottostima il path live di +0.081 Sharpe FULL di book
(23/23 offset, banda appaiata); il live vero sta sopra il canonical sul FULL, e il fix
richiesto sarebbe un declassamento (+0.054 vs +0.081) in cambio di ordini parziali sul
netto in un percorso con soldi veri.

Cablato invece il problema vero trovato per strada: fresh_5m fallisce in SILENZIO
(fallback al certificato, rigenerato 1x/giorno) -> la latenza d'uscita di SKH01 passa
da ~1h a ~1 giorno senza segnalazione, e nessun controllo esistente scatta (il gate di
staleness guarda il feed di TP01). Stesso schema del feed-freeze del 14/07.

  * livefeed.feed_age_minutes: pura, eta' dalla CHIUSURA della barra, None = non
    misurata, clamp sugli skew d'orologio
  * book_report espone skh_feed_age_min (max fra gli asset)
  * book_execute stampa lo stato e allerta su Telegram sopra skh_feed_max_age_min (30m)

Scelta dichiarata: ALLERTA, NON blocca. Bloccare fermerebbe anche TP01 (nettato sullo
stesso strumento) per un guasto di rete; forzare SKH flat chiuderebbe posizioni buone
su un glitch.

Follow-up dichiarato: la stessa barra parziale tocca gli INGRESSI (ent[n-1] da breakout
non confermato). Disaccordo in 1 bin su 112, ma con 3 entry nel campione la taglia non
e' stimabile. E' un cambio di strategia, non di strumentazione -> misura dedicata prima.

Strategia, pesi e cadenza del cron INVARIATI. Nessun ordine inviato. Suite 266 verdi.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-07-25 20:20:01 +00:00
parent 4b2de1ce87
commit 031b71bf54
9 changed files with 427 additions and 11 deletions
+29 -5
View File
@@ -21,9 +21,19 @@ deribit_book_sleeves = 0.75*TP01 + 0.25*SKH01):
GLI EXIT DI SKH01 SONO IMPLICITI: _skyhook_positions() replica il book (ingressi + SL/TP/max_bars +
non-overlap) sulla feed certificata fresca -> quando il trade va chiuso ritorna 'flat' -> skh_sign=0
-> il target netto si aggiorna -> il reconciler chiude la quota SKH. NB: gli exit sono SOFTWARE
(no bracket on-book per SKH, sennò chiuderebbero anche la quota TP01) -> latenza fino alla chiusura
della barra 230m corrente. Solo il disaster-SL (-30%) resta on-book, sulla posizione NETTA.
-> il target netto si aggiorna -> il reconciler chiude la quota SKH. Gli exit sono SOFTWARE (no
bracket on-book per SKH, sennò chiuderebbero anche la quota TP01). Solo il disaster-SL (-30%) resta
on-book, sulla posizione NETTA.
⚠️ CORREZIONE 2026-07-26 — questo docstring diceva "latenza fino alla chiusura della barra 230m
corrente". E' FALSO, e l'errore e' costato tre settimane di misure sbagliate (audit 02/07, follow-up
24/07): `resample_5m` NON scarta il bin 230m in corso, e il loop di `_skyhook_positions` ci itera
dentro usando il suo high/low CORRENTE -> il tocco di SL/TP e' visto **entro il primo cron dopo il
tocco (~1h)**, non a fine barra (mediana 115 min, p90 210 di ritardo in piu'). Misurato: modellare
il live come "detection a fine barra" (lente `hourly`) lo SOTTOSTIMA di +0.081 di Sharpe FULL sul
book, in 23/23 offset. Diario `2026-07-26-t1-esecuzione-skh-live.md`.
La latenza reale dipende quindi dalla FRESCHEZZA del feed 5m, non dalla griglia 230m -> vedi
`skh_feed_age_min` nel report e l'allerta in scripts/live/book_execute.py.
Questo modulo NON invia nulla: costruisce solo il report/ordine. L'invio è in scripts/live/book_execute.py
(doppio gate). Causale: usa solo barre chiuse (eredita la causalità di TP01 e di _skyhook_positions).
@@ -118,9 +128,18 @@ def book_report(offline: bool = False, equity_override: float | None = None,
equity = sh["equity"]
cap = _cap(equity=equity, real_equity=sh.get("real_equity"), eq_fallback=sh.get("eq_fallback"))
load5m = None
feed_ages: dict[str, float | None] = {}
if live_feed:
from src.live.livefeed import fresh_5m
load5m = fresh_5m
from src.live.livefeed import feed_age_minutes, fresh_5m
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)."""
df = fresh_5m(a)
feed_ages[a] = feed_age_minutes(df)
return df
skh_error = None
try:
skh = _skyhook_positions(load5m=load5m)
@@ -154,5 +173,10 @@ def book_report(offline: bool = False, equity_override: float | None = None,
cap_per_asset=cap, weights=dict(TP01=W_TP01, SKH01=W_SKH),
assets=assets, orders=orders, skh_error=skh_error, pos_error=sh.get("pos_error"),
eq_fallback=sh.get("eq_fallback"),
# eta' (minuti) del feed 5m REALMENTE usato per il segnale SKH: None se non misurata
# (live_feed=False) o non interpretabile. Vale il MASSIMO fra gli asset: se anche uno solo
# 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),
flat=all(abs(x["net_target"]) < FLAT_USD for x in assets),
)
+24 -1
View File
@@ -70,8 +70,31 @@ def merge_tail(base: pd.DataFrame, tail: pd.DataFrame) -> pd.DataFrame:
return merged
def feed_age_minutes(df5: pd.DataFrame, now_ms: int | None = None) -> float | None:
"""Eta' in MINUTI dell'ultima barra 5m del feed passato. None se non interpretabile.
⚠️ E' la metrica che dice se `fresh_5m` ha davvero attaccato la coda fresca o se e' ricaduta
in SILENZIO sul certificato: il fallback non solleva e non logga, quindi senza questa misura
un fetch pubblico rotto e' invisibile. Conta perche' la latenza d'uscita di SKH01 e' dove vive
la qualita' del path live (misurata il 2026-07-26): col feed fresco l'uscita e' rilevata entro
~1 ora dal tocco del livello, sul solo certificato si allunga fino a ~1 GIORNO (il rebuild
certificato gira 1x/giorno). PURA, testabile senza rete."""
if df5 is None or len(df5) == 0 or "timestamp" not in df5:
return None
try:
last = int(df5["timestamp"].iloc[-1])
except (ValueError, TypeError):
return None
now = int(pd.Timestamp.now(tz="UTC").timestamp() * 1000) if now_ms is None else int(now_ms)
return max(0.0, (now - (last + 5 * 60_000)) / 60_000.0)
def fresh_5m(asset: str, lookback_days: int = 12) -> pd.DataFrame:
"""Feed 5m certificato + coda recente effimera (in-memory). Fallback al certificato su errore."""
"""Feed 5m certificato + coda recente effimera (in-memory). Fallback al certificato su errore.
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."""
base = load_data(asset, "5m")
sym = DERIBIT_SYMBOL.get(asset)
if sym is None: