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:
@@ -578,6 +578,37 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
|
|||||||
dell'operatore): TP come limit resting reduce-only, SL strategico NON on-book.** E' un cambio di
|
dell'operatore): TP come limit resting reduce-only, SL strategico NON on-book.** E' un cambio di
|
||||||
esecuzione (non passa `weights_tilt_null`) ma piccolo, da pesare contro ordini orfani/doppio fill.
|
esecuzione (non passa `weights_tilt_null`) ma piccolo, da pesare contro ordini orfani/doppio fill.
|
||||||
Il disaster-SL −30% on-book resta com'e'.
|
Il disaster-SL −30% on-book resta com'e'.
|
||||||
|
❌ **T1 NON IMPLEMENTATO — e il perche' e' una CORREZIONE DI MODELLO** (2026-07-26, diario
|
||||||
|
`2026-07-26-t1-esecuzione-skh-live.md`). Leggendo il codice di produzione: TP01 e SKH01 tradano
|
||||||
|
lo **stesso strumento** con **una sola posizione netta** Deribit → un ordine on-book al livello di
|
||||||
|
SKH chiuderebbe anche quota TP01. Misurati i 2 ostacoli: (A) segno compatibile nel **97%** dei
|
||||||
|
trade che escono in TP (long 100%, short 94%) = risolvibile; (B) divergenza modello/live che
|
||||||
|
sembrava bloccante (ritardo mediano 115 min, **70%** dei TP con ≥1 cron dentro la finestra).
|
||||||
|
⚠️ **MA (B) NON ESISTE: `resample_5m` NON scarta il bin 230m in corso** (verificato: 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" /
|
||||||
|
"latenza fino alla chiusura della barra 230m" erano **FALSI** e hanno guidato 3 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. → non fatto, per misura, non per
|
||||||
|
difficolta'.
|
||||||
|
✅ **CABLATO INVECE il problema vero trovato per strada:** `fresh_5m` **fallisce in SILENZIO**
|
||||||
|
(fallback al feed certificato, rigenerato 1x/giorno) → la latenza d'uscita di SKH01 passa da ~1h
|
||||||
|
a **~1 giorno** senza che nulla lo segnali (conto online, posizione leggibile, e il gate di
|
||||||
|
staleness guarda il feed di TP01: **nessun controllo esistente scatta**). Stesso schema del
|
||||||
|
feed-freeze del 14/07, su un altro feed. Aggiunti: `livefeed.feed_age_minutes` (pura, eta' dalla
|
||||||
|
CHIUSURA della barra, `None`=non misurata, clamp sugli skew), `book_report.skh_feed_age_min`
|
||||||
|
(max fra gli asset), allerta Telegram in `book_execute` sopra `skh_feed_max_age_min`=**30 min**.
|
||||||
|
**Scelta dichiarata: ALLERTA, NON blocca** — bloccare fermerebbe anche TP01 (nettato sullo stesso
|
||||||
|
strumento) per un guasto di rete, e forzare SKH flat chiuderebbe posizioni buone su un glitch.
|
||||||
|
Test `tests/test_skh_feed_freshness.py` (11 casi). Strategia/pesi/cadenza INVARIATI.
|
||||||
|
⚠️ **FOLLOW-UP DICHIARATO, non risolto:** la stessa barra parziale tocca anche gli **INGRESSI** —
|
||||||
|
`ent[n-1]` puo' venire da un breakout non confermato a fine barra → se evapora, il book apre e
|
||||||
|
richiude. Su 112 bin campionati: disaccordo parziale-vs-completa in **1 (~1%)**, ma con sole 3
|
||||||
|
entry nel campione **la taglia non e' stimabile**. A differenza delle uscite qui il live DIVERGE
|
||||||
|
dal backtest in modo sostanziale (entra su un segnale che il backtest non vedrebbe): sistemarlo e'
|
||||||
|
un cambio di STRATEGIA, non di strumentazione → serve una misura dedicata prima di decidere il verso.
|
||||||
(2) ✅ **T2 — DVOLSPREAD ESCE DAL LIMBO** (era fermo dal 21/06, unico sopravvissuto del marginal
|
(2) ✅ **T2 — DVOLSPREAD ESCE DAL LIMBO** (era fermo dal 21/06, unico sopravvissuto del marginal
|
||||||
scorer indurito, mai ripreso: non nel book, non in monitor, non rifiutato). Passato ai due gate
|
scorer indurito, mai ripreso: non nel book, non in monitor, non rifiutato). Passato ai due gate
|
||||||
che nel giugno NON esistevano. ⚠️ La griglia dichiarata dall'agente ("72 celle") ne contiene
|
che nel giugno NON esistevano. ⚠️ La griglia dichiarata dall'agente ("72 celle") ne contiene
|
||||||
|
|||||||
+3
-1
@@ -7,5 +7,7 @@
|
|||||||
"min_order_usd": 5,
|
"min_order_usd": 5,
|
||||||
"disaster_sl_pct": 0.3,
|
"disaster_sl_pct": 0.3,
|
||||||
"_nota_stale": "Staleness-gate (2026-07-25): se l'ultima barra del feed certificato e' piu' vecchia di max_data_age_days, book_execute NON invia ordini e allerta su Telegram. Il 2026-07-14 il book compro' ETH con il feed fermo da 6 giorni (conto online e posizione leggibile -> gli altri due gate non scattavano). Follow-up raccomandato nel diario 2026-07-15-feed-freeze, ora cablato.",
|
"_nota_stale": "Staleness-gate (2026-07-25): se l'ultima barra del feed certificato e' piu' vecchia di max_data_age_days, book_execute NON invia ordini e allerta su Telegram. Il 2026-07-14 il book compro' ETH con il feed fermo da 6 giorni (conto online e posizione leggibile -> gli altri due gate non scattavano). Follow-up raccomandato nel diario 2026-07-15-feed-freeze, ora cablato.",
|
||||||
"max_data_age_days": 2
|
"max_data_age_days": 2,
|
||||||
|
"_nota_skh_feed": "Freschezza del feed 5m usato per il segnale SKH01 (2026-07-26). fresh_5m ricade sul feed certificato IN SILENZIO se il fetch pubblico Deribit fallisce, e il certificato si rigenera 1x/giorno: senza controllo la latenza d'uscita di SKH01 passa da ~1h a ~1 giorno senza segnalazione. Sopra soglia book_execute ALLERTA e NON blocca (bloccare fermerebbe anche TP01, nettato sullo stesso strumento, per un guasto di rete). Diario 2026-07-26-t1-esecuzione-skh-live.md.",
|
||||||
|
"skh_feed_max_age_min": 30
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -44,6 +44,7 @@ def load_config() -> dict:
|
|||||||
cfg.setdefault("min_order_usd", 5.0)
|
cfg.setdefault("min_order_usd", 5.0)
|
||||||
cfg.setdefault("disaster_sl_pct", 0.30)
|
cfg.setdefault("disaster_sl_pct", 0.30)
|
||||||
cfg.setdefault("max_data_age_days", 2.0)
|
cfg.setdefault("max_data_age_days", 2.0)
|
||||||
|
cfg.setdefault("skh_feed_max_age_min", 30.0)
|
||||||
return cfg
|
return cfg
|
||||||
|
|
||||||
|
|
||||||
@@ -92,6 +93,29 @@ def _run():
|
|||||||
print(f" ⚠️ SKH FEED ERRORE (SKH forzato flat!): {r['skh_error']}")
|
print(f" ⚠️ SKH FEED ERRORE (SKH forzato flat!): {r['skh_error']}")
|
||||||
notify("⚠️ BOOK — SKH feed fallito (sleeve forzato flat)", {"error": r["skh_error"]})
|
notify("⚠️ BOOK — SKH feed fallito (sleeve forzato flat)", {"error": r["skh_error"]})
|
||||||
|
|
||||||
|
# --- FRESCHEZZA del feed 5m usato per il segnale SKH (2026-07-26) --------------------------
|
||||||
|
# `fresh_5m` ricade sul feed certificato IN SILENZIO se il fetch pubblico Deribit fallisce, e
|
||||||
|
# il certificato lo ricostruisce il cron una volta al giorno. Non solleva, non logga: senza
|
||||||
|
# questo controllo la latenza d'uscita di SKH01 passa da ~1h a ~1 GIORNO senza che nulla lo
|
||||||
|
# dica. E' proprio la latenza d'uscita dove vive la qualita' del path live (misura 2026-07-26).
|
||||||
|
# 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 flat chiuderebbe
|
||||||
|
# posizioni buone su un glitch. La decisione resta all'operatore.
|
||||||
|
skh_age = r.get("skh_feed_age_min")
|
||||||
|
max_skh_age = float(cfg.get("skh_feed_max_age_min", 30.0))
|
||||||
|
if skh_age is None:
|
||||||
|
print(" ⚠️ freschezza feed SKH NON MISURATA (segnale su feed certificato?)")
|
||||||
|
elif skh_age > max_skh_age:
|
||||||
|
print(f" ⚠️ FEED SKH STANTIO: ultima barra 5m di {skh_age:.0f} min fa "
|
||||||
|
f"(soglia {max_skh_age:.0f}) -> le uscite SKH sono in ritardo, NON blocco.")
|
||||||
|
if do_execute:
|
||||||
|
notify("⚠️ BOOK — feed SKH stantio (uscite in ritardo)",
|
||||||
|
{"eta_min": round(skh_age), "soglia_min": round(max_skh_age),
|
||||||
|
"effetto": "SL/TP di SKH01 rilevati in ritardo",
|
||||||
|
"nota": "fresh_5m e' ricaduto sul feed certificato (fetch pubblico KO)"})
|
||||||
|
else:
|
||||||
|
print(f" feed SKH : fresco ({skh_age:.0f} min)")
|
||||||
|
|
||||||
if not r["online"]:
|
if not r["online"]:
|
||||||
print(" conto non leggibile (offline) -> stop, non eseguo a cieco.")
|
print(" conto non leggibile (offline) -> stop, non eseguo a cieco.")
|
||||||
if do_execute:
|
if do_execute:
|
||||||
|
|||||||
@@ -131,6 +131,50 @@ def sim_equity_exec(ltf: pd.DataFrame, ent: list, mode: str,
|
|||||||
entry = c[i] if mode == "canonical" else px_hour(close_ts[i])
|
entry = c[i] if mode == "canonical" else px_hour(close_ts[i])
|
||||||
fee_entry = FEE_TAKER_SIDE
|
fee_entry = FEE_TAKER_SIDE
|
||||||
|
|
||||||
|
if mode == "fastdetect":
|
||||||
|
# RILEVAZIONE INTRA-BARRA (proposta per il live, 2026-07-26): il livello SL/TP e'
|
||||||
|
# fissato all'INGRESSO da barre chiuse, quindi osservare che il prezzo l'ha gia'
|
||||||
|
# toccato NON e' look-ahead — e' cio' che fa qualunque stop reale. Oggi invece
|
||||||
|
# `_skyhook_positions` guarda solo barre 230m CHIUSE, e il tocco resta invisibile
|
||||||
|
# per una mediana di 115 minuti (p90 210).
|
||||||
|
# Qui: si trova il tocco a 5m, e si esegue al PROSSIMO multiplo orario dopo il tocco
|
||||||
|
# (non al livello: resta un'uscita a mercato del cron).
|
||||||
|
t_end_bar = int(ltf["timestamp"].values[min(i + max_bars, n - 1)]) + MS_LTF
|
||||||
|
a5 = int(np.searchsorted(ts5_close, close_ts[i], side="left"))
|
||||||
|
b5 = int(np.searchsorted(ts5_close, t_end_bar, side="right"))
|
||||||
|
hit_j = None
|
||||||
|
for j5 in range(a5, min(b5 + 1, len(c5))):
|
||||||
|
hs = (l5[j5] <= sl) if direction == 1 else (h5[j5] >= sl)
|
||||||
|
ht = (h5[j5] >= tp) if direction == 1 else (l5[j5] <= tp)
|
||||||
|
if hs or ht:
|
||||||
|
hit_j = j5
|
||||||
|
break
|
||||||
|
# la barra 230m in cui cade il tocco resta quella che chiude il trade (stesse
|
||||||
|
# boundaries del canonico: cambia SOLO il momento dell'esecuzione)
|
||||||
|
for j in range(i + 1, min(i + max_bars + 1, n)):
|
||||||
|
hit_sl = sl is not None and (
|
||||||
|
(direction == 1 and lo[j] <= sl) or (direction == -1 and h[j] >= sl))
|
||||||
|
hit_tp = tp is not None and (
|
||||||
|
(direction == 1 and h[j] >= tp) or (direction == -1 and lo[j] <= tp))
|
||||||
|
if hit_sl or hit_tp:
|
||||||
|
exit_idx, kind = j, ("sl" if hit_sl else "tp")
|
||||||
|
break
|
||||||
|
exit_idx = j
|
||||||
|
kind = "time"
|
||||||
|
else:
|
||||||
|
exit_idx = min(i + max_bars, n - 1)
|
||||||
|
kind = "time"
|
||||||
|
stats[kind] += 1
|
||||||
|
if hit_j is not None and kind in ("sl", "tp"):
|
||||||
|
exit_price = px_hour(int(ts5_close[hit_j])) # primo cron DOPO il tocco
|
||||||
|
else:
|
||||||
|
exit_price = px_hour(close_ts[exit_idx])
|
||||||
|
gross = (exit_price - entry) / entry * direction
|
||||||
|
capital = max(capital + capital * (gross - 2 * FEE_TAKER_SIDE), 1.0)
|
||||||
|
equity[i:exit_idx + 1] = capital
|
||||||
|
busy_until = exit_idx
|
||||||
|
continue
|
||||||
|
|
||||||
# --- detection identica al canonico (SL prioritario)
|
# --- detection identica al canonico (SL prioritario)
|
||||||
exit_idx = min(i + max_bars, n - 1)
|
exit_idx = min(i + max_bars, n - 1)
|
||||||
exit_lvl = c[exit_idx]
|
exit_lvl = c[exit_idx]
|
||||||
@@ -218,9 +262,10 @@ def sh3(s: pd.Series) -> tuple[float, float, float, float]:
|
|||||||
return m_all["sharpe"], m_is["sharpe"], m_ho["sharpe"], m_all["maxdd"]
|
return m_all["sharpe"], m_is["sharpe"], m_ho["sharpe"], m_all["maxdd"]
|
||||||
|
|
||||||
|
|
||||||
MODES = ("canonical", "hourly", "onbook_tp", "onbook")
|
MODES = ("canonical", "hourly", "fastdetect", "onbook_tp", "onbook")
|
||||||
LABEL = {"canonical": "canonical (backtest, fill-al-livello)",
|
LABEL = {"canonical": "canonical (backtest, fill-al-livello)",
|
||||||
"hourly": "hourly (PATH LIVE DI OGGI)",
|
"hourly": "hourly (PATH LIVE DI OGGI)",
|
||||||
|
"fastdetect": "fastdetect (rilevazione INTRA-BARRA, exit a mercato)",
|
||||||
"onbook_tp": "onbook_tp (solo TP a limite)",
|
"onbook_tp": "onbook_tp (solo TP a limite)",
|
||||||
"onbook": "onbook (TP limite + SL stop-market)"}
|
"onbook": "onbook (TP limite + SL stop-market)"}
|
||||||
|
|
||||||
@@ -297,7 +342,7 @@ def main() -> None:
|
|||||||
# per-offset, non la differenza delle mediane (che confronta offset diversi fra loro).
|
# per-offset, non la differenza delle mediane (che confronta offset diversi fra loro).
|
||||||
print("\n differenze APPAIATE per-offset (mediana [min, max] sui 23 offset):")
|
print("\n differenze APPAIATE per-offset (mediana [min, max] sui 23 offset):")
|
||||||
print(f" {'':>40} {'dFULL':>22} {'dHOLD':>22} {'dDD':>20}")
|
print(f" {'':>40} {'dFULL':>22} {'dHOLD':>22} {'dDD':>20}")
|
||||||
for m in ("canonical", "onbook_tp", "onbook"):
|
for m in ("canonical", "fastdetect", "onbook_tp", "onbook"):
|
||||||
d0 = A[m][:, 0] - A["hourly"][:, 0]
|
d0 = A[m][:, 0] - A["hourly"][:, 0]
|
||||||
d2 = A[m][:, 2] - A["hourly"][:, 2]
|
d2 = A[m][:, 2] - A["hourly"][:, 2]
|
||||||
d3 = A[m][:, 3] - A["hourly"][:, 3]
|
d3 = A[m][:, 3] - A["hourly"][:, 3]
|
||||||
@@ -314,7 +359,7 @@ def main() -> None:
|
|||||||
print(f" recuperato dal solo TP a limite: {dtp:+.3f} = {dtp/dmax:>5.0%}")
|
print(f" recuperato dal solo TP a limite: {dtp:+.3f} = {dtp/dmax:>5.0%}")
|
||||||
print(f" recuperato da TP + SL on-book : {dob:+.3f} = {dob/dmax:>5.0%}")
|
print(f" recuperato da TP + SL on-book : {dob:+.3f} = {dob/dmax:>5.0%}")
|
||||||
print("\n in quanti dei 23 offset la modalita' MIGLIORA il path live di oggi?")
|
print("\n in quanti dei 23 offset la modalita' MIGLIORA il path live di oggi?")
|
||||||
for m in ("onbook_tp", "onbook"):
|
for m in ("fastdetect", "onbook_tp", "onbook"):
|
||||||
nf = int((A[m][:, 0] - A["hourly"][:, 0] > 0).sum())
|
nf = int((A[m][:, 0] - A["hourly"][:, 0] > 0).sum())
|
||||||
nh = int((A[m][:, 2] - A["hourly"][:, 2] > 0).sum())
|
nh = int((A[m][:, 2] - A["hourly"][:, 2] > 0).sum())
|
||||||
nd = int((A[m][:, 3] - A["hourly"][:, 3] < 0).sum())
|
nd = int((A[m][:, 3] - A["hourly"][:, 3] < 0).sum())
|
||||||
|
|||||||
+29
-5
@@ -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 +
|
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
|
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
|
-> il target netto si aggiorna -> il reconciler chiude la quota SKH. Gli exit sono SOFTWARE (no
|
||||||
(no bracket on-book per SKH, sennò chiuderebbero anche la quota TP01) -> latenza fino alla chiusura
|
bracket on-book per SKH, sennò chiuderebbero anche la quota TP01). Solo il disaster-SL (-30%) resta
|
||||||
della barra 230m corrente. Solo il disaster-SL (-30%) resta on-book, sulla posizione NETTA.
|
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
|
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).
|
(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"]
|
equity = sh["equity"]
|
||||||
cap = _cap(equity=equity, real_equity=sh.get("real_equity"), eq_fallback=sh.get("eq_fallback"))
|
cap = _cap(equity=equity, real_equity=sh.get("real_equity"), eq_fallback=sh.get("eq_fallback"))
|
||||||
load5m = None
|
load5m = None
|
||||||
|
feed_ages: dict[str, float | None] = {}
|
||||||
if live_feed:
|
if live_feed:
|
||||||
from src.live.livefeed import fresh_5m
|
from src.live.livefeed import feed_age_minutes, fresh_5m
|
||||||
load5m = 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
|
skh_error = None
|
||||||
try:
|
try:
|
||||||
skh = _skyhook_positions(load5m=load5m)
|
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),
|
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"),
|
assets=assets, orders=orders, skh_error=skh_error, pos_error=sh.get("pos_error"),
|
||||||
eq_fallback=sh.get("eq_fallback"),
|
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),
|
flat=all(abs(x["net_target"]) < FLAT_USD for x in assets),
|
||||||
)
|
)
|
||||||
|
|||||||
+24
-1
@@ -70,8 +70,31 @@ def merge_tail(base: pd.DataFrame, tail: pd.DataFrame) -> pd.DataFrame:
|
|||||||
return merged
|
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:
|
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")
|
base = load_data(asset, "5m")
|
||||||
sym = DERIBIT_SYMBOL.get(asset)
|
sym = DERIBIT_SYMBOL.get(asset)
|
||||||
if sym is None:
|
if sym is None:
|
||||||
|
|||||||
@@ -239,7 +239,23 @@ def _skyhook_returns() -> pd.Series:
|
|||||||
def _skyhook_positions(load5m=None) -> dict:
|
def _skyhook_positions(load5m=None) -> dict:
|
||||||
"""Stato corrente del book Skyhook per asset (introspezione live): se c'e' un trade APERTO ORA
|
"""Stato corrente del book Skyhook per asset (introspezione live): se c'e' un trade APERTO ORA
|
||||||
-> dir/entry/sl/tp/barre-trascorse; altrimenti 'flat'. Replica la logica non-overlap di
|
-> dir/entry/sl/tp/barre-trascorse; altrimenti 'flat'. Replica la logica non-overlap di
|
||||||
entry+exit (TP/SL/max_bars) fino all'ultima barra 230m chiusa. Causale: usa solo barre chiuse.
|
entry+exit (TP/SL/max_bars).
|
||||||
|
|
||||||
|
⚠️ CORREZIONE 2026-07-26 — qui c'era scritto "fino all'ultima barra 230m CHIUSA. Causale: usa
|
||||||
|
solo barre chiuse". E' FALSO ed e' stato creduto per settimane: `resample_5m` NON scarta il bin
|
||||||
|
230m in corso (verificato: 21 barre 5m su 46 nell'ultimo bin), e il ciclo qui sotto itera fino a
|
||||||
|
`n-1`, quindi valuta gli hit di SL/TP anche sul bin PARZIALE, col suo high/low corrente.
|
||||||
|
Conseguenze, entrambe reali:
|
||||||
|
* USCITE (bene): il tocco del livello e' visto entro il primo cron dopo il tocco (~1h) invece
|
||||||
|
che a fine barra 230m -> il path live e' MIGLIORE di come lo modellavano l'audit 02/07 e il
|
||||||
|
follow-up 24/07 (+0.081 Sharpe FULL di book sulla banda appaiata, 23/23 offset).
|
||||||
|
NON e' look-ahead: il livello e' fissato all'INGRESSO da barre chiuse, e osservare che il
|
||||||
|
prezzo l'ha gia' toccato e' esattamente cio' che fa uno stop reale.
|
||||||
|
* INGRESSI (da tenere d'occhio): anche `ent[n-1]` puo' venire da un bin PARZIALE, cioe' da un
|
||||||
|
breakout non ancora confermato a fine barra -> se il segnale evapora il book apre e richiude.
|
||||||
|
Misurato su 112 bin campionati: disaccordo parziale-vs-completa in 1 (~1%), ma con sole 3
|
||||||
|
entry nel campione la TAGLIA non e' stimabile -> follow-up dichiarato, non risolto.
|
||||||
|
Diario `2026-07-26-t1-esecuzione-skh-live.md`.
|
||||||
|
|
||||||
load5m: callable(asset)->df5 opzionale (per il live: feed certificato + coda fresca effimera,
|
load5m: callable(asset)->df5 opzionale (per il live: feed certificato + coda fresca effimera,
|
||||||
vedi src/live/livefeed.fresh_5m). Default = feed certificato su disco (load_data)."""
|
vedi src/live/livefeed.fresh_5m). Default = feed certificato su disco (load_data)."""
|
||||||
|
|||||||
@@ -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