Files
PythagorasGoal/scripts/analysis/reconcile_account.py
T
Adriano Dal Pastro 82c05f6f81 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>
2026-06-11 21:36:30 +00:00

90 lines
3.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Reconciler READ-ONLY conto Deribit vs libri dei worker (primo passo verso il
position-manager, audit 2026-06-11: il conto era short 0.027 ETH oltre i libri e
nessuno se n'era accorto per ore).
Confronta, per ogni strumento USDC:
atteso = Σ quote reali dei worker (status.json persistiti: fade/DIP/SH single-leg
+ pairs a 2 gambe) + Σ gambe ORFANE registrate (orphan_legs: posizioni
che il conto ha ancora ma i libri hanno chiuso — drift SPIEGATO)
reale = get_positions(currency=USDC) (size USD / mark = amount in coin)
Drift oltre tolleranza (1.5×step del contratto) -> tabella + alert Telegram
`ACCOUNT_DRIFT` (con --telegram). Anti-race: se al primo passaggio c'e' drift,
rilegge libri+conto dopo qualche secondo e segnala solo se persiste (un worker
poteva essere a meta' di un open/close).
Nessun ordine, nessuna modifica di stato: SOLO lettura.
uv run python scripts/analysis/reconcile_account.py # stampa
uv run python scripts/analysis/reconcile_account.py --telegram # + alert se drift
"""
from __future__ import annotations
import sys
import time
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
from src.live.cerbero_client import CerberoClient
from src.live.execution import contract_spec
from src.live.books import real_books, account_net # fonte UNICA dei libri (usata
# anche dal guard del netting)
RECHECK_SLEEP = 10 # anti-race: secondi fra i due passaggi
TOL_STEPS = 1.5 # tolleranza = 1.5 × step contratto
def compute_drift(client: CerberoClient | None = None) -> list[dict]:
client = client or CerberoClient()
books, orphans = real_books()
acct = account_net(client)
rows = []
for inst in sorted(set(books) | set(orphans) | set(acct)):
exp = books.get(inst, 0.0) + orphans.get(inst, 0.0)
real = acct.get(inst, 0.0)
step = contract_spec(inst).get("step", 0.001)
tol = TOL_STEPS * step
rows.append(dict(inst=inst, books=books.get(inst, 0.0),
orphans=orphans.get(inst, 0.0), exp=exp, real=real,
drift=real - exp, tol=tol,
ok=abs(real - exp) <= tol))
return rows
def main():
rows = compute_drift()
bad = [r for r in rows if not r["ok"]]
if bad:
# anti-race: un worker poteva essere a meta' open/close -> ricontrolla
print(f"drift su {len(bad)} strumenti: ricontrollo fra {RECHECK_SLEEP}s (anti-race)...")
time.sleep(RECHECK_SLEEP)
rows = compute_drift()
bad = [r for r in rows if not r["ok"]]
print(f"{'strumento':<22}{'libri':>10}{'orfani':>9}{'atteso':>10}{'conto':>10}{'drift':>10} esito")
for r in rows:
print(f"{r['inst']:<22}{r['books']:>10.4f}{r['orphans']:>9.4f}{r['exp']:>10.4f}"
f"{r['real']:>10.4f}{r['drift']:>+10.4f} {'OK' if r['ok'] else '⚠️ DRIFT'}")
if not rows:
print("(nessuna posizione attesa ne' reale)")
print("\nESITO:", "OK — conto allineato ai libri" if not bad
else f"⚠️ DRIFT PERSISTENTE su {len(bad)} strumenti")
if bad and "--telegram" in sys.argv:
from src.live.telegram_notifier import notify_event
notify_event("ACCOUNT_DRIFT", {
"strumenti": {r["inst"]: {"atteso": round(r["exp"], 5),
"conto": round(r["real"], 5),
"drift": round(r["drift"], 5)} for r in bad},
"note": ("conto != libri worker oltre tolleranza (drift NON spiegato dagli "
"orfani registrati): verificare close cappati/gambe respinte — "
"vedi docs/diary/2026-06-11-system-audit.md")})
print("[telegram] alert ACCOUNT_DRIFT inviato")
return bool(bad)
if __name__ == "__main__":
sys.exit(1 if main() else 0)