feat(dashboard): mostra i disaster-SL attivi nella sezione LIVE

Lo shadow espone i bracket disaster-SL aperti (open_orders filtrati per label DISASTER_LABEL,
centralizzata in deribit.py): asset, stop price, size. La sezione LIVE li mostra
("disaster-SL attivi (-30%): ..." o "nessuno (flat)"). Test 28/28.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-06-20 16:05:18 +00:00
parent e5e2d3ec9b
commit 3cba5bb9d0
4 changed files with 24 additions and 7 deletions
+5 -1
View File
@@ -96,8 +96,12 @@ def html():
pos = ", ".join(f"{a['asset']} ${a['position_usd']:,.0f}" for a in sh["assets"]) pos = ", ".join(f"{a['asset']} ${a['position_usd']:,.0f}" for a in sh["assets"])
ordtxt = ("; ".join(f"{o['side'].upper()} {o['amount']:.0f} {o['instrument']}" for o in sh["orders"]) ordtxt = ("; ".join(f"{o['side'].upper()} {o['amount']:.0f} {o['instrument']}" for o in sh["orders"])
if sh.get("orders") else "nessuno (target flat / gia' a target)") if sh.get("orders") else "nessuno (target flat / gia' a target)")
dsl = sh.get("disaster_sls") or []
dsl_txt = (" · ".join(f"{x['asset']} stop <b>${x['stop']:,.0f}</b> ({x['amount']:.4f})" for x in dsl)
if dsl else "nessuno (flat)")
shadow_html = (f"mainnet · sola lettura · conto reale <b>{eq}</b> · pos {pos} · dato {sh['last_data']}<br>" shadow_html = (f"mainnet · sola lettura · conto reale <b>{eq}</b> · pos {pos} · dato {sh['last_data']}<br>"
f"TP01 target: {bits}<br>ordini-che-invierebbe (<b>NON inviati</b>): {ordtxt}") f"TP01 target: {bits}<br>ordini-che-invierebbe (<b>NON inviati</b>): {ordtxt}<br>"
f"🛡️ disaster-SL attivi (30%): {dsl_txt}")
else: else:
shadow_html = (f"conto reale non leggibile dal container (token solo su host) · dato {sh['last_data']}<br>" shadow_html = (f"conto reale non leggibile dal container (token solo su host) · dato {sh['last_data']}<br>"
f"TP01 target: {bits}<br>→ per gli ordini reali: <code>uv run python scripts/live/live_trend.py</code> (host)") f"TP01 target: {bits}<br>→ per gli ordini reali: <code>uv run python scripts/live/live_trend.py</code> (host)")
+1
View File
@@ -32,6 +32,7 @@ _CONTRACT = {
} }
# Il conto reale e' USDC -> mappiamo gli asset sui perp LINEARI USDC (gli unici eseguibili qui). # Il conto reale e' USDC -> mappiamo gli asset sui perp LINEARI USDC (gli unici eseguibili qui).
INSTRUMENT = {"BTC": "BTC_USDC-PERPETUAL", "ETH": "ETH_USDC-PERPETUAL"} INSTRUMENT = {"BTC": "BTC_USDC-PERPETUAL", "ETH": "ETH_USDC-PERPETUAL"}
DISASTER_LABEL = "tp01-disaster" # label dei bracket disaster-SL (per ritrovarli/gestirli/mostrarli)
# ----------------------------- costruzione ordini (pura, testabile, NIENTE rete) ----------------------------- # ----------------------------- costruzione ordini (pura, testabile, NIENTE rete) -----------------------------
+3 -5
View File
@@ -15,7 +15,7 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from src.live.deribit import DeribitRead, disaster_stop_price, notional_to_amount, quantize_price from src.live.deribit import DISASTER_LABEL, DeribitRead, disaster_stop_price, notional_to_amount, quantize_price
# Conto USDC -> perp LINEARE USDC (amount in base-coin). MAX_AMOUNT = TETTO HARD anti-fat-finger # Conto USDC -> perp LINEARE USDC (amount in base-coin). MAX_AMOUNT = TETTO HARD anti-fat-finger
# (~$630/$430 su un conto ~$600): backstop sopra il sizing di TP01, non il sizing operativo (quello # (~$630/$430 su un conto ~$600): backstop sopra il sizing di TP01, non il sizing operativo (quello
@@ -152,15 +152,13 @@ class DeribitTrader(DeribitRead):
def cancel_order(self, order_id: str) -> dict: def cancel_order(self, order_id: str) -> dict:
return self._unwrap(self._post("/mcp-deribit/tools/cancel_order", {"order_id": order_id})) or {} return self._unwrap(self._post("/mcp-deribit/tools/cancel_order", {"order_id": order_id})) or {}
DISASTER_LABEL = "tp01-disaster"
def ensure_disaster_sl(self, instrument: str, sl_pct: float) -> dict: def ensure_disaster_sl(self, instrument: str, sl_pct: float) -> dict:
"""Garantisce UN disaster-SL coerente con la posizione (lifecycle completo, idempotente): """Garantisce UN disaster-SL coerente con la posizione (lifecycle completo, idempotente):
- flat -> cancella eventuali bracket orfani; - flat -> cancella eventuali bracket orfani;
- long -> assicura UN solo STOP_MARKET reduce_only a ~-sl_pct, size = posizione; - long -> assicura UN solo STOP_MARKET reduce_only a ~-sl_pct, size = posizione;
- gia' coerente (1 bracket, amount~=, stop entro 5%) -> lascia com'e' (niente churn/gap).""" - gia' coerente (1 bracket, amount~=, stop entro 5%) -> lascia com'e' (niente churn/gap)."""
pos = self.position_usd(instrument) pos = self.position_usd(instrument)
brackets = [o for o in self.open_orders(instrument) if (o.get("label") or "") == self.DISASTER_LABEL] brackets = [o for o in self.open_orders(instrument) if (o.get("label") or "") == DISASTER_LABEL]
if abs(pos) < FLAT_USD: if abs(pos) < FLAT_USD:
for o in brackets: for o in brackets:
self.cancel_order(o.get("order_id")) self.cancel_order(o.get("order_id"))
@@ -179,7 +177,7 @@ class DeribitTrader(DeribitRead):
for o in brackets: # incoerente o multipli -> ricostruisci UN bracket for o in brackets: # incoerente o multipli -> ricostruisci UN bracket
self.cancel_order(o.get("order_id")) self.cancel_order(o.get("order_id"))
f = self.place_disaster_sl(instrument, "buy" if long else "sell", want_amount, want_stop, f = self.place_disaster_sl(instrument, "buy" if long else "sell", want_amount, want_stop,
label=self.DISASTER_LABEL) label=DISASTER_LABEL)
return {"state": "placed" if f.verified else "place-failed", "stop": want_stop, return {"state": "placed" if f.verified else "place-failed", "stop": want_stop,
"amount": want_amount, "notes": f.notes} "amount": want_amount, "notes": f.notes}
# trade_history / open_orders ereditati da DeribitRead (read-only) # trade_history / open_orders ereditati da DeribitRead (read-only)
+15 -1
View File
@@ -11,7 +11,7 @@ import numpy as np
import pandas as pd import pandas as pd
from src.backtest.harness import load from src.backtest.harness import load
from src.live.deribit import INSTRUMENT, DeribitRead, build_rebalance_order from src.live.deribit import DISASTER_LABEL, INSTRUMENT, DeribitRead, build_rebalance_order
from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio, resample_1d from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio, resample_1d
PROJECT_ROOT = Path(__file__).resolve().parents[2] PROJECT_ROOT = Path(__file__).resolve().parents[2]
@@ -155,11 +155,25 @@ def shadow_report(offline: bool = False, equity_override: float | None = None) -
live_trades.sort(key=lambda r: r["ts"], reverse=True) live_trades.sort(key=lambda r: r["ts"], reverse=True)
live_trades = live_trades[:12] 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( return dict(
last_data=str(pd.Timestamp(last_ts, unit="ms", tz="UTC").date()), last_data=str(pd.Timestamp(last_ts, unit="ms", tz="UTC").date()),
online=(client is not None and marks_src.get("BTC") == "mainnet"), online=(client is not None and marks_src.get("BTC") == "mainnet"),
real_equity=real_eq, equity=equity, eq_basis=eq_basis, real_equity=real_eq, equity=equity, eq_basis=eq_basis,
pos_src=pos_src, assets=assets, orders=orders, live_trades=live_trades, pos_src=pos_src, assets=assets, orders=orders, live_trades=live_trades,
disaster_sls=disaster_sls,
flat=all(abs(targets[a]) < 1e-9 for a in ASSETS), flat=all(abs(targets[a]) < 1e-9 for a in ASSETS),
paper_aligned=(paper_ts == last_ts), paper_aligned=(paper_ts == last_ts),
) )