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:
Adriano Dal Pastro
2026-06-08 10:19:56 +00:00
parent a2f1b960ec
commit c655604131
9 changed files with 470 additions and 12 deletions
+81
View File
@@ -0,0 +1,81 @@
"""Smoke ESECUZIONE REALE a 2 gambe (PairsExecutionClient) su Deribit testnet.
Apre e chiude una posizione pair micro (ETH/BTC lineari USDC) come farebbe il
PairsWorker, esercitando: open_pair (2 market simultanei long A/short B), verifica
per-gamba, close_pair (2 reduce-only), riconciliazione PnL/fee a 2 gambe.
Verifica finale: conto flat su ENTRAMBI gli strumenti.
Costo testnet = €0. Non tocca lo stato di produzione (data_dir temporanea).
uv run python scripts/analysis/live_pairs_smoke.py
"""
from __future__ import annotations
import tempfile
from pathlib import Path
from src.live.cerbero_client import CerberoClient
from src.live.execution import ExecutionClient, PairsExecutionClient
from src.live.pairs_worker import PairsWorker
def main() -> None:
client = CerberoClient()
acct = client.get_account_summary(currency="USDC")
print(f"Account testnet equity={acct.get('equity')} USDC testnet={acct.get('testnet')}")
if not acct.get("testnet"):
raise SystemExit("ABORT: non testnet")
pex = PairsExecutionClient(leg=ExecutionClient(client=client))
inst = {"ETH": "ETH_USDC-PERPETUAL", "BTC": "BTC_USDC-PERPETUAL"}
pex.ensure_specs(*inst.values())
print(f"strumenti: {inst}")
# GUARDIA: questo conto e' condiviso col runner di produzione (fade reali su
# BTC/ETH_USDC). Se ci sono posizioni aperte, ABORT: lo smoke userebbe lo stesso
# strumento e la verifica "conto flat" sarebbe falsata (e un eventuale
# close_position flatterebbe le quote del runner). Mai eseguire close_position qui.
for i in inst.values():
sz = pex.leg._position_size(i)
if sz != 0:
raise SystemExit(f"ABORT: posizione di produzione aperta su {i} (size={sz}). "
f"Eseguire lo smoke solo a conto flat.")
with tempfile.TemporaryDirectory() as tmp:
# capitale piccolo -> notional micro per gamba (capital*pos*lev)
w = PairsWorker("ETH", "BTC", "1h", capital=60.0, position_size=0.15, leverage=2.0,
data_dir=Path(tmp), executor=pex, exec_instruments=inst)
print(f"execution_enabled={w.execution_enabled} notional/gamba atteso=${60*0.15*2:.0f}")
# prezzi sim plausibili (il ramo sim del worker li usa per il suo ledger;
# il reale usa i fill veri). Niente zeri -> niente divisione per zero.
pa = pex.leg._mark_price(inst["ETH"]) or 1668.0
pb = pex.leg._mark_price(inst["BTC"]) or 63000.0
print("\n[A] OPEN long ratio (long ETH / short BTC)")
w._open(1, pa, pb, -2.5)
print(f" real_in_position={w.real_in_position} dir={w.real_dir}")
print(f" gamba A {w.real_side_a} {w.real_amount_a} @ {w.real_entry_a} (notional ${w.real_notional_a:.2f})")
print(f" gamba B {w.real_side_b} {w.real_amount_b} @ {w.real_entry_b} (notional ${w.real_notional_b:.2f})")
print(f" entry_fee ${w.real_entry_fee:.5f}")
assert w.real_in_position, "open pair non verificato"
assert w.real_side_a == "buy" and w.real_side_b == "sell"
print("\n[B] CLOSE (richiude entrambe le gambe reduce-only)")
cap0 = w.real_capital
pa2 = pex.leg._mark_price(inst["ETH"]) or pa
pb2 = pex.leg._mark_price(inst["BTC"]) or pb
w._close(pa2, pb2, 0.3, "mean_revert")
print(f" real_capital {cap0:.4f} -> {w.real_capital:.4f}{w.real_capital-cap0:+.4f}) trades={w.real_trades}")
assert not w.real_in_position, "close pair non azzera la posizione"
# verifica conto flat su entrambi gli strumenti
pa = pex.leg._position_size(inst["ETH"])
pb = pex.leg._position_size(inst["BTC"])
print(f"\n posizione netta ETH={pa} BTC={pb}")
print("✓ catena pairs OK — open 2 gambe verificate, close 2 gambe, fee reali, conto flat"
if pa == 0 and pb == 0 else
f"⚠ residuo sul book (ETH={pa} BTC={pb}) — verificare manualmente")
if __name__ == "__main__":
main()