82c05f6f81
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>
90 lines
3.9 KiB
Python
90 lines
3.9 KiB
Python
"""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)
|