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:
@@ -0,0 +1,96 @@
|
||||
"""Test della misura di FRESCHEZZA del feed 5m usato dal segnale SKH01 (2026-07-26).
|
||||
|
||||
Perche' esiste questo file: `src.live.livefeed.fresh_5m` ricade sul feed certificato **in
|
||||
silenzio** se il fetch pubblico Deribit fallisce — non solleva, non logga. Il feed certificato si
|
||||
rigenera una volta al giorno, quindi in quel caso la latenza d'uscita di SKH01 passa da ~1 ora a
|
||||
~1 giorno senza che nulla lo segnali. Ed e' proprio la latenza d'uscita dove vive la qualita' del
|
||||
path live (misura del 2026-07-26: modellarla male sottostimava il book di +0.081 Sharpe FULL).
|
||||
|
||||
I test blindano che la misura ci sia, sia corretta nei casi limite, e arrivi fino al report.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
pytest.importorskip("pandas")
|
||||
import pandas as pd # noqa: E402
|
||||
|
||||
from src.live.livefeed import SCHEMA, feed_age_minutes, merge_tail # noqa: E402
|
||||
|
||||
|
||||
def _df(ts_list):
|
||||
return pd.DataFrame([[t, 1.0, 1.0, 1.0, 1.0, 1.0] for t in ts_list], columns=SCHEMA)
|
||||
|
||||
|
||||
NOW = 1_800_000_000_000 # riferimento fisso: i test non devono dipendere dall'orologio
|
||||
|
||||
|
||||
def test_feed_fresco_ha_eta_zero():
|
||||
"""Barra chiusa proprio adesso -> eta' 0 (l'eta' si misura dalla CHIUSURA, non dall'apertura)."""
|
||||
assert feed_age_minutes(_df([NOW - 5 * 60_000]), now_ms=NOW) == pytest.approx(0.0)
|
||||
|
||||
|
||||
def test_eta_misurata_dalla_chiusura_della_barra():
|
||||
"""Una barra 5m aperta 35 min fa e' chiusa da 30 -> eta' 30, non 35."""
|
||||
assert feed_age_minutes(_df([NOW - 35 * 60_000]), now_ms=NOW) == pytest.approx(30.0)
|
||||
|
||||
|
||||
def test_feed_di_un_giorno_supera_di_gran_lunga_la_soglia():
|
||||
"""E' lo scenario da intercettare: fallback silenzioso sul certificato (rebuild 1x/giorno)."""
|
||||
age = feed_age_minutes(_df([NOW - 24 * 60 * 60_000]), now_ms=NOW)
|
||||
assert age == pytest.approx(24 * 60 - 5)
|
||||
assert age > 30.0, "il caso che l'allerta deve prendere"
|
||||
|
||||
|
||||
def test_eta_non_negativa_con_barra_nel_futuro():
|
||||
"""Uno skew d'orologio non deve produrre un'eta' negativa che passerebbe ogni soglia."""
|
||||
assert feed_age_minutes(_df([NOW + 60 * 60_000]), now_ms=NOW) == 0.0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad", [None, pd.DataFrame(), pd.DataFrame({"x": [1]})])
|
||||
def test_input_non_interpretabile_da_None_non_zero(bad):
|
||||
"""None = 'non misurata' e va trattata come sospetta; se tornasse 0.0 sembrerebbe fresca."""
|
||||
assert feed_age_minutes(bad, now_ms=NOW) is None
|
||||
|
||||
|
||||
def test_merge_tail_vince_sui_duplicati_e_ringiovanisce_il_feed():
|
||||
"""La coda fresca deve davvero spostare in avanti l'ultima barra: e' il meccanismo che
|
||||
tiene bassa la latenza d'uscita."""
|
||||
base = _df([NOW - 3 * 86_400_000, NOW - 2 * 86_400_000])
|
||||
tail = _df([NOW - 2 * 86_400_000, NOW - 5 * 60_000])
|
||||
m = merge_tail(base, tail)
|
||||
assert len(m) == 3, "il duplicato dev'essere collassato"
|
||||
assert feed_age_minutes(m, now_ms=NOW) == pytest.approx(0.0)
|
||||
assert feed_age_minutes(base, now_ms=NOW) > 30.0
|
||||
|
||||
|
||||
def test_merge_tail_vuota_lascia_il_base_intatto():
|
||||
"""Fetch fallito -> coda vuota -> il feed resta il certificato: e' il fallback silenzioso,
|
||||
e il test lo fissa come comportamento ATTESO (non e' un bug, e' il motivo della misura)."""
|
||||
base = _df([NOW - 86_400_000])
|
||||
assert merge_tail(base, pd.DataFrame(columns=SCHEMA)).equals(base)
|
||||
assert merge_tail(base, None).equals(base)
|
||||
|
||||
|
||||
def test_book_report_espone_la_chiave(monkeypatch):
|
||||
"""La misura deve arrivare fino al report, altrimenti l'esecutore non puo' allertare.
|
||||
Senza live_feed la chiave esiste ma vale None ('non misurata')."""
|
||||
from src.live import book as bk
|
||||
r = bk.book_report(offline=True, equity_override=600.0)
|
||||
assert "skh_feed_age_min" in r
|
||||
assert r["skh_feed_age_min"] is None
|
||||
|
||||
|
||||
def test_soglia_di_config_presente_e_sensata():
|
||||
"""La soglia dev'essere piu' larga della cadenza del cron (orario) e molto piu' stretta
|
||||
di un giorno, sennò non distingue 'fresco' da 'ricaduto sul certificato'."""
|
||||
import json
|
||||
cfg = json.loads((ROOT / "config" / "live.json").read_text())
|
||||
thr = float(cfg["skh_feed_max_age_min"])
|
||||
assert 10.0 <= thr <= 120.0, thr
|
||||
Reference in New Issue
Block a user