PINK: fix fee-sign bug + WARN-unfreeze — 451/451 tests green

Defect A (fee sign): bingx_user_stream._normalise_order flipped to
  fee = -raw_fee so BingX negative-n costs arrive as positive kernel
  costs.  k_maker_rebates no longer accumulates phantom rebates.

Defect B (opening fee dropped): fill_qty now falls back to "z"
  (cumFilledQty) when "l" (lastFilledQty) is zero/absent, so
  apply_predicted_fill computes a non-zero opening-leg fee.

Architectural fix (WARN unfreezes): lib.rs reconcile() now unfreezes
  capital_frozen on WARN as well as OK.  WARN (0.01-20 USDT delta) is
  normal in-flight settlement — only ERROR (≥20, unexplained) should
  halt ENTERs.  The old keep-state logic trapped the kernel permanently
  frozen after the first trade's ENTER predicted-fee phase pushed delta
  briefly into ERROR.

Acceptance criterion: |k_capital - bingx_balance| < 1 USDT, frozen=False
after every round-trip trade — verified numerically against T-1/T-2
ground truth from the CRITICAL doc.

Docs: CRITICAL_AGENT-TODO_ACCOUNTING_BUGFIX.md §12-13 (fix record),
      CAPITAL_BOOKKEEPING_DESIGN.md §8 (kernel spec), SYSTEM_BIBLE §11.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Codex
2026-06-08 11:08:31 +02:00
parent 7e83a5c5c5
commit e38ec77221
8 changed files with 2455 additions and 11 deletions

View File

@@ -333,15 +333,22 @@ class BingxUserStream:
str(o.get("o") or o.get("type") or "MARKET").upper() == "LIMIT"
and status in {"FILLED", "PARTIALLY_FILLED"}
))
# Fees: BingX sends commission as positive for costs, negative for rebates
# DEFECT A FIX: BingX sends "n" (commission) NEGATIVE for costs, POSITIVE for
# rebates on VST. Kernel convention is the opposite: POSITIVE = cost, NEGATIVE
# = rebate. Flip at this boundary so the kernel never sees wrong-sign fees.
raw_fee = _safe_float(o.get("n") or 0.0)
fee = raw_fee # may be negative (rebate)
fee = -raw_fee # BingX cost (negative n) → kernel cost (positive fee)
# DEFECT B FIX: prefer "l" (lastFilledQty) when non-zero; fall back to "z"
# (cumFilledQty) for ENTER fills where BingX reports qty only in "z".
_last_qty = _safe_float(o.get("l") or 0.0)
_cum_qty = _safe_float(o.get("z") or o.get("cumFilledQty") or 0.0)
fill_qty = _last_qty if _last_qty > 0.0 else _cum_qty
return ExchangeEvent(
kind=kind,
event_id=str(o.get("i") or o.get("orderId") or uuid.uuid4().hex),
exchange_ts=ts,
fill_price=_safe_float(o.get("L") or o.get("ap") or o.get("p")),
fill_qty=_safe_float(o.get("l") or o.get("lastFilledQty") or 0.0),
fill_qty=fill_qty,
fee=fee,
fee_asset=str(o.get("N") or ""),
realized_pnl=_safe_float(o.get("rp") or o.get("realizedPnl") or 0.0),