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
|
||||
esecuzione (non passa `weights_tilt_null`) ma piccolo, da pesare contro ordini orfani/doppio fill.
|
||||
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
|
||||
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
|
||||
|
||||
+3
-1
@@ -7,5 +7,7 @@
|
||||
"min_order_usd": 5,
|
||||
"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.",
|
||||
"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("disaster_sl_pct", 0.30)
|
||||
cfg.setdefault("max_data_age_days", 2.0)
|
||||
cfg.setdefault("skh_feed_max_age_min", 30.0)
|
||||
return cfg
|
||||
|
||||
|
||||
@@ -92,6 +93,29 @@ def _run():
|
||||
print(f" ⚠️ SKH FEED ERRORE (SKH forzato flat!): {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"]:
|
||||
print(" conto non leggibile (offline) -> stop, non eseguo a cieco.")
|
||||
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])
|
||||
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)
|
||||
exit_idx = min(i + max_bars, n - 1)
|
||||
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"]
|
||||
|
||||
|
||||
MODES = ("canonical", "hourly", "onbook_tp", "onbook")
|
||||
MODES = ("canonical", "hourly", "fastdetect", "onbook_tp", "onbook")
|
||||
LABEL = {"canonical": "canonical (backtest, fill-al-livello)",
|
||||
"hourly": "hourly (PATH LIVE DI OGGI)",
|
||||
"fastdetect": "fastdetect (rilevazione INTRA-BARRA, exit a mercato)",
|
||||
"onbook_tp": "onbook_tp (solo TP a limite)",
|
||||
"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).
|
||||
print("\n differenze APPAIATE per-offset (mediana [min, max] sui 23 offset):")
|
||||
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]
|
||||
d2 = A[m][:, 2] - A["hourly"][:, 2]
|
||||
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 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?")
|
||||
for m in ("onbook_tp", "onbook"):
|
||||
for m in ("fastdetect", "onbook_tp", "onbook"):
|
||||
nf = int((A[m][:, 0] - A["hourly"][:, 0] > 0).sum())
|
||||
nh = int((A[m][:, 2] - A["hourly"][:, 2] > 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 +
|
||||
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
@@ -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:
|
||||
|
||||
@@ -239,7 +239,23 @@ def _skyhook_returns() -> pd.Series:
|
||||
def _skyhook_positions(load5m=None) -> dict:
|
||||
"""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
|
||||
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,
|
||||
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