Files
PythagorasGoal/tests/portfolio/test_pairs_execution.py
T
Adriano Dal Pastro 82c05f6f81 fix(exec): code-review serale — guard anti-posizione-nuda sul netting + verità per-frazione
7 finder paralleli sul diff della giornata (8adf388..HEAD), fix dei confermati:

CRITICI (money-path):
- close_amount: GUARD stato-stantio sul fallback netting — il residuo
  non-reduce-only e' consentito solo fino al gap (conto reale - libri degli
  altri worker - orfani) nella direzione della chiusura (src/live/books.py =
  fonte unica, usata anche dal reconciler). Senza guard, un close su stato
  stantio (DSL scattato in outage, flatten manuale) APRIVA una posizione nuda
  a taglia piena bookata come 'chiusura verificata'. Fail-safe se il gap non
  e' calcolabile. Check polvere PRIMA di _quantize_step (che clampa al lotto
  minimo: un residuo 1e-17 diventava un ordine nudo da un lotto).
- _real_close: market_amt = filled_amount anche a merged verified=False (i
  contratti chiusi dal reduce-only non si buttano se il leg netting fallisce);
  REAL_CLOSE_PARTIAL non piu' gateato su verified (era soppresso proprio nel
  caso parziale reale).
- pairs: verita' per-FRAZIONE di gamba (gross proporzionale al fillato, orfano
  = solo il residuo — prima falsava reconciler e real_capital della parte gia'
  chiusa); REAL_OPEN_PAIR booka filled_amount; docstring applied corretta.
- open_pair unwind: chiude il FILLATO, non il richiesto (senza il cap silenzioso
  del reduce-only avrebbe mangiato quota altrui).
- place_tp_limit: quantize CONSERVATIVO (sell=floor/buy=ceil) — il rounding al
  tick piu' vicino poteva mettere il resting oltre il TP sim -> tocco genuino
  classificato phantom sistematicamente.

ROBUSTEZZA/OSSERVABILITA':
- runner: WORKER_ERROR_STREAK a 5 e poi ogni 50 poll + recovery RIPRESO +
  flag real_in_position nell'alert (prima: one-shot a n==5, poi silenzio).
- _tp_phantom: TTL 120s sul verdetto (era ~50 HTTP/h per worker a barra
  fantasma); merge notes con entrambi gli order_id (audit-trail).
- reset_flatten: _quantize_step Decimal (round float produceva amount che
  Deribit rifiuta); hourly_report: book XS01 con gambe SHORT visibili (abs).
- validate_xsec_worker: POS/LEV importati (non hardcoded); xs01_tranche:
  regression-lock ESEGUITO vs xsec_sim; reconcile su src/live/books;
  drift_monitor: rolling vettoriale + exit code 1 su warn.

Test: +4 guard/dust, fixture filled_amount -> 114 passed.
Deferiti (TODO): resting esposti al netting, lifecycle orphan_legs, finestra
trade-history TP_PHANTOM, validazione feed a monte, dedup minori.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 21:36:30 +00:00

102 lines
4.4 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,
filled_amount=amt if ok else 0.0)
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