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:
+34
-13
@@ -224,7 +224,9 @@ class PairsWorker:
|
||||
self.real_in_position = True
|
||||
self.real_dir = d
|
||||
self.real_side_a, self.real_side_b = pf.leg_a.side, pf.leg_b.side
|
||||
self.real_amount_a, self.real_amount_b = pf.leg_a.amount, pf.leg_b.amount
|
||||
# amount FILLATO, non richiesto (coerente con strategy_worker, 2026-06-11)
|
||||
self.real_amount_a = pf.leg_a.filled_amount or pf.leg_a.amount
|
||||
self.real_amount_b = pf.leg_b.filled_amount or pf.leg_b.amount
|
||||
self.real_entry_a = pf.leg_a.fill_price or sim_a
|
||||
self.real_entry_b = pf.leg_b.fill_price or sim_b
|
||||
self.real_notional_a = pf.leg_a.amount * self.real_entry_a
|
||||
@@ -240,11 +242,12 @@ class PairsWorker:
|
||||
|
||||
def _real_close_pair(self, sim_a: float, sim_b: float, reason: str,
|
||||
sim_pnl: float) -> tuple[float | None, bool]:
|
||||
"""Chiusura REALE shadow: richiude entrambe le gambe reduce-only, riconcilia
|
||||
PnL reale (per gamba) e fee, aggiorna il ledger reale parallelo.
|
||||
"""Chiusura REALE shadow: richiude entrambe le gambe (netting-aware),
|
||||
riconcilia PnL reale per-gamba e fee, aggiorna il ledger reale parallelo.
|
||||
|
||||
Ritorna (real_pnl, applied): applied=True se almeno una gamba ha chiuso con
|
||||
fill verificato (PnL reale utilizzabile come verita' in real-truth)."""
|
||||
Ritorna (real_pnl, applied): applied=True SOLO se ENTRAMBE le gambe hanno
|
||||
chiuso per intero con fill verificato — con una gamba orfana il "PnL dello
|
||||
spread" non esiste e real-truth ricade sul sim DICHIARATO."""
|
||||
if not self.real_in_position:
|
||||
return None, False
|
||||
pf = self.executor.close_pair(self.inst_a, self.inst_b, self.real_side_a,
|
||||
@@ -257,12 +260,25 @@ class PairsWorker:
|
||||
# reale (3 PnL fantasma il 2026-06-11, gamba ETH orfana sul conto).
|
||||
# Ora: si booka SOLO il realizzato delle gambe con fill verificato; la gamba
|
||||
# respinta diventa un ORFANO registrato (persistito) + alert Telegram.
|
||||
ok_a, ok_b = bool(pf.leg_a.verified), bool(pf.leg_b.verified)
|
||||
from src.live.execution import contract_spec
|
||||
for leg in (pf.leg_a, pf.leg_b):
|
||||
if "netting" in (getattr(leg, "notes", "") or ""):
|
||||
# reduce-only cappato/respinto, residuo in market puro (v1.1.25)
|
||||
self._log("NET_CLOSE", {"instrument": leg.instrument, "note": leg.notes})
|
||||
self._notify("NET_CLOSE", {"instrument": leg.instrument, "note": leg.notes})
|
||||
# verita' per-FRAZIONE di gamba (code-review 2026-06-11): una gamba puo'
|
||||
# chiudere PARZIALMENTE (reduce-only cappato + netting negato/fallito) —
|
||||
# si booka il gross della sola frazione FILLATA e l'orfano registra il
|
||||
# solo RESIDUO (prima: gross binario tutto-o-niente e orfano a amount
|
||||
# pieno, che falsava reconciler e real_capital della parte gia' chiusa).
|
||||
filled_a = min(getattr(pf.leg_a, "filled_amount", 0.0), self.real_amount_a)
|
||||
filled_b = min(getattr(pf.leg_b, "filled_amount", 0.0), self.real_amount_b)
|
||||
step_a = contract_spec(self.inst_a).get("step", 0.001)
|
||||
step_b = contract_spec(self.inst_b).get("step", 0.001)
|
||||
ok_a = filled_a >= self.real_amount_a - step_a / 2
|
||||
ok_b = filled_b >= self.real_amount_b - step_b / 2
|
||||
frac_a = filled_a / self.real_amount_a if self.real_amount_a else 0.0
|
||||
frac_b = filled_b / self.real_amount_b if self.real_amount_b else 0.0
|
||||
exit_a = pf.leg_a.fill_price or sim_a
|
||||
exit_b = pf.leg_b.fill_price or sim_b
|
||||
# PnL per gamba: dir A = +d (long ratio compra A), dir B = -d
|
||||
@@ -270,26 +286,31 @@ class PairsWorker:
|
||||
gross_a = da * (exit_a - self.real_entry_a) / self.real_entry_a * self.real_notional_a
|
||||
gross_b = db * (exit_b - self.real_entry_b) / self.real_entry_b * self.real_notional_b
|
||||
exit_fee = pf.leg_a.fee_usd + pf.leg_b.fee_usd
|
||||
real_pnl = ((gross_a if ok_a else 0.0) + (gross_b if ok_b else 0.0)
|
||||
real_pnl = (gross_a * frac_a + gross_b * frac_b
|
||||
- self.real_entry_fee - exit_fee)
|
||||
self.real_capital += real_pnl
|
||||
self.real_trades += 1
|
||||
self._log("REAL_CLOSE_PAIR", {
|
||||
"reason": reason, "exit_a": exit_a, "exit_b": exit_b,
|
||||
"leg_a_ok": ok_a, "leg_b_ok": ok_b,
|
||||
"filled_a": filled_a, "filled_b": filled_b,
|
||||
"real_pnl_usd": round(real_pnl, 4), "sim_pnl_usd": round(sim_pnl, 4),
|
||||
"entry_fee": round(self.real_entry_fee, 5), "exit_fee": round(exit_fee, 5),
|
||||
"real_capital": round(self.real_capital, 4), "verified": pf.verified})
|
||||
for ok, inst, side, amt in ((ok_a, self.inst_a, self.real_side_a, self.real_amount_a),
|
||||
(ok_b, self.inst_b, self.real_side_b, self.real_amount_b)):
|
||||
if not ok and amt > 0:
|
||||
orphan = {"instrument": inst, "entry_side": side, "amount": amt,
|
||||
for ok, inst, side, amt, filled, step in (
|
||||
(ok_a, self.inst_a, self.real_side_a, self.real_amount_a, filled_a, step_a),
|
||||
(ok_b, self.inst_b, self.real_side_b, self.real_amount_b, filled_b, step_b)):
|
||||
residue = amt - filled
|
||||
if not ok and residue >= step / 2:
|
||||
orphan = {"instrument": inst, "entry_side": side,
|
||||
"amount": round(residue, 8),
|
||||
"ts": datetime.now(timezone.utc).isoformat(), "reason": reason}
|
||||
self.orphan_legs.append(orphan)
|
||||
self._notify("PAIR_LEG_ORPHAN", {
|
||||
"worker": self.worker_id, **orphan,
|
||||
"note": ("gamba NON chiusa (reduce-only respinto dal netting di "
|
||||
"conto?): posizione orfana sul conto, intervento richiesto")})
|
||||
"note": ("gamba NON chiusa per il residuo indicato (netting "
|
||||
"negato/fallito): posizione orfana sul conto — "
|
||||
"risolvere e RIMUOVERE l'orfano dallo status")})
|
||||
self.real_in_position = False
|
||||
self.real_dir = 0
|
||||
self.real_side_a = self.real_side_b = ""
|
||||
|
||||
Reference in New Issue
Block a user