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:
Adriano Dal Pastro
2026-06-11 20:05:42 +00:00
parent 8a2b065dd7
commit 429fa01c97
9 changed files with 234 additions and 21 deletions
+19 -2
View File
@@ -387,6 +387,7 @@ def run(config_path: str = "portfolios.yml"):
# non solleva eccezione ma i worker dell'asset mancante saltano il tick in silenzio.
_OUTAGE_POLLS = 5
fail_streak = 0
worker_err_streak: dict[str, int] = {} # errori consecutivi per worker (isolamento tick)
def _outage_tick(failed: bool, streak: int, detail: str = "") -> int:
"""Aggiorna lo streak e gestisce gli alert FEED_OUTAGE (start a soglia, una
@@ -468,11 +469,27 @@ def run(config_path: str = "portfolios.yml"):
# single (fade/dip): StrategyWorker su feed live.
w.tick(res[s.asset])
# isolamento per-worker (audit 2026-06-11): un'eccezione in un tick NON
# deve saltare gli altri worker (= exit non valutati per tutti) ne'
# l'update dell'equity. Streak per-worker -> alert dopo 5 fail di fila.
def _tick_safe(s, w):
try:
_tick(s, w)
worker_err_streak.pop(s.sid, None)
except Exception as e:
n = worker_err_streak.get(s.sid, 0) + 1
worker_err_streak[s.sid] = n
print(f"[runner] errore worker {s.sid}: {e} (streak {n}; gli altri proseguono)")
if n == 5:
from src.live.telegram_notifier import notify_event
notify_event("WORKER_ERROR_STREAK",
{"worker": s.sid, "streak": n, "errore": str(e)[:200]})
for s in live_specs:
_tick(s, workers[s.sid])
_tick_safe(s, workers[s.sid])
# PAPER: ticcati per statistica, MAI nel ledger del portafoglio
for s in paper_specs:
_tick(s, paper_workers[s.sid])
_tick_safe(s, paper_workers[s.sid])
ledger.update_equity({sid: _worker_equity(wk) for sid, wk in workers.items()})