book_execute: SOLO PROTEZIONE su equity/SKH ignoti; fill mai persi su rebalance parziale (#21, #22, #23)
Prima, con equity reale non leggibile il giro dimensionava comunque un ordine sul paper_cap ($2.000 nominali) e lo mandava; con SKH ignoto il target restava quello (fail-safe solo nel segno). Ora: se non si sa quanto vale il conto o qual e' il segnale, nessun rebalance_signed — ma il disaster-SL si controlla LO STESSO per ogni asset (dipende da posizione/mark, non da equity/segnale). Giro degradato -> exit 2, solo se armato (non in anteprima dry-run). Fill persi: rebalance_signed puo' eseguire un fill (es. il close di un flip) e poi sollevare nel passo successivo (l'open dall'altra parte) — quel fill spariva perche' la lettura di `newpos`, PRIMA del giro che lo registrava, andava in eccezione a sua volta. Estratta la registrazione in _registra_fill(...), chiamata SUBITO dopo ogni fill (anche quelli di RebalanceParziale, nuova eccezione di execution.py con .fills/.causa) e PRIMA di leggere la posizione; la lettura ora e' in un try separato, non fatale (pos_after=None + "reconcile: pos illeggibile" se fallisce). Aggiornati due test che assumevano il comportamento vecchio (eq_fallback "prosegue" sul paper capital) e il guard sulla sorgente di test_tradesdb.py (bar_ts ora e' un parametro di _registra_fill, non piu' `r['last_data']` inline). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -46,7 +46,7 @@ PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from src.live.book import ScalaNonAutorizzata, book_report
|
||||
from src.live.execution import DeribitTrader
|
||||
from src.live.execution import DeribitTrader, RebalanceParziale
|
||||
from src.live.notifier import notify
|
||||
from src.live.venue_probe import diagnose, errori_dal_report
|
||||
|
||||
@@ -85,6 +85,31 @@ def log_event(rec: dict):
|
||||
f.write(json.dumps(rec) + "\n")
|
||||
|
||||
|
||||
def _registra_fill(f, *, asset: str, act: str, net: float, bar_ts, tp_frac: float, skh_sign: int):
|
||||
"""Registra UN fill (print + log_event + notify). SUBITO dopo l'ordine, PRIMA di leggere la
|
||||
posizione risultante: un ordine eseguito non deve dipendere dalla lettura della posizione
|
||||
successiva per essere salvato (#23 — un 502 su `position_usd()` dopo un fill riuscito lo
|
||||
faceva sparire dal log, perche' la lettura veniva PRIMA del giro `for f in fills`).
|
||||
|
||||
`pos_after` e' sempre None qui: la posizione dopo il rebalance si legge (best-effort) DOPO
|
||||
aver registrato tutti i fill, in `_run` — vedi il commento li'."""
|
||||
print(f" -> {f.side.upper()} {f.filled:.4f} @ ${f.price or 0:,.1f} fee {f.fee_usdc:.5f} "
|
||||
f"({'OK' if f.verified else 'NON VERIFICATO: ' + f.notes})")
|
||||
# `ts_utc` = ORA VERA del fill. Fino al 2026-08-23 qui c'era la data della
|
||||
# BARRA di segnale (`r['last_data']`): 19 righe su 19 a 00:00:00, e un trade
|
||||
# registrato SEI GIORNI prima di essere eseguito (ETH 0.04 del 14/07, scritto
|
||||
# 08/07). La barra resta, sotto il suo nome: `bar_ts`.
|
||||
log_event(dict(ts_utc=datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
||||
bar_ts=str(pd.Timestamp(bar_ts)), asset=asset, action=act,
|
||||
side=f.side, filled=f.filled, price=f.price, fee=f.fee_usdc,
|
||||
verified=f.verified, notes=f.notes, net_target=net, pos_after=None,
|
||||
tp_frac=tp_frac, skh_sign=skh_sign))
|
||||
det = dict(asset=asset, side=f.side, amount=round(f.filled, 4), price=round(f.price or 0, 1),
|
||||
fee=round(f.fee_usdc, 5), net=round(net, 0))
|
||||
notify(f"✅ BOOK {act}" if f.verified else "⚠️ BOOK ORDINE NON VERIFICATO",
|
||||
det if f.verified else {**det, "notes": f.notes})
|
||||
|
||||
|
||||
def _run():
|
||||
cfg = load_config()
|
||||
want_execute = "--execute" in sys.argv[1:]
|
||||
@@ -129,7 +154,9 @@ def _run():
|
||||
print(f" ultima barra : {r['last_data']}\n")
|
||||
|
||||
if r.get("skh_error"): # SKH feed fallito -> book.py ha forzato flat IN SILENZIO
|
||||
print(f" ⚠️ SKH FEED ERRORE (SKH forzato flat!): {r['skh_error']}")
|
||||
# (#22) Non e' piu' "SKH forzato flat!": book_execute non manda PIU' ordini basati su
|
||||
# quel segnale ignoto (modo SOLO PROTEZIONE, sotto) — i target restano informativi.
|
||||
print(f" ⚠️ SKH FEED ERRORE (SKH non calcolato: nessun ordine, solo protezione): {r['skh_error']}")
|
||||
notify("⚠️ BOOK — SKH feed fallito (sleeve forzato flat)", {"error": r["skh_error"]})
|
||||
|
||||
# --- FRESCHEZZA del feed 5m usato per il segnale SKH (2026-07-26) --------------------------
|
||||
@@ -220,11 +247,24 @@ def _run():
|
||||
"azione": "rebuild_history.py --asset BTC ETH"})
|
||||
return
|
||||
|
||||
if r.get("eq_fallback"): # equity reale non leggibile -> sizing su paper_cap
|
||||
print(f" ⚠️ EQUITY FALLBACK (sizing su paper_cap, NON blocco): {r['eq_fallback']}")
|
||||
if do_execute: # solo diagnostica: l'hard-cap $/asset limita il downside
|
||||
if r.get("eq_fallback"): # equity reale non leggibile -> niente sizing sul conto
|
||||
print(f" ⚠️ EQUITY FALLBACK (nessun ordine, solo protezione): {r['eq_fallback']}")
|
||||
if do_execute: # (#21) segnalato: il modo SOLO PROTEZIONE e' sotto
|
||||
notify("⚠️ BOOK LIVE — equity fallback (sizing su paper_cap)", {"nota": r["eq_fallback"]})
|
||||
|
||||
# --- SOLO PROTEZIONE (#21 equity ignota, #22 SKH ignoto) ------------------------------------
|
||||
# Principio unico: quando il giro non sa QUANTO vale il conto o QUALE e' il segnale, NON manda
|
||||
# ordini di ribilancio (dimensionare un ordine su un numero che non si conosce e' peggio che
|
||||
# non ordinare) -> ma CONTROLLA LO STESSO il disaster-SL di ogni asset: la protezione dipende
|
||||
# da posizione e mark, non da equity o segnale. Stesso principio che pos_error applica gia'
|
||||
# bloccando l'esecuzione, qui senza saltare la protezione.
|
||||
solo_protezione: str | None = r.get("eq_fallback") or r.get("skh_error")
|
||||
if solo_protezione and do_execute:
|
||||
titolo = ("⚠️ BOOK LIVE — equity non leggibile: nessun ordine, solo protezione"
|
||||
if r.get("eq_fallback") else
|
||||
"⚠️ BOOK LIVE — SKH non calcolato: nessun ordine, solo protezione")
|
||||
notify(titolo, {"nota": solo_protezione})
|
||||
|
||||
# Salto di equity: un versamento (conferma che il sizing lo ha seguito) oppure un movimento
|
||||
# NON richiesto (prelievo, perdita anomala). In entrambi i casi va detto subito, e con il
|
||||
# nuovo dimensionamento accanto, cosi' il messaggio si legge senza aprire il repo.
|
||||
@@ -256,7 +296,12 @@ def _run():
|
||||
sk = a["skh_state"]
|
||||
sk_txt = "flat" if sk == "flat" else f"{sk['dir']}@{sk.get('entry')}"
|
||||
order = a["order"]
|
||||
if order is None:
|
||||
if solo_protezione:
|
||||
# (#21, #22) nessun rebalance: l'azione stampata lo dice, non e' "a target" — e'
|
||||
# "non tocco perche' non so quanto vale il conto / qual e' il segnale".
|
||||
motivo_breve = solo_protezione if len(solo_protezione) <= 60 else solo_protezione[:60] + "…"
|
||||
act = f"HOLD (solo protezione: {motivo_breve})"
|
||||
elif order is None:
|
||||
act = "HOLD (a target)"
|
||||
elif order.get("is_close"):
|
||||
act = f"CLOSE ${cur:,.0f}"
|
||||
@@ -268,26 +313,28 @@ def _run():
|
||||
f"| pos ${cur:+,.0f} -> {act}")
|
||||
|
||||
try:
|
||||
if do_execute and order is not None:
|
||||
fills = trader.rebalance_signed(inst, net, mark, min_usd=min_order)
|
||||
newpos = trader.position_usd(inst)
|
||||
if do_execute and order is not None and not solo_protezione:
|
||||
try:
|
||||
fills = trader.rebalance_signed(inst, net, mark, min_usd=min_order)
|
||||
except RebalanceParziale as rp:
|
||||
# (#23) almeno un fill e' passato prima che il resto del passo sollevasse
|
||||
# (es. flip: close ok, open KO) -> si registra quel fill SUBITO (non e' un
|
||||
# fill perso), poi si rilancia la causa nell'handler di isolamento esistente.
|
||||
for f in rp.fills:
|
||||
_registra_fill(f, asset=asset, act=act, net=net, bar_ts=r["last_data"],
|
||||
tp_frac=a["tp_frac"], skh_sign=a["skh_sign"])
|
||||
raise rp.causa
|
||||
for f in fills:
|
||||
print(f" -> {f.side.upper()} {f.filled:.4f} @ ${f.price or 0:,.1f} fee {f.fee_usdc:.5f} "
|
||||
f"({'OK' if f.verified else 'NON VERIFICATO: ' + f.notes})")
|
||||
# `ts_utc` = ORA VERA del fill. Fino al 2026-08-23 qui c'era la data della
|
||||
# BARRA di segnale (`r['last_data']`): 19 righe su 19 a 00:00:00, e un trade
|
||||
# registrato SEI GIORNI prima di essere eseguito (ETH 0.04 del 14/07, scritto
|
||||
# 08/07). La barra resta, sotto il suo nome: `bar_ts`.
|
||||
log_event(dict(ts_utc=datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
||||
bar_ts=str(pd.Timestamp(r['last_data'])), asset=asset, action=act,
|
||||
side=f.side, filled=f.filled, price=f.price, fee=f.fee_usdc,
|
||||
verified=f.verified, notes=f.notes, net_target=net, pos_after=newpos,
|
||||
tp_frac=a["tp_frac"], skh_sign=a["skh_sign"]))
|
||||
det = dict(asset=asset, side=f.side, amount=round(f.filled, 4), price=round(f.price or 0, 1),
|
||||
fee=round(f.fee_usdc, 5), net=round(net, 0), pos_after=round(newpos, 0))
|
||||
notify(f"✅ BOOK {act}" if f.verified else "⚠️ BOOK ORDINE NON VERIFICATO",
|
||||
det if f.verified else {**det, "notes": f.notes})
|
||||
print(f" reconcile: pos ${newpos:,.0f}")
|
||||
_registra_fill(f, asset=asset, act=act, net=net, bar_ts=r["last_data"],
|
||||
tp_frac=a["tp_frac"], skh_sign=a["skh_sign"])
|
||||
# newpos e' un RECONCILE informativo, letto DOPO aver registrato i fill (#23): se
|
||||
# questa lettura solleva, i fill sono gia' salvi (sopra), e non e' un guasto del
|
||||
# giro (isolamento) -> si segnala e si continua (disaster-SL sotto, comunque).
|
||||
try:
|
||||
newpos = trader.position_usd(inst)
|
||||
print(f" reconcile: pos ${newpos:,.0f}")
|
||||
except Exception as e: # noqa: BLE001 — non e' un fill perso, e' un reconcile mancato
|
||||
print(f" reconcile: pos illeggibile ({type(e).__name__})")
|
||||
if do_execute:
|
||||
ds = trader.ensure_disaster_sl(inst, sl_pct) # bracket su posizione NETTA (adatta long/short)
|
||||
stato = ds.get("state")
|
||||
@@ -336,7 +383,10 @@ def _run():
|
||||
print(" => Esecuzione completata (vedi data/live/book_executions.jsonl).")
|
||||
# Uscita non-zero se il giro e' stato DEGRADATO: il log del cron deve poterlo contare senza
|
||||
# rileggere la prosa. Non cambia nulla operativamente (cron_book.sh non ha `set -e`).
|
||||
if scoperti or falliti:
|
||||
# (#21, #22) SOLO PROTEZIONE e' degradato anche senza asset falliti/scoperti: il giro non ha
|
||||
# ribilanciato nulla perche' non sapeva quanto vale il conto o qual era il segnale. Solo se
|
||||
# il giro era ARMATO: in DRY-RUN non si e' comunque tentato nulla, e' solo un'anteprima.
|
||||
if scoperti or falliti or (solo_protezione and do_execute):
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user