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,139 @@
|
||||
"""Accesso Deribit MAINNET in SOLA LETTURA (via Cerbero MCP) + costruttore ordini deterministico.
|
||||
|
||||
Serve lo SHADOW MODE di TP01 (`scripts/live/live_trend.py`): legge prezzi / conto / posizioni REALI
|
||||
dal mainnet (token `.env.mainnet`) e costruisce gli ordini di ribilancio **senza inviarli**. Qui NON
|
||||
esiste alcun metodo di trading — by design: l'unica via per piazzare ordini sara' un modulo separato,
|
||||
abilitato esplicitamente, dopo la validazione shadow + micro-test.
|
||||
|
||||
Disciplina del progetto: **testnet FUORI** (feed farlocco, causa del reset v2.0.0). Solo mainnet reale,
|
||||
e in questa fase solo in lettura. I contratti sono ristretti a BTC/ETH-PERPETUAL (inverse: `amount`
|
||||
in USD notional, step verificato su Deribit: BTC $10, ETH $1).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
BASE_URL = os.environ.get("CERBERO_BASE_URL", "https://cerbero-mcp.tielogic.xyz")
|
||||
TIMEOUT = 15
|
||||
|
||||
# Inverse perp: amount = USD notional, step in USD. settle = base-coin (per get_positions/fee).
|
||||
_CONTRACT = {
|
||||
"BTC-PERPETUAL": {"min": 10.0, "step": 10.0, "tick": 0.5, "settle": "BTC"},
|
||||
"ETH-PERPETUAL": {"min": 1.0, "step": 1.0, "tick": 0.05, "settle": "ETH"},
|
||||
}
|
||||
INSTRUMENT = {"BTC": "BTC-PERPETUAL", "ETH": "ETH-PERPETUAL"}
|
||||
|
||||
|
||||
# ----------------------------- costruzione ordini (pura, testabile, NIENTE rete) -----------------------------
|
||||
|
||||
def _quantize_step(value: float, step: float, mn: float) -> float:
|
||||
"""Arrotonda al multiplo di `step` (Decimal, niente artefatti float), clampa al minimo."""
|
||||
n = round(value / step)
|
||||
return float(max(n * Decimal(str(step)), Decimal(str(mn))))
|
||||
|
||||
|
||||
def notional_to_amount(instrument: str, notional_usd: float) -> float:
|
||||
"""USD notional -> `amount` Deribit (inverse: amount in USD), arrotondato allo step e clampato
|
||||
al minimo. Ritorna 0.0 se |notional| < mezzo step (sotto-soglia: niente ordine)."""
|
||||
spec = _CONTRACT[instrument]
|
||||
step, mn = spec["step"], spec["min"]
|
||||
if abs(notional_usd) < step / 2:
|
||||
return 0.0
|
||||
return _quantize_step(abs(notional_usd), step, mn)
|
||||
|
||||
|
||||
def target_notional_usd(target_fraction: float, weight: float, equity_usd: float) -> float:
|
||||
"""Notional bersaglio (USD) di un asset = peso nel book * frazione-di-equity TP01 * equity.
|
||||
Coerente col paper trader (esposizione asset = WEIGHT * target * equity)."""
|
||||
return weight * target_fraction * equity_usd
|
||||
|
||||
|
||||
def build_rebalance_order(instrument: str, target_fraction: float, weight: float,
|
||||
equity_usd: float, current_pos_usd: float) -> dict | None:
|
||||
"""COSTRUISCE (non invia) l'ordine di ribilancio verso il target. Ritorna un dict-ordine o None
|
||||
se sotto-soglia. Long-only TP01 -> target_notional >= 0; delta = target - posizione corrente."""
|
||||
tgt = target_notional_usd(target_fraction, weight, equity_usd)
|
||||
delta = tgt - current_pos_usd
|
||||
amount = notional_to_amount(instrument, delta)
|
||||
if amount == 0.0:
|
||||
return None
|
||||
is_exit = abs(tgt) < 1e-9 and abs(current_pos_usd) > 0
|
||||
return dict(
|
||||
instrument=instrument,
|
||||
side="buy" if delta > 0 else "sell",
|
||||
amount=amount,
|
||||
type="market",
|
||||
reduce_only=is_exit,
|
||||
target_notional=round(tgt, 2),
|
||||
current_notional=round(current_pos_usd, 2),
|
||||
delta_notional=round(delta, 2),
|
||||
)
|
||||
|
||||
|
||||
# ----------------------------- lettura mainnet (Cerbero MCP) — SOLA LETTURA -----------------------------
|
||||
|
||||
def _load_mainnet_token() -> tuple[str, str]:
|
||||
"""Legge CERBERO_TOKEN (mainnet) + bot-tag da .env.mainnet. Il token NON viene mai stampato."""
|
||||
env: dict[str, str] = {}
|
||||
for ln in (PROJECT_ROOT / ".env.mainnet").read_text().splitlines():
|
||||
ln = ln.strip()
|
||||
if ln and not ln.startswith("#") and "=" in ln:
|
||||
k, v = ln.split("=", 1)
|
||||
env[k] = v.strip()
|
||||
if "CERBERO_TOKEN" not in env:
|
||||
raise RuntimeError("CERBERO_TOKEN assente in .env.mainnet")
|
||||
return env["CERBERO_TOKEN"], env.get("CERBERO_BOT_TAG", "pythagoras-shadow")
|
||||
|
||||
|
||||
class DeribitRead:
|
||||
"""Accesso Deribit mainnet in SOLA LETTURA via Cerbero MCP. Nessun metodo di trading (by design)."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._token, self._tag = _load_mainnet_token()
|
||||
|
||||
def _post(self, path: str, payload: dict) -> dict | list:
|
||||
r = requests.post(
|
||||
f"{BASE_URL}{path}",
|
||||
headers={"Authorization": f"Bearer {self._token}", "X-Bot-Tag": self._tag,
|
||||
"Content-Type": "application/json"},
|
||||
json=payload, timeout=TIMEOUT,
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
@staticmethod
|
||||
def _unwrap(resp: dict | list) -> dict | list:
|
||||
return resp.get("result", resp) if isinstance(resp, dict) else resp
|
||||
|
||||
def ticker(self, instrument: str) -> dict:
|
||||
return self._unwrap(self._post("/mcp-deribit/tools/get_ticker", {"instrument": instrument})) or {}
|
||||
|
||||
def mark_price(self, instrument: str) -> float:
|
||||
t = self.ticker(instrument)
|
||||
for k in ("mark_price", "index_price", "last_price", "last"):
|
||||
v = t.get(k)
|
||||
if v:
|
||||
return float(v)
|
||||
raise ValueError(f"prezzo assente nel ticker {instrument}: chiavi={list(t)[:8]}")
|
||||
|
||||
def account_summary(self, currency: str) -> dict:
|
||||
return self._unwrap(self._post("/mcp-deribit/tools/get_account_summary", {"currency": currency})) or {}
|
||||
|
||||
def positions(self, currency: str) -> list[dict]:
|
||||
out = self._unwrap(self._post("/mcp-deribit/tools/get_positions", {"currency": currency}))
|
||||
if isinstance(out, list):
|
||||
return out
|
||||
return out.get("positions", []) if isinstance(out, dict) else []
|
||||
|
||||
def position_usd(self, instrument: str) -> float:
|
||||
"""Size netta (USD notional, segno = direzione) della posizione su `instrument`. 0 se flat."""
|
||||
cur = _CONTRACT[instrument]["settle"]
|
||||
for p in self.positions(cur):
|
||||
if p.get("instrument_name") == instrument or p.get("instrument") == instrument:
|
||||
return float(p.get("size") or p.get("size_currency") or 0.0)
|
||||
return 0.0
|
||||
Reference in New Issue
Block a user