"""Netting delle chiusure market (v1.1.25): close_amount tenta il reduce-only e riesegue il RESIDUO in market puro quando il netting di conto lo cappa/respinge (audit 2026-06-11: 3 gambe pairs orfane + 1 close cappato). Il chiamante riceve UN Fill combinato. Nessuna rete: _submit monkeypatchato.""" from src.live.execution import ExecutionClient, Fill def _fill(inst, side, amount, filled, px, verified=True, state="filled", fee=0.05): return Fill(inst, side, 0.0, amount, px if filled else None, 0.0, fee, f"oid-{side}-{filled}", state, verified, filled_amount=filled) def _client_with(submits, allowance=float("inf")): """ExecutionClient con _submit finto e guard del netting stubbato (il guard reale legge conto+libri via HTTP/filesystem; qui si inietta il gap).""" ec = ExecutionClient() calls = [] def fake_submit(instrument, side, amount, notional, reduce_only, label=None, order_type="market", price=None): calls.append(dict(amount=amount, reduce_only=reduce_only, label=label)) return submits.pop(0)(instrument, side, amount) ec._submit = fake_submit ec._net_close_allowance = lambda *a, **k: allowance return ec, calls INST = "ETH_USDC-PERPETUAL" def test_full_reduce_only_no_fallback(): # caso normale: il reduce-only filla tutto -> NESSUN secondo ordine ec, calls = _client_with([ lambda i, s, a: _fill(i, s, a, filled=a, px=1700.0), ]) f = ec.close_amount(INST, "sell", 0.103, label="w1") assert len(calls) == 1 and calls[0]["reduce_only"] is True assert f.verified and abs(f.filled_amount - 0.103) < 1e-9 assert "netting" not in (f.notes or "") def test_capped_reduce_only_nets_residual(): # reduce-only CAPPATO (filla 0.078 su 0.105) -> residuo 0.027 in market puro ec, calls = _client_with([ lambda i, s, a: _fill(i, s, a, filled=0.078, px=1700.0), lambda i, s, a: _fill(i, s, a, filled=a, px=1702.0), ]) f = ec.close_amount(INST, "sell", 0.105, label="w1") assert len(calls) == 2 assert calls[0]["reduce_only"] is True and calls[1]["reduce_only"] is False assert abs(calls[1]["amount"] - 0.027) < 1e-9 assert calls[1]["label"] == "w1|net" assert f.verified and abs(f.filled_amount - 0.105) < 1e-9 # prezzo medio pesato: (0.078*1700 + 0.027*1702) / 0.105 exp_px = (0.078 * 1700.0 + 0.027 * 1702.0) / 0.105 assert abs(f.fill_price - exp_px) < 1e-6 assert abs(f.fee_usd - 0.10) < 1e-9 # fee sommate assert "netting" in f.notes def test_rejected_reduce_only_nets_full_amount(): # reduce-only RESPINTO (error, 0 fill) -> tutto l'amount in market puro ec, calls = _client_with([ lambda i, s, a: _fill(i, s, a, filled=0.0, px=None, verified=False, state="error", fee=0.0), lambda i, s, a: _fill(i, s, a, filled=a, px=1701.0), ]) f = ec.close_amount(INST, "buy", 0.027, label="pair") assert len(calls) == 2 and abs(calls[1]["amount"] - 0.027) < 1e-9 assert f.verified and abs(f.filled_amount - 0.027) < 1e-9 assert f.fill_price == 1701.0 assert "netting" in f.notes def test_net_leg_also_fails_reports_partial(): # anche il market puro fallisce -> Fill combinato NON verified, filled = solo r-o ec, calls = _client_with([ lambda i, s, a: _fill(i, s, a, filled=0.05, px=1700.0), lambda i, s, a: _fill(i, s, a, filled=0.0, px=None, verified=False, state="error", fee=0.0), ]) f = ec.close_amount(INST, "sell", 0.105, label="w1") assert len(calls) == 2 assert not f.verified assert abs(f.filled_amount - 0.05) < 1e-9 # solo il fillato vero nel ledger def test_stale_state_guard_blocks_naked_order(): # quota gia' chiusa (DSL in outage / flatten manuale): reduce-only filla 0 e # il gap conto-vs-libri e' 0 -> NESSUN ordine market nudo (code-review 2026-06-11) ec, calls = _client_with([ lambda i, s, a: _fill(i, s, a, filled=0.0, px=None, verified=False, state="error", fee=0.0), ], allowance=0.0) f = ec.close_amount(INST, "buy", 0.105, label="w1") assert len(calls) == 1 # solo il reduce-only, niente fallback assert not f.verified and f.filled_amount == 0.0 assert "netting NEGATO" in f.notes def test_guard_unknown_fails_safe(): # guard non calcolabile (rete): FAIL-SAFE, niente ordine nudo ec, calls = _client_with([ lambda i, s, a: _fill(i, s, a, filled=0.0, px=None, verified=False, state="error", fee=0.0), ], allowance=None) f = ec.close_amount(INST, "buy", 0.105, label="w1") assert len(calls) == 1 assert "non calcolabile" in f.notes def test_guard_caps_residual_to_allowance(): # gap parziale: il residuo nudo e' cappato a quanto i libri giustificano ec, calls = _client_with([ lambda i, s, a: _fill(i, s, a, filled=0.0, px=None, verified=False, state="error", fee=0.0), lambda i, s, a: _fill(i, s, a, filled=a, px=1700.0), ], allowance=0.027) f = ec.close_amount(INST, "sell", 0.105, label="w1") assert len(calls) == 2 assert abs(calls[1]["amount"] - 0.027) < 1e-9 assert not f.verified # 0.027 < 0.105: chiusura parziale onesta assert abs(f.filled_amount - 0.027) < 1e-9 def test_dust_residual_no_fallback(): # residuo da artefatto float (1e-17): NON deve diventare un lotto minimo nudo ec, calls = _client_with([ lambda i, s, a: _fill(i, s, a, filled=a - 1e-17, px=1700.0), ]) f = ec.close_amount(INST, "sell", 0.105, label="w1") assert len(calls) == 1 assert f.verified