feat(live): arma il BOOK DERIBIT (TP01+SKH01 nettati in software)
Estende l'esecuzione live da TP01-only al book Deribit-only completo. TP01 e SKH01 tradano lo STESSO strumento (una sola posizione netta per conto su Deribit) -> netting in software: un solo ordine/asset verso il target netto. - src/live/book.py: target NETTO per asset = clamp(0.5*E*(0.75*tp_frac + 0.25*skh_sign), ±cap). Riusa current_target (TP01, causale) + _skyhook_positions (segno L/S, book 230m) + conto reale. - src/live/execution.py: rebalance_signed() — reconcile CON SEGNO (long/short, flip via close+open, reduce reduce_only). La close resta sempre permessa (si esce da qualunque posizione). - src/live/livefeed.py: fresh_5m() — feed 5m certificato + coda recente EFFIMERA da Deribit pubblico (stesso simbolo inverse, in-memory, NON scrive su disco -> dati certificati intatti; fallback al certificato su errore). Solo SKH01 ne ha bisogno (e' a 230m); TP01 e' giornaliero. - scripts/live/book_execute.py: executor doppio-gate (config + --execute), disaster-SL on-book sulla posizione netta, log in data/live/book_executions.jsonl. Feed SKH fresco (live_feed=True). - scripts/cron_book.sh + crontab ORARIO: book idempotente ogni ora (riconcilia al netto corrente); rimossa la riga live_execute.py (TP01-only) dal cron daily per non far collidere i due. - config/live.json: ARMATO (execution_enabled=true). cap/asset $300 split 75/25, disaster-SL -30%. Tutto flat all'arming -> nessun ordine finche' un segnale non arma. - tests/test_book_live.py: 20 test (sizing 75/25, cap, flip/close/reduce, parita' pesi backtest, gate-safety, reconcile con trader fittizio, merge/dedup feed + fallback, loader onorato). 76/76 pass. CAVEAT: exit SKH SOFTWARE (latenza fino a fine barra 230m; solo disaster-SL on-book); TP01 scende a peso 0.75 (max $225/asset); SKH01 resta research-grade (equity daily-step, margine DD ETH sottile). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
"""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 fresh_5m(asset: str, lookback_days: int = 12) -> pd.DataFrame:
|
||||
"""Feed 5m certificato + coda recente effimera (in-memory). Fallback al certificato su errore."""
|
||||
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)
|
||||
Reference in New Issue
Block a user