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
@@ -0,0 +1,155 @@
# 2026-07-26 — T1 nel live: il fix richiesto NON va fatto, e il perché è una correzione di modello
**Richiesta:** implementare nel live il fix T1 (TP di SKH01 come limit resting on-book).
**Esito: NON implementato**, con i numeri sotto. **Implementato invece** ciò che le misure
sostengono: la sorveglianza della freschezza del feed 5m di SKH01, e la correzione dei docstring
che hanno causato tre settimane di misure sbagliate.
**File toccati (produzione):** `src/live/livefeed.py`, `src/live/book.py`, `src/portfolio/sleeves.py`
(solo docstring), `scripts/live/book_execute.py`, `config/live.json`.
**Test:** `tests/test_skh_feed_freshness.py` (11 casi) · suite **266 verdi**.
**Strategia, pesi, cadenza del cron: INVARIATI.** Nessun ordine inviato.
---
## 1. Cosa ho trovato leggendo il codice di produzione
Il fix T1 nasceva da una simulazione in cui SKH01 esce per conto suo. Il live non funziona così, e
il vincolo era scritto nel codice:
> `book.py`: *«gli exit sono SOFTWARE (no bracket on-book per SKH, sennò chiuderebbero anche la
> quota TP01)»*
TP01 e SKH01 tradano **lo stesso strumento** e Deribit tiene **una sola posizione netta**. Un
ordine on-book al livello di TP di SKH chiuderebbe una fetta della posizione netta, che è per il
75% di TP01. Ho quindi misurato i due ostacoli invece di assumerli:
**(A) Compatibilità di segno — risolvibile.** Un ordine reduce-only può muovere la posizione solo
verso lo zero. Se SKH è short mentre il netto è long (perché TP01 pesa di più), chiudere lo short
significa *aumentare* il netto: non esprimibile. Frequenza sui trade che escono in TP — gli unici
che il fix tocca: **97% compatibili** (SKH long 100%, SKH short 94%). Il 3% si sarebbe potuto
saltare con fallback all'uscita software.
**(B) Divergenza modello/live — sembrava bloccante.** Se il limit riempie a metà barra ma il
modello non "vede" l'uscita, il cron successivo ricalcola il target includendo ancora la quota SKH
e **ricompra ciò che il TP ha appena chiuso**. Misurato assumendo la rilevazione a fine barra 230m:
ritardo mediano **115 min**, p90 **210**, e nel **70% dei TP almeno un cron gira nella finestra**
(mediana 1.9 cron, p90 3.5).
## 2. …e poi ho scoperto che il problema (B) non esiste
Il problema (B) presuppone che il modello veda le uscite solo a barra 230m chiusa. È ciò che dicono
i docstring:
> `sleeves._skyhook_positions`: *«fino all'ultima barra 230m chiusa. Causale: usa solo barre chiuse»*
> `book.py`: *«latenza fino alla chiusura della barra 230m corrente»*
**Sono falsi.** `resample_5m` **non scarta il bin in corso** — aggrega le barre 5m che ci sono
finora — e il ciclo di `_skyhook_positions` itera fino a `n-1`, quindi valuta gli hit di SL/TP
anche sul bin **parziale**, col suo high/low corrente. Verificato sul feed reale: l'ultimo bin 230m
conteneva **21 barre 5m su 46** (105 minuti su 230).
Quindi il live rileva il tocco **entro il primo cron dopo il tocco (~1h)**, non a fine barra. E non
è look-ahead: il livello è fissato all'ingresso da barre chiuse, e osservare che il prezzo l'ha già
toccato è esattamente ciò che fa qualunque stop reale.
### La conseguenza: il path live era modellato male da tre settimane
La lente `hourly` — usata dall'audit del 02/07, dal follow-up del 24/07 e dal mio stesso T1 di
stamattina — assume la rilevazione a fine barra. Il modello fedele è un altro (`fastdetect`:
rilevazione intra-barra a 5m, uscita a mercato al primo cron successivo). Sulla banda appaiata dei
23 offset, **relativo al path live come veniva modellato**:
| modalità | ΔFULL mediana [min,max] | ΔHOLD mediana | offset migliorati (FULL) |
|---|---|---|---|
| **`fastdetect` = il live VERO** | **+0.081** [+0.00,+0.14] | +0.039 | **23/23** |
| `canonical` (tetto teorico del backtest) | +0.054 [0.05,+0.12] | +0.099 | — |
| `onbook_tp` (**il fix richiesto**) | +0.054 [0.03,+0.07] | +0.061 | 19/23 |
| `onbook` (TP+SL on-book) | +0.024 [0.07,+0.11] | +0.031 | 17/23 |
Due letture, entrambe importanti:
1. **Il "0.35 di degrado del path live" è in gran parte un artefatto di modello**, sopra
l'artefatto di ancora già trovato stamattina. Sul FULL il live vero sta **sopra** il canonical.
2. **Il fix richiesto sarebbe un declassamento**: +0.054 contro i +0.081 che il live già fa — e in
cambio si porterebbero in un percorso con soldi veri ordini parziali sul netto, un 3% di casi
non esprimibili, e la riconciliazione fra fill del broker e stato del modello.
**Per questo non l'ho implementato.** Non è una difficoltà tecnica: è che la misura, fatta sulla
lente giusta, dice che peggiorerebbe.
---
## 3. Il problema vero, trovato per strada
`fresh_5m` è il pezzo che tiene bassa la latenza d'uscita. E **fallisce in silenzio**:
```python
try:
tail = _fetch_recent_5m(sym, lookback_days)
except Exception:
return base # ← nessun log, nessun errore, nessuna traccia
```
`_fetch_recent_5m` inghiotte a sua volta le eccezioni (`except Exception: break`). Se il fetch
pubblico Deribit non risponde, il segnale SKH gira sul **feed certificato**, che il cron ricostruisce
**una volta al giorno**. La latenza d'uscita passa da **~1 ora a ~1 giorno**, e niente lo segnala:
il conto è online, la posizione è leggibile, il gate di staleness guarda il feed *di TP01* — nessuno
dei controlli esistenti scatta. È lo stesso schema del feed-freeze del 2026-07-14, su un altro feed.
E conta esattamente quanto il resto di questo lavoro: la latenza d'uscita **è** dove vive la qualità
del path live.
### Cosa ho cablato
- **`livefeed.feed_age_minutes(df5)`** — pura, testabile senza rete: età in minuti dell'ultima barra
5m, misurata dalla sua **chiusura**, con `None` per input non interpretabili (`None` = "non
misurata", da trattare come sospetta: se tornasse `0.0` sembrerebbe fresca) e clamp a zero sugli
skew d'orologio.
- **`book.book_report(live_feed=True)`** avvolge il loader per misurare la freschezza del feed
**effettivamente usato**, e la espone come `skh_feed_age_min` (massimo fra gli asset: se anche uno
solo è stantio, il netto è sospetto).
- **`book_execute`** stampa lo stato e **allerta su Telegram** sopra `skh_feed_max_age_min` (30 min,
in `config/live.json`).
**Scelta dichiarata: allerta, NON blocca.** Bloccare fermerebbe anche il ribilancio di TP01 — sono
nettati sullo stesso strumento — per un guasto di rete; e forzare SKH a flat chiuderebbe posizioni
buone su un glitch. Non ho evidenza su quale politica di blocco sia giusta, quindi la decisione
resta all'operatore, informato.
### Docstring corretti
I due docstring falsi sono stati corretti *sul posto*, con la misura e il perché. Erano loro a
sostenere la catena di ragionamenti sbagliata, ed erano la cosa più economica da riparare.
---
## 4. Follow-up dichiarato, non risolto
La stessa barra parziale che migliora le **uscite** tocca anche gli **ingressi**: `ent[n-1]` può
venire da un bin parziale, cioè da un breakout non ancora confermato a fine barra. Se il segnale
evapora, il book apre e richiude — churn puro.
Misurato su **112 bin campionati**: entry sulla barra completa in 3, sulla parziale in 2,
**disaccordo in 1 (~1% dei bin)**. Ma con sole 3 entry nel campione la **taglia dell'effetto non è
stimabile**: so che esiste, non so quanto pesa. Non l'ho toccato — a differenza delle uscite, qui
il comportamento del live **diverge dal backtest in modo sostanziale** (entra su un segnale che il
backtest non avrebbe visto), e sistemarlo è un cambio di strategia, non di strumentazione.
Serve una misura dedicata prima di decidere in che verso.
---
## 5. Lezioni
1. **Prima di implementare una raccomandazione, leggere il codice che dovrà ospitarla.** T1 era
corretto sul suo modello e inapplicabile sull'architettura reale — e leggendo quel codice è
saltato fuori che il modello stesso era sbagliato. La ricerca del mattino non lo poteva sapere:
simulava lo sleeve, non il book nettato.
2. **Un docstring falso costa più di un bug**, perché nessun test lo cattura e tutti ci ragionano
sopra. Questi due hanno guidato tre analisi (02/07, 24/07, T1) verso una latenza che il codice
non aveva.
3. **Un fallback silenzioso è un guasto silenzioso.** `fresh_5m` fa la cosa giusta (mai operare a
cieco) ma non dice di averla fatta: la scelta giusta è tenere il fallback **e** misurarlo.
4. Terzo caso in due giorni in cui **il numero di partenza era peggio di com'era stato scritto**:
la fortuna d'ancora sul degrado (stamattina), la lente `hourly` (qui), il wick indipendente
(ieri). Ogni volta il difetto stava nella **lente**, non nella strategia.