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:
+20
-1
@@ -119,6 +119,12 @@ class Fill:
|
||||
verified: bool # posizione/trade riscontrati su Deribit
|
||||
raw: dict[str, Any] = field(default_factory=dict)
|
||||
notes: str = ""
|
||||
# amount REALMENTE fillato (order.filled_amount / somma trades) — puo' essere
|
||||
# MINORE del richiesto: un reduce-only cappato dal netting di conto (worker
|
||||
# long e short sullo stesso strumento) viene ridotto in silenzio da Deribit.
|
||||
# Audit 2026-06-11: close 0.105, fillato 0.078, bookato pieno perche' il
|
||||
# ledger usava `amount`. I ledger devono usare QUESTO campo.
|
||||
filled_amount: float = 0.0
|
||||
|
||||
|
||||
def _avg_fill_price(order: dict, trades: list[dict]) -> float | None:
|
||||
@@ -233,6 +239,15 @@ class ExecutionClient:
|
||||
fee_usd = fee_coin if spec.get("linear") else (
|
||||
fee_coin * fill_price if (fee_coin and fill_price) else 0.0)
|
||||
|
||||
# amount REALMENTE fillato: order.filled_amount e' la fonte autorevole
|
||||
# (Deribit RIDUCE in silenzio un reduce-only che eccede il netto di conto);
|
||||
# fallback: somma trades del fill, poi trade history
|
||||
filled = float(order.get("filled_amount") or 0)
|
||||
if not filled:
|
||||
filled = sum(float(t.get("amount", 0) or 0) for t in trades)
|
||||
if not filled and th:
|
||||
filled = float(th.get("amount") or 0)
|
||||
|
||||
# VERIFICA: market = ordine filled E fill riscontrato (trades o history);
|
||||
# limit = accettato in book ('open') o gia' eseguito ('filled');
|
||||
# stop_market = trigger accettato ('untriggered' finche' il mark non tocca)
|
||||
@@ -242,9 +257,13 @@ class ExecutionClient:
|
||||
verified = state in ("untriggered", "open", "filled")
|
||||
else:
|
||||
verified = state in ("open", "filled")
|
||||
notes = "" if verified else f"fill non verificato (state={state}, trades={len(trades)})"
|
||||
if verified and order_type == "market" and filled < amount - 1e-12:
|
||||
notes = (f"FILL PARZIALE: {filled} su {amount} richiesti "
|
||||
"(reduce-only cappato dal netting di conto?)")
|
||||
return Fill(instrument, side, requested_notional, amount, fill_price,
|
||||
fee_coin, fee_usd, order_id, state, verified, raw=resp,
|
||||
notes="" if verified else f"fill non verificato (state={state}, trades={len(trades)})")
|
||||
notes=notes, filled_amount=filled)
|
||||
|
||||
def open(self, instrument: str, side: str, notional_usd: float,
|
||||
label: str | None = None) -> Fill:
|
||||
|
||||
Reference in New Issue
Block a user