Files
PythagorasGoal/tests/portfolio/test_pairs_execution.py
T
Adriano Dal Pastro c655604131 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>
2026-06-08 10:19:56 +00:00

101 lines
4.3 KiB
Python

"""PairsWorker esecuzione reale a 2 gambe (shadow): apertura/chiusura, leg-risk unwind,
ledger reale parallelo. Executor finto, nessuna rete."""
from types import SimpleNamespace
import numpy as np
import pandas as pd
import pytest
from src.live.execution import Fill, PairFill
from src.live.pairs_worker import PairsWorker
class FakePairsExec:
"""Simula PairsExecutionClient: fill deterministici, con possibilità di leg-fail."""
def __init__(self, fail_leg=None):
self.fail_leg = fail_leg # None | 'a' | 'b' : quale gamba NON filla all'open
self.opened = []
self.closed = []
self.unwound = []
def _fill(self, inst, side, price, amt=1.0, ok=True):
return Fill(inst, side, 0.0, amt if ok else 0.0, price if ok else None,
0.0, 0.05 if ok else 0.0, "oid" if ok else None,
"filled" if ok else "error", ok)
def open_pair(self, inst_a, inst_b, direction, notional, label=None):
side_a = "buy" if direction == 1 else "sell"
side_b = "sell" if direction == 1 else "buy"
ok_a = self.fail_leg != 'a'
ok_b = self.fail_leg != 'b'
fa = self._fill(inst_a, side_a, 100.0, ok=ok_a)
fb = self._fill(inst_b, side_b, 50.0, ok=ok_b)
self.opened.append((inst_a, inst_b, direction))
if ok_a and ok_b:
return PairFill(True, fa, fb)
if ok_a and fa.amount > 0:
self.unwound.append(inst_a)
if ok_b and fb.amount > 0:
self.unwound.append(inst_b)
return PairFill(False, fa, fb, unwound=bool(self.unwound), notes="leg-fail")
def close_pair(self, inst_a, inst_b, side_a, side_b, amt_a, amt_b, label=None):
opp_a = "sell" if side_a == "buy" else "buy"
opp_b = "sell" if side_b == "buy" else "buy"
self.closed.append((inst_a, inst_b))
# prezzi di uscita: A salito a 102, B fermo a 50 -> long-A/short-B guadagna
return PairFill(True, self._fill(inst_a, opp_a, 102.0, amt_a),
self._fill(inst_b, opp_b, 50.0, amt_b))
INST = {"ETH": "ETH_USDC-PERPETUAL", "BTC": "BTC_USDC-PERPETUAL"}
def _worker(tmp_path, fake, monkeypatch, notified=None):
if notified is not None:
monkeypatch.setattr("src.live.pairs_worker.notify_event",
lambda ev, data=None: notified.append(ev))
return PairsWorker("ETH", "BTC", "1h", capital=200.0, position_size=0.2, leverage=2.0,
data_dir=tmp_path, executor=fake, exec_instruments=INST)
def test_execution_enabled_wired(tmp_path):
w = _worker(tmp_path, FakePairsExec(), pytest.MonkeyPatch())
assert w.execution_enabled and w.inst_a == "ETH_USDC-PERPETUAL" and w.inst_b == "BTC_USDC-PERPETUAL"
def test_real_open_and_close_pair(tmp_path, monkeypatch):
notified = []
fake = FakePairsExec()
w = _worker(tmp_path, fake, monkeypatch, notified)
w._open(1, 100.0, 50.0, -2.5) # long ratio
assert w.real_in_position and w.real_dir == 1
assert w.real_side_a == "buy" and w.real_side_b == "sell"
assert fake.opened and "REAL_EXEC_LIVE" in notified
cap0 = w.real_capital
w._close(102.0, 50.0, 0.3, "mean_revert")
assert not w.real_in_position and fake.closed
assert w.real_capital > cap0 # A +2% / B 0 -> long-A/short-B in utile
assert w.real_trades == 1
def test_leg_fail_unwinds_and_no_position(tmp_path, monkeypatch):
notified = []
fake = FakePairsExec(fail_leg='b') # gamba B non filla
w = _worker(tmp_path, fake, monkeypatch, notified)
w._open(1, 100.0, 50.0, -2.5)
assert not w.real_in_position # niente posizione reale
assert "ETH_USDC-PERPETUAL" in fake.unwound # la gamba A fillata e' stata richiusa
assert "REAL_OPEN_FAIL" in notified
def test_real_ledger_persists_and_resumes(tmp_path, monkeypatch):
fake = FakePairsExec()
w = _worker(tmp_path, fake, monkeypatch, [])
w._open(-1, 100.0, 50.0, 2.5) # short ratio
assert w.real_in_position and w.real_dir == -1
w2 = PairsWorker("ETH", "BTC", "1h", capital=200.0, position_size=0.2, leverage=2.0,
data_dir=tmp_path, executor=fake, exec_instruments=INST)
assert w2.real_in_position and w2.real_dir == -1
assert w2.real_side_a == "sell" and w2.real_amount_a > 0