feat(exec): netting delle chiusure market — il residuo reduce-only cappato/respinto va in market puro

close_amount ora: (1) tenta il market reduce-only (sicurezza storica: un bug di
stato filla 0 invece di aprire posizioni); (2) il residuo cappato/respinto dal
netting di conto (worker in direzioni opposte sullo stesso strumento) viene
rieseguito in MARKET PURO con label '|net' — muove il conto esattamente del
delta del libro = netta contro le quote opposte. Niente piu' gambe pairs orfane
ne' close cappati per costruzione (close_pair passa da close_amount).

- _merge_close_fills: il chiamante riceve UN Fill (prezzo medio pesato sui fill,
  fee sommate, filled_amount totale, verified se copre il richiesto, notes
  'netting' quando il fallback scatta)
- worker single-leg + pairs: evento NET_CLOSE (log jsonl + Telegram) a ogni
  fallback — osservabilita' della frequenza dei conflitti di netting
- sicurezza persa sul residuo coperta dal reconciler orario (ACCOUNT_DRIFT);
  orphan_legs/REAL_CLOSE_PARTIAL restano come ultima difesa
- 4 test nuovi (full r-o senza fallback, cappato, respinto, doppio fail) -> 110 passed

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-06-11 20:54:43 +00:00
parent 22b77668f1
commit 844a9d0e22
6 changed files with 155 additions and 7 deletions
+82
View File
@@ -0,0 +1,82 @@
"""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):
"""ExecutionClient con _submit finto: consuma la lista `submits` e registra le chiamate."""
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
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