feat(live): executor a 2 gambe per i pairs (PairsExecutionClient, shadow) — pronto, spento
PairsExecutionClient (compone ExecutionClient): open_pair (2 market long A/short B, verifica per-gamba, LEG-RISK unwind se una sola filla), close_pair (2 reduce-only via close_amount, MAI close_position), register_contract (fetch spec USDC). Spec LTC/ADA/SOL aggiunti. PairsWorker: ledger reale shadow a 2 gambe resume-safe (_real_open_pair/ _real_close_pair, PnL per gamba dir A=+d/B=-d, doppio arrotondamento riportato). Runner: pairs_executor gated su execution.pairs_enabled (false di default). Validazione: test 92/92 (open/close, leg-risk unwind, resume) + smoke testnet end-to-end (open 2 gambe verificate, close reduce-only, PnL reale -0.039 vs sim -0.036, conto flat). Smoke ora aborta se ci sono posizioni di produzione + usa solo close_amount. NB incidente testnet documentato (diario): pulizia manuale con close_position ha flattato le quote shadow dei fade sul conto condiviso -> auto-riconciliazione al prossimo close. Lezione: mai close_position su strumenti condivisi. pairs_enabled resta FALSE: accendere con finestra a conto flat + osservazione. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+88
-2
@@ -29,8 +29,12 @@ from src.live.cerbero_client import CerberoClient
|
||||
_CONTRACT: dict[str, dict[str, Any]] = {
|
||||
"BTC-PERPETUAL": {"linear": False, "min": 10.0, "step": 10.0, "tick": 0.5},
|
||||
"ETH-PERPETUAL": {"linear": False, "min": 1.0, "step": 1.0, "tick": 0.05},
|
||||
"BTC_USDC-PERPETUAL": {"linear": True, "min": 0.0001, "step": 0.0001, "tick": 0.5, "settle": "USDC"},
|
||||
"ETH_USDC-PERPETUAL": {"linear": True, "min": 0.001, "step": 0.001, "tick": 0.05, "settle": "USDC"},
|
||||
"BTC_USDC-PERPETUAL": {"linear": True, "min": 0.0001, "step": 0.0001, "tick": 0.5, "settle": "USDC"},
|
||||
"ETH_USDC-PERPETUAL": {"linear": True, "min": 0.001, "step": 0.001, "tick": 0.05, "settle": "USDC"},
|
||||
# lineari USDC per le gambe dei pairs (PairsExecutionClient, 2026-06-08)
|
||||
"LTC_USDC-PERPETUAL": {"linear": True, "min": 0.1, "step": 0.1, "tick": 0.01, "settle": "USDC"},
|
||||
"ADA_USDC-PERPETUAL": {"linear": True, "min": 0.2, "step": 0.2, "tick": 1e-05, "settle": "USDC"},
|
||||
"SOL_USDC-PERPETUAL": {"linear": True, "min": 0.1, "step": 0.1, "tick": 0.001, "settle": "USDC"},
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +42,24 @@ def contract_spec(instrument: str) -> dict[str, Any]:
|
||||
return _CONTRACT.get(instrument, {"linear": False, "min": 1.0, "step": 1.0})
|
||||
|
||||
|
||||
def register_contract(instrument: str, client) -> dict[str, Any]:
|
||||
"""Recupera lo spec di uno strumento USDC da Deribit (get_instruments) e lo
|
||||
registra in _CONTRACT. Fallback per strumenti pair non hardcodati; ritorna lo
|
||||
spec (o il default se non trovato). Idempotente."""
|
||||
if instrument in _CONTRACT:
|
||||
return _CONTRACT[instrument]
|
||||
try:
|
||||
for i in client.get_instruments(currency="USDC", kind="future", limit=300):
|
||||
if i.get("symbol") == instrument:
|
||||
step = float(i["native"].get("min_trade_amount"))
|
||||
_CONTRACT[instrument] = {"linear": True, "min": step, "step": step,
|
||||
"tick": float(i.get("tick_size") or 0), "settle": "USDC"}
|
||||
return _CONTRACT[instrument]
|
||||
except Exception:
|
||||
pass
|
||||
return contract_spec(instrument)
|
||||
|
||||
|
||||
def settlement_currency(instrument: str) -> str:
|
||||
"""Inverse → base-coin (BTC/ETH); lineari USDC → USDC. Usato per get_positions
|
||||
e per denominare la fee."""
|
||||
@@ -345,3 +367,67 @@ class ExecutionClient:
|
||||
return Fill(instrument, side, 0.0, 0.0, fill_price, fee_coin, fee_usd,
|
||||
order_id, resp.get("state"), verified, raw=resp,
|
||||
notes="" if verified else f"posizione non flat dopo close (pos={pos})")
|
||||
|
||||
|
||||
@dataclass
|
||||
class PairFill:
|
||||
"""Esito verificato di un'apertura/chiusura a 2 GAMBE."""
|
||||
verified: bool # entrambe le gambe eseguite e verificate
|
||||
leg_a: Fill
|
||||
leg_b: Fill
|
||||
unwound: bool = False # true se una gamba e' fallita e l'altra e' stata richiusa
|
||||
notes: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class PairsExecutionClient:
|
||||
"""Esecuzione REALE a 2 gambe (shadow) per i pairs market-neutral su Deribit.
|
||||
|
||||
Compone un ExecutionClient single-leg: apre/chiude le due gambe (long A / short B
|
||||
o viceversa) come market reduce_only-aware, con gestione del LEG-RISK:
|
||||
- open_pair: piazza entrambe; se UNA sola filla -> UNWIND (richiude la fillata
|
||||
reduce-only) per non restare con esposizione direzionale netta -> verified=False.
|
||||
- close_pair: chiude entrambe reduce-only (market); ritorna fee e prezzi reali.
|
||||
Strumenti = lineari USDC (payoff lineare == matematica del backtest a 2 gambe; fee
|
||||
in USDC). amount per gamba arrotondato allo step del rispettivo strumento (doppio
|
||||
arrotondamento: piccolo sbilanciamento di notional inevitabile, riportato).
|
||||
"""
|
||||
leg: "ExecutionClient" = field(default_factory=ExecutionClient)
|
||||
|
||||
def __post_init__(self):
|
||||
# registra gli spec USDC degli strumenti pair non hardcodati (LTC/ADA/SOL ci sono;
|
||||
# questo copre eventuali coppie future)
|
||||
self.client = self.leg.client
|
||||
|
||||
def ensure_specs(self, *instruments: str):
|
||||
for inst in instruments:
|
||||
register_contract(inst, self.client)
|
||||
|
||||
def open_pair(self, inst_a: str, inst_b: str, direction: int,
|
||||
notional_usd: float, label: str | None = None) -> PairFill:
|
||||
"""direction +1 = long A / short B; -1 = short A / long B. notional uguale per gamba."""
|
||||
self.ensure_specs(inst_a, inst_b)
|
||||
side_a = "buy" if direction == 1 else "sell"
|
||||
side_b = "sell" if direction == 1 else "buy"
|
||||
fa = self.leg.open(inst_a, side_a, notional_usd, label=label)
|
||||
fb = self.leg.open(inst_b, side_b, notional_usd, label=label)
|
||||
if fa.verified and fb.verified:
|
||||
return PairFill(True, fa, fb)
|
||||
# LEG-RISK: una sola gamba (o nessuna) verificata -> unwind la fillata
|
||||
unwound = False
|
||||
for f, inst in ((fa, inst_a), (fb, inst_b)):
|
||||
if f.verified and f.amount > 0:
|
||||
self.leg.close_amount(inst, f.side, f.amount, label=label)
|
||||
unwound = True
|
||||
return PairFill(False, fa, fb, unwound=unwound,
|
||||
notes=f"leg-fail (a={fa.verified} b={fb.verified}), unwound={unwound}")
|
||||
|
||||
def close_pair(self, inst_a: str, inst_b: str, side_a: str, side_b: str,
|
||||
amount_a: float, amount_b: float, label: str | None = None) -> PairFill:
|
||||
"""Chiude entrambe le gambe a mercato (reduce-only del lato opposto all'entrata).
|
||||
Ritorna PairFill con i Fill di chiusura (fee/prezzi reali). verified = entrambe chiuse."""
|
||||
ca = self.leg.close_amount(inst_a, side_a, amount_a, label=label)
|
||||
cb = self.leg.close_amount(inst_b, side_b, amount_b, label=label)
|
||||
return PairFill(ca.verified and cb.verified, ca, cb,
|
||||
notes="" if (ca.verified and cb.verified)
|
||||
else f"close parziale (a={ca.verified} b={cb.verified})")
|
||||
|
||||
Reference in New Issue
Block a user