db738bce3b
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>
141 lines
6.9 KiB
Python
141 lines
6.9 KiB
Python
"""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),
|
|
)
|