031b71bf54
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>
107 lines
5.0 KiB
Python
107 lines
5.0 KiB
Python
"""FEED LIVE EFFIMERO per il segnale SKH01 (book a 230m) — NON tocca i dati certificati su disco.
|
||
|
||
SKH01 decide su griglia 230m: per eseguirlo fedelmente il segnale serve fresco all'ultima barra
|
||
chiusa. Il rebuild certificato (rebuild_history.py) gira 1×/giorno e fa un rebuild COMPLETO (pesante):
|
||
girarlo ogni ora sarebbe sbagliato e violerebbe la regola "aggiornare lo storico SOLO con
|
||
rebuild_history + certificare". Quindi qui NON scriviamo su disco: carichiamo il 5m CERTIFICATO e gli
|
||
appendiamo IN MEMORIA una coda recente presa da Deribit PUBBLICO (ccxt, tokenless, STESSO simbolo
|
||
inverse del feed certificato -> prezzi entro ~3 bps). I dati certificati restano la verità su disco;
|
||
questa estensione vive solo nel processo live e per il calcolo del segnale.
|
||
|
||
Robusto ai fallimenti: qualunque errore di rete/fetch -> ritorna il feed certificato invariato (il
|
||
runner degrada a "fermo all'ultimo dato certificato", mai opera a cieco). Solo SKH01 ne ha bisogno:
|
||
TP01 è giornaliero e gira bene sul feed certificato.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import time
|
||
|
||
import pandas as pd
|
||
|
||
from src.data.downloader import load_data
|
||
|
||
# STESSO simbolo del feed certificato (vedi scripts/analysis/rebuild_history.DERIBIT_INSTR):
|
||
# inverse USD perp, storia lunga, entro ~3 bps dal lineare USDC su cui eseguiamo.
|
||
DERIBIT_SYMBOL = {"BTC": "BTC/USD:BTC", "ETH": "ETH/USD:ETH"}
|
||
SCHEMA = ["timestamp", "open", "high", "low", "close", "volume"]
|
||
|
||
|
||
def _fetch_recent_5m(symbol: str, lookback_days: int) -> pd.DataFrame:
|
||
"""Coda recente di 5m da Deribit pubblico (ccxt). Paginazione in avanti. Solo letture pubbliche."""
|
||
import ccxt
|
||
ex = ccxt.deribit({"enableRateLimit": True})
|
||
tf_ms = 5 * 60 * 1000
|
||
since = int((time.time() - lookback_days * 86400) * 1000)
|
||
rows: dict[int, list] = {}
|
||
guard = 0
|
||
while guard < 200:
|
||
guard += 1
|
||
try:
|
||
r = ex.fetch_ohlcv(symbol, "5m", since=since, limit=1000)
|
||
except Exception:
|
||
break
|
||
r = [x for x in r if int(x[0]) >= since]
|
||
if not r:
|
||
break
|
||
for x in r:
|
||
t = int(x[0])
|
||
rows[t] = [t, float(x[1]), float(x[2]), float(x[3]), float(x[4]), float(x[5] or 0)]
|
||
nxt = int(r[-1][0]) + tf_ms
|
||
if nxt <= since:
|
||
break
|
||
since = nxt
|
||
if not rows:
|
||
return pd.DataFrame(columns=SCHEMA)
|
||
return pd.DataFrame(rows.values(), columns=SCHEMA).sort_values("timestamp").reset_index(drop=True)
|
||
|
||
|
||
def merge_tail(base: pd.DataFrame, tail: pd.DataFrame) -> pd.DataFrame:
|
||
"""Fonde la coda fresca sul feed certificato: concat, dedup per timestamp (la coda VINCE sui
|
||
duplicati, ma le barre certificate storiche restano), riordina. Mantiene lo schema di load_data
|
||
(inclusa 'datetime' se presente). PURA, testabile senza rete."""
|
||
if tail is None or tail.empty:
|
||
return base
|
||
cols = [c for c in SCHEMA if c in base.columns]
|
||
t = tail[[c for c in SCHEMA if c in tail.columns]].copy()
|
||
merged = pd.concat([base[cols], t], ignore_index=True)
|
||
merged = merged.drop_duplicates("timestamp", keep="last").sort_values("timestamp").reset_index(drop=True)
|
||
# ricostruisci 'datetime' coerente (build_frames non la usa, ma load_data la espone)
|
||
merged["datetime"] = pd.to_datetime(merged["timestamp"], unit="ms", utc=True)
|
||
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.
|
||
|
||
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:
|
||
return base
|
||
try:
|
||
tail = _fetch_recent_5m(sym, lookback_days)
|
||
except Exception:
|
||
return base
|
||
return merge_tail(base, tail)
|