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:
@@ -106,6 +106,7 @@ class PairsWorker:
|
||||
self.real_entry_fee = 0.0
|
||||
self.real_trades = 0
|
||||
self.real_first_notified = False
|
||||
self.orphan_legs: list[dict] = [] # gambe respinte dal netting (persistite)
|
||||
|
||||
self._load_state()
|
||||
self._save_state()
|
||||
@@ -146,6 +147,7 @@ class PairsWorker:
|
||||
self.real_entry_fee = s.get("real_entry_fee", 0.0)
|
||||
self.real_trades = s.get("real_trades", 0)
|
||||
self.real_first_notified = s.get("real_first_notified", False)
|
||||
self.orphan_legs = s.get("orphan_legs", [])
|
||||
self._log("RESUME", {"capital": round(self.capital, 2),
|
||||
"total_trades": self.total_trades, "in_position": self.in_position,
|
||||
"real_capital": round(self.real_capital, 2),
|
||||
@@ -167,6 +169,7 @@ class PairsWorker:
|
||||
"real_notional_a": self.real_notional_a, "real_notional_b": self.real_notional_b,
|
||||
"real_entry_fee": self.real_entry_fee, "real_trades": self.real_trades,
|
||||
"real_first_notified": self.real_first_notified,
|
||||
"orphan_legs": self.orphan_legs,
|
||||
}
|
||||
with open(self.status_path, "w") as f:
|
||||
json.dump(state, f, indent=2)
|
||||
@@ -247,23 +250,41 @@ class PairsWorker:
|
||||
pf = self.executor.close_pair(self.inst_a, self.inst_b, self.real_side_a,
|
||||
self.real_side_b, self.real_amount_a, self.real_amount_b,
|
||||
label=self.worker_id)
|
||||
# VERITA' PER-GAMBA (audit 2026-06-11): una gamba puo' essere RESPINTA dal
|
||||
# netting di conto (reduce-only nel verso sbagliato quando un altro worker e'
|
||||
# nella direzione opposta sullo stesso strumento). Prima il PnL veniva
|
||||
# calcolato col prezzo SIM per la gamba mai eseguita e sommato al ledger
|
||||
# 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)
|
||||
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
|
||||
da, db = self.real_dir, -self.real_dir
|
||||
gross = (da * (exit_a - self.real_entry_a) / self.real_entry_a * self.real_notional_a
|
||||
+ db * (exit_b - self.real_entry_b) / self.real_entry_b * self.real_notional_b)
|
||||
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 - self.real_entry_fee - exit_fee
|
||||
real_pnl = ((gross_a if ok_a else 0.0) + (gross_b if ok_b else 0.0)
|
||||
- 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,
|
||||
"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})
|
||||
if not pf.verified:
|
||||
self._notify("REAL_CLOSE_FAILED", {"worker": self.worker_id, "note": pf.notes})
|
||||
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,
|
||||
"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")})
|
||||
self.real_in_position = False
|
||||
self.real_dir = 0
|
||||
self.real_side_a = self.real_side_b = ""
|
||||
@@ -272,7 +293,10 @@ class PairsWorker:
|
||||
self.real_notional_a = self.real_notional_b = 0.0
|
||||
self.real_entry_fee = 0.0
|
||||
self._save_state()
|
||||
return real_pnl, bool(pf.verified or pf.leg_a.verified or pf.leg_b.verified)
|
||||
# applied (real-truth) SOLO se entrambe le gambe hanno chiuso verificate:
|
||||
# con una gamba orfana il "PnL reale dello spread" non esiste -> meglio il
|
||||
# fallback sim DICHIARATO che un numero mezzo-reale
|
||||
return real_pnl, ok_a and ok_b
|
||||
|
||||
def _close(self, ca: float, cb: float, z: float, reason: str):
|
||||
if not self.in_position:
|
||||
|
||||
Reference in New Issue
Block a user