"""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 numpy as np import pandas as pd from src.backtest.harness import load from src.live.deribit import DISASTER_LABEL, 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 # Fallback LEGACY: lo scrittore (paper_trend.py) e' RITIRATO 2026-07-17 (-> Old/). Il file non esiste # piu' -> guardia .exists() sotto degrada a FALLBACK_CAPITAL (=2000) e paper_pos=None (parita' disattiva). PAPER_STATE = PROJECT_ROOT / "data" / "paper_trend" / "state.json" def tp01_trades(limit: int = 15) -> list[dict]: """Lista dei TRADE di TP01 = cambi di posizione (ENTRY long / EXIT flat) dedotti dal segnale causale `target_series` sui dati certificati. Account-independent (gira anche offline nel container). Si ricalcola a ogni render -> include sia lo storico sia i trade forward man mano che arrivano. Ritorna gli ultimi `limit` (piu' recenti primi).""" tp = TrendPortfolio(**CANONICAL) out: list[dict] = [] for a in ASSETS: df = resample_1d(load(a, "1h")) c = df["close"].to_numpy(dtype=float) dt = pd.to_datetime(df["datetime"]).to_numpy() tgt = tp.target_series(df) prev = 0.0 for i in range(len(tgt)): if np.sign(tgt[i]) != np.sign(prev): out.append(dict( ts=int(pd.Timestamp(dt[i]).value // 10**6), date=str(pd.Timestamp(dt[i]).date()), asset=a, action="ENTRY" if tgt[i] != 0 else "EXIT", from_pos=round(float(prev), 3), to_pos=round(float(tgt[i]), 3), price=round(float(c[i]), 1), )) prev = tgt[i] out.sort(key=lambda r: r["ts"], reverse=True) return out[:limit] 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", None pos, note, failed = {}, "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" failed.append(f"{a} ({type(e).__name__}: {e})") # pos_error != None SOLO se ONLINE ma la read ha lanciato: la posizione e' IGNOTA (assunta flat) -> # l'esecutore NON deve operare a cieco. L'offline (client=None) e' gestito a parte dal gate 'online'. err = ("posizione non leggibile, assunta FLAT: " + ", ".join(failed)) if failed else None return pos, note, err 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" # real_eq=None qui -> shadow_report ripiega su paper_cap e alza eq_fallback (diagnostica B): # segnalato a book_execute (alert), non blocca (l'hard-cap $300/asset limita il downside). 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, pos_error = _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)") # Opzione B (diagnostica): ONLINE ma equity reale non leggibile -> sizing su paper_cap invece del # conto reale. NON blocca (l'hard-cap $300/asset limita il downside), ma va SEGNALATO. eq_fallback = (f"equity reale non leggibile ({eq_src}) -> sizing su paper_cap ${paper_cap:,.0f} " f"invece del conto reale") if (equity_override is None and client is not None and not real_eq) else None assets, orders = [], [] for a in ASSETS: inst = INSTRUMENT[a] order = build_rebalance_order(inst, targets[a], WEIGHT, equity, positions[a], price=marks[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, )) live_trades = [] if client is not None: for a in ASSETS: try: for tr in client.trade_history(INSTRUMENT[a], limit=8): live_trades.append(dict( ts=int(tr.get("timestamp") or 0), instrument=INSTRUMENT[a], direction=(tr.get("direction") or "").upper(), amount=float(tr.get("amount") or 0), price=float(tr.get("price") or 0), fee=float(tr.get("fee") or 0))) except Exception: pass live_trades.sort(key=lambda r: r["ts"], reverse=True) live_trades = live_trades[:12] disaster_sls = [] if client is not None: for a in ASSETS: try: for o in client.open_orders(INSTRUMENT[a]): if (o.get("label") or "") == DISASTER_LABEL: disaster_sls.append(dict( asset=a, instrument=INSTRUMENT[a], amount=float(o.get("amount") or 0), stop=float(o.get("trigger_price") or o.get("stop_price") or o.get("price") or 0))) except Exception: pass 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, pos_error=pos_error, eq_fallback=eq_fallback, assets=assets, orders=orders, live_trades=live_trades, disaster_sls=disaster_sls, flat=all(abs(targets[a]) < 1e-9 for a in ASSETS), paper_aligned=(paper_ts == last_ts), )