"""BOOK DERIBIT-ONLY (TP01 + SKH01) — target NETTO per asset via NETTING SOFTWARE su un solo conto. TP01 e SKH01 tradano lo STESSO strumento (BTC/ETH _USDC-PERPETUAL). Su Deribit esiste UNA sola posizione netta per strumento per conto -> non si possono tenere due gambe separate: si combinano in software in un unico target netto, e si manda UN ordine per asset per raggiungerlo. Formula (preserva il budget di rischio $300/asset, split 75/25; coerente col blend di ritorni deribit_book_sleeves = 0.75*TP01 + 0.25*SKH01): net_target_usd[asset] = clamp( WEIGHT * E * (W_TP01 * tp_frac + W_SKH * skh_sign), ±CAP ) WEIGHT = 0.5 (book 50/50 BTC+ETH, come i due sleeve) tp_frac = target TP01 (causale, >=0, long-flat) da TrendPortfolio.current_target skh_sign = +1 long / -1 short / 0 flat da _skyhook_positions (book 230m) E = equity reale del conto (fallback paper se offline) CAP = max_notional_per_asset_usd (config/live.json, $300) 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. 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). """ from __future__ import annotations import json from pathlib import Path from src.live.deribit import INSTRUMENT, notional_to_amount from src.live.shadow import ASSETS, WEIGHT, shadow_report from src.portfolio.sleeves import _skyhook_positions PROJECT_ROOT = Path(__file__).resolve().parents[2] CONFIG = PROJECT_ROOT / "config" / "live.json" # Pesi del book Deribit-only (vedi src/portfolio/sleeves.deribit_book_sleeves). W_TP01 = 0.75 W_SKH = 0.25 FLAT_USD = 1.0 def _cap() -> float: cfg = json.loads(CONFIG.read_text()) if CONFIG.exists() else {} return float(cfg.get("max_notional_per_asset_usd", 300.0)) def book_net_target(tp_frac: float, skh_sign: int, equity: float, cap: float, weight: float = WEIGHT) -> float: """Target NETTO (USD notional, segno = direzione) di un asset del book. PURA, testabile. Combina la frazione long-flat di TP01 (peso 0.75) e il segno L/S di SKH01 (peso 0.25), clampata al cap per-asset. Vedi formula nel docstring del modulo.""" raw = weight * equity * (W_TP01 * max(tp_frac, 0.0) + W_SKH * float(skh_sign)) return max(-cap, min(cap, raw)) def _skh_sign(state) -> int: if state == "flat" or not isinstance(state, dict): return 0 return 1 if state.get("dir") == "LONG" else -1 def build_book_order(instrument: str, net_target_usd: float, current_pos_usd: float, mark: float | None, min_usd: float = 5.0) -> dict | None: """COSTRUISCE (non invia) l'ordine per portare la posizione al target NETTO. Ritorna dict-ordine o None se sotto-soglia. Gestisce long/short e i flip: - |delta| < min_usd -> None (già a target); - target ~0 -> CLOSE (reduce_only, esce sempre); - flip di segno (long<->short) -> needs_flip=True (close + open, gestito dall'executor); - stesso segno, |target|<|cur| -> REDUCE (reduce_only); - altrimenti -> OPEN/INCREASE (buy se delta>0, sell se delta<0).""" delta = net_target_usd - current_pos_usd if abs(delta) < min_usd: return None side = "buy" if delta > 0 else "sell" is_close = abs(net_target_usd) < FLAT_USD and abs(current_pos_usd) > FLAT_USD needs_flip = (current_pos_usd > FLAT_USD and net_target_usd < -FLAT_USD) or \ (current_pos_usd < -FLAT_USD and net_target_usd > FLAT_USD) same_sign = (net_target_usd > 0) == (current_pos_usd > 0) is_reduce = is_close or (same_sign and abs(net_target_usd) < abs(current_pos_usd) and not needs_flip) amount = notional_to_amount(instrument, abs(delta), price=mark) if amount == 0.0 and not is_close: return None return dict( instrument=instrument, side=side, amount=amount, type="market", reduce_only=bool(is_reduce or is_close), needs_flip=bool(needs_flip), is_close=bool(is_close), net_target=round(net_target_usd, 2), current=round(current_pos_usd, 2), delta=round(delta, 2), ) def book_report(offline: bool = False, equity_override: float | None = None, live_feed: bool = False) -> dict: """Stato completo del book (TP01+SKH01) NETTO, per asset. NON invia nulla. Serializzabile. Riusa shadow_report (target TP01 + conto/posizioni/mark reali) e ci somma il segno di SKH01. live_feed: se True usa il feed 5m fresco effimero per SKH01 (src/live/livefeed.fresh_5m) -> segnale 230m all'ultima barra chiusa, per l'esecuzione reale. Default False (feed certificato, deterministico: dashboard/test). TP01 resta sul certificato (è giornaliero).""" sh = shadow_report(offline=offline, equity_override=equity_override) cap = _cap() equity = sh["equity"] load5m = None if live_feed: from src.live.livefeed import fresh_5m load5m = fresh_5m try: skh = _skyhook_positions(load5m=load5m) except Exception as e: # non bloccare il report se il feed SKH fallisce skh = {a: "flat" for a in ASSETS} sh = {**sh, "skh_error": f"{type(e).__name__}: {e}"} assets, orders = [], [] for a_rec in sh["assets"]: a = a_rec["asset"] inst = INSTRUMENT[a] tp_frac = float(a_rec["target"]) st = skh.get(a, "flat") sign = _skh_sign(st) net = book_net_target(tp_frac, sign, equity, cap) cur = float(a_rec["position_usd"]) mark = a_rec["mark"] order = build_book_order(inst, net, cur, mark, min_usd=5.0) if order: orders.append(order) assets.append(dict( asset=a, instrument=inst, tp_frac=round(tp_frac, 4), skh_sign=sign, skh_state=(st if st == "flat" else {k: st[k] for k in ("dir", "entry", "sl", "tp", "bars_in", "max_bars") if k in st}), net_target=round(net, 2), position_usd=round(cur, 2), mark=mark, mark_src=a_rec.get("mark_src"), order=order, )) return dict( last_data=sh["last_data"], online=sh["online"], real_equity=sh["real_equity"], equity=equity, eq_basis=sh["eq_basis"], cap_per_asset=cap, weights=dict(TP01=W_TP01, SKH01=W_SKH), assets=assets, orders=orders, flat=all(abs(x["net_target"]) < FLAT_USD for x in assets), )