feat(live): SHADOW MODE TP01 su Deribit mainnet (sola lettura) + dashboard 3-way
Validazione esecuzione di TP01 a RISCHIO ZERO: gira il loop live contro dati/conto/posizioni REALI del mainnet, costruisce gli ordini di ribilancio esatti e li STAMPA invece di inviarli. Niente testnet (e' la causa del reset v2.0.0: feed farlocco) -> shadow su mainnet reale + micro-test a size minima come unica via per il fill (passo successivo). - src/live/deribit.py : client Deribit mainnet SOLA LETTURA (ticker/conto/posizioni via Cerbero MCP) + costruttore ordini deterministico (notional->contratti, step BTC $10/ETH $1, quantizzazione, delta vs posizione). Nessun metodo di trading, by design. - src/live/shadow.py : shadow_report() condiviso CLI+dashboard (niente drift); degrada con grazia se il mainnet non risponde. - scripts/live/live_trend.py : CLI shadow (--no-net offline, --equity override). Verificato su mainnet reale: conto $598.07, posizioni flat, TP01 flat -> 0 ordini, parita' col paper OK. - src/live/dashboard.py : box "Shadow live" + titolo/note al 3-way (TP01+XS01+VRP01). - tests/test_live_shadow.py : 9 test deterministici (quantizzazione, sizing 50/50, entry/exit/None, parita' live==backtest). Suite 26/26. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
"""Stato SHADOW di TP01 (Deribit mainnet, SOLA LETTURA): target causali + conto/posizioni REALI +
|
||||
ordini di ribilancio COSTRUITI (mai inviati). Modulo condiviso da `scripts/live/live_trend.py` (CLI)
|
||||
e dalla dashboard, cosi' i due non divergono. Robusto ai fallimenti di rete: degrada a offline/flat
|
||||
senza sollevare eccezioni (la dashboard non deve crashare se il mainnet non risponde)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from src.backtest.harness import load
|
||||
from src.live.deribit import INSTRUMENT, DeribitRead, build_rebalance_order
|
||||
from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio, resample_1d
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
ASSETS = ["BTC", "ETH"]
|
||||
WEIGHT = 0.5
|
||||
FALLBACK_CAPITAL = 2000.0
|
||||
PAPER_STATE = PROJECT_ROOT / "data" / "paper_trend" / "state.json"
|
||||
|
||||
|
||||
def _safe_client() -> DeribitRead | None:
|
||||
try:
|
||||
return DeribitRead()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _marks(client, dfs):
|
||||
marks, src = {}, {}
|
||||
for a in ASSETS:
|
||||
if client is None:
|
||||
marks[a], src[a] = float(dfs[a]["close"].iloc[-1]), "close certificata"
|
||||
continue
|
||||
try:
|
||||
marks[a], src[a] = client.mark_price(INSTRUMENT[a]), "mainnet"
|
||||
except Exception as e:
|
||||
marks[a], src[a] = float(dfs[a]["close"].iloc[-1]), f"fallback close ({type(e).__name__})"
|
||||
return marks, src
|
||||
|
||||
|
||||
def _positions(client):
|
||||
if client is None:
|
||||
return {a: 0.0 for a in ASSETS}, "offline -> assunto flat"
|
||||
pos, note = {}, "mainnet"
|
||||
for a in ASSETS:
|
||||
try:
|
||||
pos[a] = client.position_usd(INSTRUMENT[a])
|
||||
except Exception as e:
|
||||
pos[a], note = 0.0, f"read fallito ({type(e).__name__}) -> assunto flat"
|
||||
return pos, note
|
||||
|
||||
|
||||
def _equity(client, marks):
|
||||
if client is None:
|
||||
return None, "offline"
|
||||
try:
|
||||
eq = float(client.account_summary("USDC").get("equity") or 0)
|
||||
if eq > 1:
|
||||
return eq, "mainnet USDC"
|
||||
except Exception:
|
||||
pass
|
||||
tot, any_ok = 0.0, False
|
||||
for a in ASSETS:
|
||||
try:
|
||||
eq = float(client.account_summary(a).get("equity") or 0)
|
||||
tot += eq * marks[a]
|
||||
any_ok = True
|
||||
except Exception:
|
||||
pass
|
||||
if any_ok and tot > 1:
|
||||
return tot, "mainnet coin-margined"
|
||||
return None, "conto flat / non finanziato"
|
||||
|
||||
|
||||
def shadow_report(offline: bool = False, equity_override: float | None = None) -> dict:
|
||||
"""Calcola lo stato shadow completo. NON invia nulla. Ritorna un dict serializzabile."""
|
||||
dfs = {a: resample_1d(load(a, "1h")) for a in ASSETS}
|
||||
tp = TrendPortfolio(**CANONICAL)
|
||||
targets = {a: tp.current_target(dfs[a]) for a in ASSETS}
|
||||
last_ts = min(int(dfs[a]["timestamp"].iloc[-1]) for a in ASSETS)
|
||||
|
||||
client = None if offline else _safe_client()
|
||||
marks, marks_src = _marks(client, dfs)
|
||||
positions, pos_src = _positions(client)
|
||||
real_eq, eq_src = _equity(client, marks)
|
||||
|
||||
paper = json.loads(PAPER_STATE.read_text()) if PAPER_STATE.exists() else None
|
||||
paper_cap = float(paper["capital"]) if paper else FALLBACK_CAPITAL
|
||||
paper_pos = paper.get("positions") if paper else None
|
||||
paper_ts = int(paper["last_ts"]) if paper else 0
|
||||
|
||||
equity = equity_override if equity_override is not None else (real_eq if real_eq else paper_cap)
|
||||
eq_basis = ("override" if equity_override is not None
|
||||
else eq_src if real_eq else "paper capital (ipotetico: conto non finanziato)")
|
||||
|
||||
assets, orders = [], []
|
||||
for a in ASSETS:
|
||||
inst = INSTRUMENT[a]
|
||||
order = build_rebalance_order(inst, targets[a], WEIGHT, equity, positions[a])
|
||||
if order:
|
||||
orders.append(order)
|
||||
parity = None
|
||||
if paper_pos is not None:
|
||||
parity = abs(float(paper_pos.get(a, 0.0)) - targets[a]) < 1e-9
|
||||
assets.append(dict(
|
||||
asset=a, instrument=inst, target=targets[a],
|
||||
target_notional=WEIGHT * targets[a] * equity,
|
||||
position_usd=positions[a], mark=marks[a], mark_src=marks_src[a],
|
||||
order=order, paper=(float(paper_pos.get(a, 0.0)) if paper_pos else None), parity=parity,
|
||||
))
|
||||
return dict(
|
||||
last_data=str(pd.Timestamp(last_ts, unit="ms", tz="UTC").date()),
|
||||
online=(client is not None and marks_src.get("BTC") == "mainnet"),
|
||||
real_equity=real_eq, equity=equity, eq_basis=eq_basis,
|
||||
pos_src=pos_src, assets=assets, orders=orders,
|
||||
flat=all(abs(targets[a]) < 1e-9 for a in ASSETS),
|
||||
paper_aligned=(paper_ts == last_ts),
|
||||
)
|
||||
Reference in New Issue
Block a user