82c05f6f81
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>
102 lines
4.4 KiB
Python
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
|