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:
@@ -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
|
||||
Reference in New Issue
Block a user