Review of PINK_ACCOUNTING_EXEC_FIX execution found the Phase 3.2 repair
path triply broken: (1) !closed guard blocked repair on terminal fills —
the common price-less case; (2) wrapper on_account_event was a raw FFI
passthrough so repairs never settled into published capital; (3) live
FILL_SETTLED carried no slot_id and realized_pnl=0 (pre-folded) — repair
was dead code. Fixes: repair allowed on closed slots (flag+dedup keep it
idempotent); wrapper settles the baseline diff on FILL_SETTLED-with-slot_id;
dedicated repair_realized_pnl field avoids double-folding the K-ledger;
_FakeKernelAccount fixture mirrors the Phase 1 anchor_to_exchange contract.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two related accounting fixes:
1. _calibrate_fee_model startup guard: before calling calibrate_fee,
compute raw deviation from the published taker/maker rate (ignoring
any stale calibration_ratio). If >15%, skip and log WARNING rather
than letting a bad REST fill set calibration_ratio to ~0.8 and cause
ESTIMATED fees to understate actuals by 20% for the entire session.
2. fee_settled_events trade_id: BingX WS does not echo back our
clientOrderId in fill events (field "c" is empty). Was falling back
to BingX's internal orderId (p-e-mq5.../p-x-mq5...) which can't
be joined to trade_events. Now reads trade_id from kernel slot 0
(which retains the trade_id until the next ENTER) so
fee_settled_events.trade_id = BTCUSDT-T-N. Added venue_order_id
field to persist_fee_settled for bidirectional reconciliation.
128/128 tests green.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_reconcile_position_slot passed all N BingX positions (all slot_id=0) to
reconcile_from_slots; with N>1 the kernel silently took one and forgot the
rest. Now: sort by size desc, take only the largest, log ERROR naming every
ignored orphan symbol. Caller must flatten exchange to 0 before restarting.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
S1 — Leverage cache (bingx_direct.py):
_ensure_leverage(): per-symbol asyncio.Lock + cached value check; skips ~350ms
POST when exchange already has the requested leverage. Saves ~350ms/trade.
Cache updated ONLY on success; failed POST leaves cache stale → correct retry.
Persist: JSON sidecar /tmp/.bingx_leverage_cache_{env}.json; survives restarts.
connect(): _verify_leverage_drift() detects when another process changed leverage
at the exchange and updates cache to exchange truth (logs WARNING on drift).
Multi-runner contract: leverage is account-level on BingX; documented that
concurrent runners with different leverage desires for same symbol conflict.
20 mock tests: same-lev skip, change-triggers-POST, failure-no-cache-update,
concurrent-same-symbol (lock prevents race), drift-detect, persist/restore,
multi-runner known-limitation documentation test.
S2 — Background state refresh (bingx_direct.py):
MARKET fills: asyncio.create_task(_refresh_state_background) — does not block
submit path. WS FILL_SETTLED + ACCOUNT_UPDATE deliver capital truth anyway.
LIMIT fills: synchronous refresh retained (include_history=False, not True) —
needed to detect resting order state for next pump cycle.
Saves ~600–900ms/trade on MARKET exits. ENTER similarly improved.
Gap 1 — VenueEvent friction fields (contracts.py):
Added: fee, fee_asset, fee_source, is_maker, exchange_ts, slippage_bps,
mark_at_submit — all with defaults so existing callers are unaffected.
Detailed inline docs for sign conventions and provenance codes.
Gap 2 — Fee estimation + WS_SETTLED provenance (bingx_direct.py, pink_clickhouse.py):
submit_intent: estimates fee from fill_price × fill_qty × taker/maker rate;
annotates ack_row with _fee_estimated, _fee_source, _is_maker_est.
persist_fee_settled(): new method writes fee_settled_events row when WS
ORDER_TRADE_UPDATE delivers actual commission ("n" field); fee_source="WS_SETTLED".
pink_direct._run_account_stream: calls persist_fee_settled on FILL_SETTLED.
Gap 3 — Slippage measurement (bingx_direct.py, bingx_venue.py, pink_clickhouse.py):
Captures mark_at_submit before the order POST; computes slippage_bps signed
by side: positive = adverse (taker overpaid / maker undersold), negative =
price improvement. Measured for BOTH taker and maker fills for symmetry.
Flows through VenueEvent → trade_events.slippage_bps + trade_exit_legs.slippage_bps.
S3 / SOR — Maker order placement: comprehensive TODO block in submit_intent with:
SHORT/LONG-aware price offset design, OBF integration requirements,
TODO_ADD_PARAMSET_VIBRISS for spread_bps threshold, intelligent timeout_s
calibration requirements, price-impact awareness gap, SOR abstraction CRITICAL TODO.
REST/WS split: documented why BingX (and all retail venues) separate these
and why a unified VenueAdapter protocol is the long-term solution.
151/151 existing tests green + 20 new leverage cache tests = 171 total.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously: set_seed_capital(hardcoded_25000) then on_account_event(BingX_100K+)
→ reconcile delta ~75K → capital_frozen=True → no trades allowed.
Fix: _fetch_exchange_wallet_balance() queries BingX wallet balance BEFORE
seeding the kernel. set_seed_capital() and the subsequent ACCOUNT_UPDATE
reconcile now agree → delta ≈ 0 → capital_frozen=False → sizing correct.
Falls back to DOLPHIN_INITIAL_CAPITAL if BingX is unreachable, with WARNING.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TUI Hz fix:
- hazelcast_projection.py: write_engine_snapshot now writes all NAUTILUS-era
field aliases (trades_executed, current_leverage, open_positions as list,
last_scan_number, last_vel_div, vol_ok, open_notional) so gear_rows/capital
panel work with no TUI changes.
- dolphin_status_pink.py: _normalize_eng_for_tui() safety-net translation added;
render() uses it on every Hz read.
DC gate (SYSTEM BIBLE §4.2, champion config):
- pink_direct.py: _dc_contradicts() — 7-tick lookback, 0.75 bps threshold.
Rising price (chg > 0.75 bps) blocks ENTER via dataclasses.replace(HOLD, DC_CONTRADICT).
Price history deque initialized in connect(); dc_skip_contradicts=True enforced.
ACB boost (SYSTEM BIBLE §10):
- hazelcast_feed.py: fix wrong key "latest_acb" → "acb_boost" (DOLPHIN_FEATURES key
written by acb_processor_service.py).
- pink_direct.py: _last_acb_boost read from scan_payload["acb_boost"] first (scan
bridge may embed it), then Hz direct fallback. Applied to intent.leverage via
dataclasses.replace() after IntentEngine.plan(), capped at 3x.
- _last_scan_number, _last_vel_div, _last_vol_ok tracked from scan_payload.
OBF gate: NOT implemented. OBF shards (DOLPHIN_FEATURES_SHARD_*) require new
Hz map connections + symbol routing. Gap documented; requires separate decision.
Tests: TestDCGate (5) + TestNormalizeEngForTui (5) — 10 new, 104 total, all green.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ExchangeFeeConfig in AccountState:
taker_rate, maker_rate, lot_step, tick_size, funding_interval_secs
calibration_ratio: EMA of actual/expected, updated on every fill
Kernel now predicts fees at fill time (PREDICTED_FILL event):
k_capital updated immediately without waiting for WS FILL_SETTLED
When actual fee arrives, prediction is replaced and ratio recalibrated
Reconcile delta: 0.000000 (was ~0.9 USDT in canary without prediction)
Calibration loop on connect():
Fetches recent fill history, validates model vs exchange actuals
deviation < 1pct -> OK; < 5pct -> WARN; >= 5pct -> ERROR (pre-trade gate)
New FFI: dita_kernel_set_exchange_config_json, dita_kernel_calibrate_fee_json
New ExecutionKernel methods: set_exchange_config(), calibrate_fee()
pink_direct.py: loads BingX fee config on connect, calibrates before stream
131/131 offline pass.
Root cause (harness multi_leg, ~14-TRX residual): pink_direct rebuilds the legacy
TradePosition from the kernel slot every step, but left exit_leg_index=0, so
IntentEngine.next_exit_ratio() consumed ratio[0] (0.5) on EVERY leg and never
advanced to the final leg's 1.0:
leg1: 0.5×53 ≈ 26 closed -> 27 remain
leg2: 0.5×27 ≈ 13 closed -> 14 RESIDUAL (kernel believes flat, exchange isn't)
Fix: propagate the kernel slot's authoritative active_leg_index into the rebuilt
legacy position's exit_leg_index, so the intent engine consumes the correct leg
ratio. The final leg now closes the full remaining -> fully flattens.
Verified: offline 18 green (no regression); live VST harness multi_leg now closes
fully (XPASS) — residual gone, all 6 capital invariants hold. xfail mark removed;
capital-accounting battery is now fully green (7/7) on testnet.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Located the source of the cutover non-finite: target_size = capital × fraction ×
leverage / price. notional (capital×fraction×leverage) is self-limiting (no division,
bounded by capital), so a non-finite size can only come from a corrupt raw input —
non-finite capital, or a price below the industry floor that overflows the division.
Guards in the PINK algo runner (pink_direct), per design review:
- _MIN_SANE_PRICE = 1e-8 industry-smallest-price floor.
- ENTER: _unsafe_entry_reason() rejects the OPEN (logs provenance, no trade) when
capital/leverage/size are non-finite/non-positive or price < floor. A corrupt sizing
input is an untrustworthy signal — don't open (nothing to strand).
- EXIT: _exit_intent_from_slot() sizes the close from the kernel's authoritative
slot.size (cap to remaining; full remaining if policy size malformed) — a bad-math
exit can never strand or overshoot a position. Falls back to policy size only when the
kernel reports no/unknown remaining size.
size semantics confirmed: base-asset QUANTITY; notional = size×price; margin = notional/
leverage ≈ 0.2×capital (already margin-bounded by construction — no extra clamp needed).
Tests: test_pink_sizing_guards.py (4) green; full offline suite 25 green (no regression).
Complements the kernel INVALID_INTENT guard (9168cf0): source refuses to produce bad
sizes; kernel rejects any that slip through.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The aborted hard cutover crash-looped with "Rust kernel returned null string" from
process_intent on the first live trading step. Root cause (reproduced): a non-finite
(inf/NaN) numeric field reaching the kernel — Python json.dumps emits the Infinity/NaN
token, serde_json rejects it at parse, and the FFI returned null. Magnitude is fine;
only finiteness was the problem.
Defense in depth, kernel catches it:
- Rust FFI (lib.rs): dita_kernel_process_intent_json / _on_venue_event_json now return
a clean INVALID_INTENT KernelResult on parse failure (incl. Infinity/NaN tokens) AND
on serialize failure (a non-finite produced internally) — never a null string.
- Python bridge (rust_backend.py): ExecutionKernel.process_intent validates intent
finiteness/bounds (target_size, reference_price, limit_price, leverage, exit_leg_ratios;
size>=0) BEFORE the FFI and rejects INVALID_INTENT, naming the offending field+value.
- contracts.py: add KernelDiagnosticCode.INVALID_INTENT.
- pink_direct.py: on INVALID_INTENT, log full upstream provenance (snapshot.price,
capital, leverage, sizes) so the numerical SOURCE can be located on the next live run.
- on_venue_event bridge tolerates the fallback's null slot (uses the live slot).
Verified: kernel recompiled; offline 65 + 7 new guard tests green (no regression);
direct-FFI inf payload -> INVALID_INTENT (no null crash). NOTE: this turns the cutover
crash into a clean rejection — the upstream source of the non-finite (the live run's
inf) still needs locating, now aided by the provenance log.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First commit of the previously-untracked PINK-on-DITAv2 migration system
(execution moves to the Rust kernel; policy stays on legacy DITA, so Alpha
Engine algorithmic integrity is preserved). BLUE is untouched.
Sprint 0 (safety snapshot + flaw-fix verification, MARKET single-leg scope):
- Verified Rust FSM fixes (flaws 2,4,10,11,13) by source read of lib.rs.
- Hardened 5 vacuous/guarded assertions in test_flaws.py so each flaw test
genuinely exercises its fix. Most important: Flaw 5 now asserts capital
moves by EXACTLY realized PnL (was entering/exiting at the same price).
- Offline suites: 533 passed, 0 failed (35 flaws + 402 kernel/accounting/
bridge + 96 runtime/persistence/multi-exit/restart/seams).
- GATE PASS: MARKET-path-critical flaws 1,2,5 confirmed fixed + green.
- Added SPRINT0_FLAW_VERIFICATION.md report and _rust_kernel/.gitignore
(excludes Rust target/ build artifacts).
LIMIT/partial-fill remain explicitly out of scope (MARKET-only bring-up).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>