Files
PythagorasGoal/tests/test_skh_feed_freshness.py
T
Adriano Dal Pastro 031b71bf54 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>
2026-07-25 20:20:01 +00:00

97 lines
4.1 KiB
Python

"""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