"""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