book_execute: pos_after di nuovo nel jsonl, letta subito dopo il rebalance in un try (#39)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-27 17:42:34 +00:00
parent d34b0a81d8
commit ae887a3d98
2 changed files with 105 additions and 10 deletions
+17 -10
View File
@@ -85,14 +85,16 @@ 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):
def _registra_fill(f, *, asset: str, act: str, net: float, bar_ts, tp_frac: float, skh_sign: int,
pos_after: float | None = None):
"""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'."""
`pos_after` arriva dal chiamante (#39 — regressione di #23): nel ramo normale di `_run` si
legge SUBITO dopo il rebalance, in un try che isola la lettura dalla registrazione dei fill;
resta None quando la lettura solleva o quando il passo e' un `RebalanceParziale` (a meta')."""
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
@@ -102,7 +104,7 @@ def _registra_fill(f, *, asset: str, act: str, net: float, bar_ts, tp_frac: floa
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,
verified=f.verified, notes=f.notes, net_target=net, pos_after=pos_after,
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))
@@ -332,17 +334,22 @@ def _run():
_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:
_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).
# newpos si legge SUBITO dopo il rebalance, in un try che ISOLA la lettura dalla
# registrazione dei fill (#39, regressione di #23: dal merge di #23 pos_after era
# sempre None, perche' la lettura restava un side-effect informativo dopo il ciclo
# e il suo risultato non arrivava piu' a `_registra_fill`). Se solleva, i fill si
# registrano comunque, con pos_after=None — il vincolo #23 (un fill non si perde
# per un reconcile mancato) resta: il try isola solo la LETTURA, non la scrittura.
try:
newpos = trader.position_usd(inst)
pos_after = newpos
print(f" reconcile: pos ${newpos:,.0f}")
except Exception as e: # noqa: BLE001 — non e' un fill perso, e' un reconcile mancato
pos_after = None
print(f" reconcile: pos illeggibile ({type(e).__name__})")
for f in 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"], pos_after=pos_after)
if do_execute:
ds = trader.ensure_disaster_sl(inst, sl_pct) # bracket su posizione NETTA (adatta long/short)
stato = ds.get("state")