fix(exec): verità contabile sul netting — filled_amount, gambe orfane, tick isolato
Da audit live (3 indagini parallele, diario 2026-06-11-system-audit.md): il modello quote-per-worker reduce-only si rompe con direzioni opposte sullo stesso strumento (pairs long ETH vs fade short ETH) — close cappati bookati pieni, 3 gambe pairs mai eseguite col PnL sim nel ledger reale, conto short 0.027 ETH oltre i libri (riallineato a mano + DSL orfano cancellato). - Fill.filled_amount (order.filled_amount): TUTTI i ledger usano il fillato reale - REAL_CLOSE_PARTIAL (log+alert) su close cappato; residuo orfano dichiarato - pairs: PnL solo per gambe verificate; gamba respinta -> orphan_legs persistito + alert PAIR_LEG_ORPHAN; applied solo con entrambe le gambe (else sim_fallback) - REAL_DIVERGENCE anche su jsonl (prima solo Telegram) - runner: tick isolato per-worker + WORKER_ERROR_STREAK a 5 fail consecutivi Strutturale APERTO (decisione utente, opzioni nel diario): position-manager centrale / sotto-conti per famiglia / status-quo monitorato. Test: +2 (partial-close, orphan-leg), fixture filled_amount -> 106 passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -28,7 +28,7 @@ class FakeExec:
|
||||
|
||||
def close_amount(self, instrument, side, amount, label=None):
|
||||
return Fill(instrument, "sell", 0.0, amount, 100.0, 0.0, 0.0,
|
||||
"oid-close", "filled", True)
|
||||
"oid-close", "filled", True, filled_amount=amount)
|
||||
|
||||
|
||||
class FakeFullExec(FakeExec):
|
||||
@@ -53,7 +53,8 @@ class FakeFullExec(FakeExec):
|
||||
|
||||
def close_amount(self, instrument, side, amount, label=None):
|
||||
self.close_amounts.append(amount)
|
||||
return Fill(instrument, "sell", 0.0, amount, 105.0, 0.0, 0.05, "oid-close", "filled", True)
|
||||
return Fill(instrument, "sell", 0.0, amount, 105.0, 0.0, 0.05, "oid-close",
|
||||
"filled", True, filled_amount=amount)
|
||||
|
||||
|
||||
def test_sh01_real_open_close_no_tp(tmp_path, monkeypatch):
|
||||
|
||||
@@ -26,7 +26,7 @@ class FakeExec:
|
||||
def open(self, instrument, side, notional, label=None):
|
||||
amt = round(notional / self.open_px, 6)
|
||||
return Fill(instrument, side, notional, amt, self.open_px, 0.0, 0.05,
|
||||
"oid-open", "filled", True)
|
||||
"oid-open", "filled", True, filled_amount=amt)
|
||||
|
||||
def place_tp_limit(self, *a, **k):
|
||||
return Fill("x", "sell", 0.0, 0.0, None, 0.0, 0.0, None, None, False)
|
||||
@@ -42,7 +42,8 @@ class FakeExec:
|
||||
|
||||
def close_amount(self, instrument, side, amount, label=None):
|
||||
return Fill(instrument, "sell" if side == "buy" else "buy", 0.0, amount,
|
||||
self.close_px, 0.0, 0.05, "oid-close", "filled", self.close_verified)
|
||||
self.close_px, 0.0, 0.05, "oid-close", "filled", self.close_verified,
|
||||
filled_amount=amount)
|
||||
|
||||
|
||||
def _sw(tmp_path, fake, real_truth=True):
|
||||
@@ -188,3 +189,50 @@ def test_pairs_fallback_sim_on_leg_fail(tmp_path):
|
||||
if '"CLOSE"' in l][-1])
|
||||
assert c["pnl_source"] == "sim_fallback"
|
||||
assert c["pnl"] == c["sim_pnl"]
|
||||
|
||||
|
||||
def test_partial_close_books_only_filled_amount(tmp_path):
|
||||
"""Reduce-only cappato dal netting (audit 2026-06-11): close richiesto 1.0 ma
|
||||
fillato 0.5 -> il ledger booka SOLO il fillato, REAL_CLOSE verified=False e
|
||||
evento REAL_CLOSE_PARTIAL col residuo orfano (niente piu' chiusure 'piene' finte)."""
|
||||
class PartialFake(FakeExec):
|
||||
def close_amount(self, instrument, side, amount, label=None):
|
||||
return Fill(instrument, "sell" if side == "buy" else "buy", 0.0, amount,
|
||||
self.close_px, 0.0, 0.05, "oid-close", "filled", True,
|
||||
filled_amount=amount * 0.5)
|
||||
|
||||
fake = PartialFake(open_px=100.0, close_px=105.0)
|
||||
w = _sw(tmp_path, fake)
|
||||
_open_sim(w, 100.0)
|
||||
w._close_position(105.0, "time_limit")
|
||||
rows = [json.loads(l) for l in w.trades_path.read_text().strip().splitlines()]
|
||||
rc = [r for r in rows if r.get("event") == "REAL_CLOSE"][-1]
|
||||
assert rc["verified"] is False # chiusura NON completa
|
||||
partial = [r for r in rows if r.get("event") == "REAL_CLOSE_PARTIAL"]
|
||||
assert partial and partial[-1]["residuo_orfano"] > 0
|
||||
|
||||
|
||||
def test_pairs_orphan_leg_close_not_booked(tmp_path):
|
||||
"""Gamba A respinta in CHIUSURA (reduce-only vs netting, audit 2026-06-11): il suo
|
||||
'profitto' al prezzo sim NON va nel ledger reale, l'orfano e' registrato/persistito
|
||||
e il real-truth ricade sul sim DICHIARATO (applied=False)."""
|
||||
class LegFailClose(FakePairsExec):
|
||||
def close_pair(self, inst_a, inst_b, side_a, side_b, amount_a, amount_b, label=None):
|
||||
bad_a = Fill(inst_a, "sell", 0.0, amount_a, None, 0.0, 0.0, None, "error", False)
|
||||
ok_b = self._fill(inst_b, "buy", amount_b, self.px["close_b"])
|
||||
return PairFill(False, bad_a, ok_b)
|
||||
|
||||
fake = LegFailClose(open_a=100.0, close_a=110.0) # gamba A +10% ma MAI chiusa
|
||||
w = _pw(tmp_path, fake)
|
||||
w._open(1, 100.0, 50.0, -2.1)
|
||||
na = w.real_notional_a
|
||||
w._close(110.0, 50.0, 0.1, "mean_revert")
|
||||
assert w.orphan_legs and w.orphan_legs[0]["instrument"] == "ETH_USDC-PERPETUAL"
|
||||
rc = json.loads([l for l in w.trades_path.read_text().splitlines()
|
||||
if '"REAL_CLOSE_PAIR"' in l][-1])
|
||||
assert rc["leg_a_ok"] is False and rc["leg_b_ok"] is True
|
||||
# il +10% della gamba A (mai eseguita) NON e' nel capitale reale
|
||||
assert w.real_capital < 100.0 + 0.10 * na - 0.05
|
||||
c = json.loads([l for l in w.trades_path.read_text().splitlines()
|
||||
if '"event": "CLOSE"' in l][-1])
|
||||
assert c["pnl_source"] == "sim_fallback"
|
||||
|
||||
@@ -35,6 +35,7 @@ class _FakeExecutor:
|
||||
class F:
|
||||
verified = True
|
||||
amount = 0.01
|
||||
filled_amount = 0.01
|
||||
fill_price = 100.0
|
||||
fee_usd = 0.01
|
||||
order_id = "X"
|
||||
|
||||
Reference in New Issue
Block a user