fix(exec): code-review serale — guard anti-posizione-nuda sul netting + verità per-frazione
7 finder paralleli sul diff della giornata (8adf388..HEAD), fix dei confermati: CRITICI (money-path): - close_amount: GUARD stato-stantio sul fallback netting — il residuo non-reduce-only e' consentito solo fino al gap (conto reale - libri degli altri worker - orfani) nella direzione della chiusura (src/live/books.py = fonte unica, usata anche dal reconciler). Senza guard, un close su stato stantio (DSL scattato in outage, flatten manuale) APRIVA una posizione nuda a taglia piena bookata come 'chiusura verificata'. Fail-safe se il gap non e' calcolabile. Check polvere PRIMA di _quantize_step (che clampa al lotto minimo: un residuo 1e-17 diventava un ordine nudo da un lotto). - _real_close: market_amt = filled_amount anche a merged verified=False (i contratti chiusi dal reduce-only non si buttano se il leg netting fallisce); REAL_CLOSE_PARTIAL non piu' gateato su verified (era soppresso proprio nel caso parziale reale). - pairs: verita' per-FRAZIONE di gamba (gross proporzionale al fillato, orfano = solo il residuo — prima falsava reconciler e real_capital della parte gia' chiusa); REAL_OPEN_PAIR booka filled_amount; docstring applied corretta. - open_pair unwind: chiude il FILLATO, non il richiesto (senza il cap silenzioso del reduce-only avrebbe mangiato quota altrui). - place_tp_limit: quantize CONSERVATIVO (sell=floor/buy=ceil) — il rounding al tick piu' vicino poteva mettere il resting oltre il TP sim -> tocco genuino classificato phantom sistematicamente. ROBUSTEZZA/OSSERVABILITA': - runner: WORKER_ERROR_STREAK a 5 e poi ogni 50 poll + recovery RIPRESO + flag real_in_position nell'alert (prima: one-shot a n==5, poi silenzio). - _tp_phantom: TTL 120s sul verdetto (era ~50 HTTP/h per worker a barra fantasma); merge notes con entrambi gli order_id (audit-trail). - reset_flatten: _quantize_step Decimal (round float produceva amount che Deribit rifiuta); hourly_report: book XS01 con gambe SHORT visibili (abs). - validate_xsec_worker: POS/LEV importati (non hardcoded); xs01_tranche: regression-lock ESEGUITO vs xsec_sim; reconcile su src/live/books; drift_monitor: rolling vettoriale + exit code 1 su warn. Test: +4 guard/dust, fixture filled_amount -> 114 passed. Deferiti (TODO): resting esposti al netting, lifecycle orphan_legs, finestra trade-history TP_PHANTOM, validazione feed a monte, dedup minori. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+57
-8
@@ -76,13 +76,24 @@ def _quantize_step(value: float, step: float, mn: float) -> float:
|
||||
return float(max(n_steps * Decimal(str(step)), Decimal(str(mn))))
|
||||
|
||||
|
||||
def quantize_price(instrument: str, price: float) -> float:
|
||||
def quantize_price(instrument: str, price: float, side: str | None = None) -> float:
|
||||
"""Arrotonda il prezzo al tick dello strumento (Decimal, niente artefatti
|
||||
float) — richiesto per gli ordini limit (Deribit rifiuta prezzi off-tick)."""
|
||||
float) — richiesto per gli ordini limit (Deribit rifiuta prezzi off-tick).
|
||||
|
||||
side (code-review 2026-06-11): per un limit di USCITA il rounding al tick
|
||||
piu' vicino puo' mettere il resting OLTRE il livello sim → un tocco esatto
|
||||
del livello non filla MAI il resting e il gate TP_PHANTOM classificherebbe
|
||||
phantom un tocco genuino, sistematicamente. 'sell' = floor (mai sopra il
|
||||
livello), 'buy' = ceil (mai sotto): il resting e' sempre raggiungibile
|
||||
quando il sim dichiara il tocco (costo: <=1 tick di PnL vs sim)."""
|
||||
import math
|
||||
tick = contract_spec(instrument).get("tick")
|
||||
if not tick or price <= 0:
|
||||
return price
|
||||
return float(round(price / tick) * Decimal(str(tick)))
|
||||
n = (math.floor(price / tick) if side == "sell"
|
||||
else math.ceil(price / tick) if side == "buy"
|
||||
else round(price / tick))
|
||||
return float(n * Decimal(str(tick)))
|
||||
|
||||
|
||||
def notional_to_amount(instrument: str, notional_usd: float,
|
||||
@@ -143,8 +154,8 @@ def _merge_close_fills(ro: Fill, net: Fill, requested: float) -> Fill:
|
||||
ro.fee_coin + net.fee_coin, ro.fee_usd + net.fee_usd,
|
||||
net.order_id or ro.order_id, net.order_state or ro.order_state,
|
||||
verified, raw={"reduce_only": ro.raw, "net": net.raw},
|
||||
notes=(f"netting: reduce-only {fa}/{requested}, residuo {fb} "
|
||||
f"market puro ({net.order_state})"),
|
||||
notes=(f"netting: reduce-only {fa}/{requested} (oid {ro.order_id}), "
|
||||
f"residuo {fb} market puro ({net.order_state}, oid {net.order_id})"),
|
||||
filled_amount=tot)
|
||||
|
||||
|
||||
@@ -316,13 +327,48 @@ class ExecutionClient:
|
||||
fill = self._submit(instrument, opp, amount, 0.0,
|
||||
reduce_only=True, label=label)
|
||||
residual = amount - fill.filled_amount
|
||||
residual = _quantize_step(residual, spec["step"], spec["min"]) if residual > 0 else 0.0
|
||||
# check sulla POLVERE prima di quantizzare: _quantize_step clampa al lotto
|
||||
# minimo, quindi un residuo da artefatto float (1e-17) diventerebbe un
|
||||
# ordine nudo da un lotto intero (code-review 2026-06-11)
|
||||
if residual < spec["step"] / 2:
|
||||
return fill
|
||||
residual = _quantize_step(residual, spec["step"], spec["min"])
|
||||
# GUARD stato-stantio (code-review 2026-06-11): il residuo NON-reduce-only
|
||||
# e' consentito solo fino al gap (conto reale − libri degli ALTRI worker −
|
||||
# orfani) nella direzione della chiusura. Se la nostra quota non esiste
|
||||
# piu' sul conto (disaster-SL scattato in outage, flatten manuale, stato
|
||||
# stantio al resume) il gap e' ~0 → NIENTE ordine nudo: si ritorna il
|
||||
# parziale onesto (il chiamante alza REAL_CLOSE_PARTIAL). Se il guard non
|
||||
# e' calcolabile (rete) si resta FAIL-SAFE: meglio un orfano tracciato
|
||||
# che una posizione nuda non tracciata.
|
||||
allowed = self._net_close_allowance(instrument, opp, label)
|
||||
if allowed is None or allowed < spec["step"] / 2:
|
||||
fill.notes = (fill.notes + " | " if fill.notes else "") + (
|
||||
f"netting NEGATO: residuo {residual} ma gap conto-libri "
|
||||
f"{'non calcolabile' if allowed is None else round(allowed, 6)} "
|
||||
"(stato stantio? quota gia' chiusa?)")
|
||||
return fill
|
||||
if allowed < residual:
|
||||
residual = _quantize_step(allowed, spec["step"], spec["min"])
|
||||
net = self._submit(instrument, opp, residual, 0.0, reduce_only=False,
|
||||
label=f"{label}|net" if label else "net")
|
||||
return _merge_close_fills(fill, net, amount)
|
||||
|
||||
def _net_close_allowance(self, instrument: str, close_side: str,
|
||||
self_worker: str | None) -> float | None:
|
||||
"""Quanto residuo non-reduce-only e' GIUSTIFICATO dai libri: gap firmato
|
||||
fra il conto reale e (libri degli altri worker + orfani registrati),
|
||||
proiettato nella direzione della chiusura. None = non calcolabile."""
|
||||
try:
|
||||
from src.live.books import real_books, account_net
|
||||
books, orphans = real_books(exclude_worker=self_worker)
|
||||
target = books.get(instrument, 0.0) + orphans.get(instrument, 0.0)
|
||||
pos = account_net(self.client).get(instrument, 0.0)
|
||||
gap = pos - target # quota nostra ancora sul conto (firmata)
|
||||
return max(0.0, gap if close_side == "sell" else -gap)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def place_tp_limit(self, instrument: str, entry_side: str, amount: float,
|
||||
tp_price: float, label: str | None = None) -> Fill:
|
||||
"""LIMIT reduce-only appoggiato al livello TP (lato opposto all'entrata):
|
||||
@@ -333,7 +379,7 @@ class ExecutionClient:
|
||||
TP il limit crossa e filla subito (state 'filled'); la riconciliazione a
|
||||
valle (resting_fills) lo gestisce come un fill normale."""
|
||||
opp = "sell" if entry_side == "buy" else "buy"
|
||||
px = quantize_price(instrument, tp_price)
|
||||
px = quantize_price(instrument, tp_price, side=opp)
|
||||
if amount <= 0 or px <= 0:
|
||||
return Fill(instrument, opp, 0.0, amount, None, 0.0, 0.0,
|
||||
None, None, False, notes="amount/tp non validi")
|
||||
@@ -474,7 +520,10 @@ class PairsExecutionClient:
|
||||
unwound = False
|
||||
for f, inst in ((fa, inst_a), (fb, inst_b)):
|
||||
if f.verified and f.amount > 0:
|
||||
self.leg.close_amount(inst, f.side, f.amount, label=label)
|
||||
# unwind del FILLATO, non del richiesto: col fallback netting un
|
||||
# amount sovrastimato non viene piu' cappato in silenzio dal
|
||||
# reduce-only — chiuderebbe quota altrui (code-review 2026-06-11)
|
||||
self.leg.close_amount(inst, f.side, f.filled_amount, label=label)
|
||||
unwound = True
|
||||
return PairFill(False, fa, fb, unwound=unwound,
|
||||
notes=f"leg-fail (a={fa.verified} b={fb.verified}), unwound={unwound}")
|
||||
|
||||
Reference in New Issue
Block a user