_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>
- migrate_pink_sink_schema.sql: ALTER TABLE adds fee/fee_source/is_maker/
slippage_bps/mark_at_submit/exchange_ts to trade_events and trade_exit_legs;
CREATE TABLE fee_settled_events (was missing entirely). DDL already applied.
- monitor_pink.py: wire real PinkClickHousePersistence so monitor roundtrips
write to dolphin_pink CH tables. Adds _make_decision_intent() helper; calls
persist_step() after ENTER and EXIT. Persistence failure is non-fatal (warns
and continues). 42 persistence tests green.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause 1 (http.py): duplicate signature= in POST body — canonical_query
included signature key after build_signed_params injected it, then body
appended &signature= again. Fix: exclude 'signature' from canonical.
Root cause 2 (bingx_direct + http.py): HTTP retry sent same MARKET order to
backup URL (bingx.pro), which hits the same VST account. Without clientOrderId,
each retry opened a new SHORT position; EXIT BUY 10 only closed one. Fix:
restore clientOrderId in hyphen format p-{e/x}-{base36_ts}-{rand4} (pure
alphanumeric rejected by VST; hyphen format accepted). Adds max_retries_override
+ urls_to_try to _request_json for non-idempotent override path.
Root cause 3 (flat_and_start_pink): k.venue.connect() ran backend.connect()
inside asyncio.run() in a thread-pool. httpx session created there references
a dead event loop; order POSTs raise RuntimeError("Event loop is closed").
Fix: await adapter.connect() directly from main event loop.
Also: enter_wall_ms + tight _is_our_position createTime filter to separate
PINK's position from concurrent strategies on shared VST account. 1.5s
settle sleep before flat check.
New test suite test_bingx_http_safety.py: 20 tests covering idempotency,
retry correctness, backup-URL dedup, event-loop hygiene, signing correctness.
Live result: ENTER 290ms, EXIT 260ms — both sub-second. Position flat.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bug fixes:
1. bingx_venue.py: add cancel_async() — async cancel that awaits backend.cancel()
directly in the main event loop. The sync cancel() path goes through _run()
→ thread-pool → asyncio.run() in a new thread, but aiohttp is bound to the
main loop → deadlock. Identical root cause as the old sync submit() → fixed
via submit_async. Remove dead cancel_order branch (BingxDirectExecutionAdapter
has cancel, not cancel_order).
2. rust_backend.py: process_intent_async CANCEL path now uses cancel_async when
available (matching the submit_async pattern for ENTER/EXIT). Sync cancel()
fallback kept for MockVenueAdapter compat.
3. bingx_direct.py: guard S2 background refresh task per symbol. Old code discarded
the task reference; rapid submits piled up concurrent _refresh_state_background
calls all writing self._state in arbitrary completion order (stale last-writer-
wins). Now: skip creating a new task if one is already pending for the symbol;
store reference and clear via done-callback.
Test additions (test_bingx_bugs.py, 29 tests):
- cancel_async: awaitable, calls backend.cancel directly, maps all statuses
- process_intent_async CANCEL: dispatches cancel_async / falls back to sync
- S2 guard: task stored, no duplicates while pending, new task after done
- _events_from_submit with None snapshots: FILLED/NEW/REJECTED/PARTIAL/RATE_LIMITED
- _filled_size_from_snapshots(None, None): safe 0.0 return
- _events_from_cancel: before/after completely ignored
- connect(): no double refresh_state, no-op if backend has no connect
- submit() sync with None snapshots: FULL_FILL still emitted
- cancel() branch audit: uses cancel not cancel_order, raises for no-cancel backend
Fix: test_exchange_event_seam_parity.py TestMockSubscribe — replace deprecated
asyncio.get_event_loop().run_until_complete() with asyncio.run() (Python 3.12
raises RuntimeError when event loop is closed by earlier suite tests).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix 1: connect() — remove redundant _backend_snapshot(include_history=True).
backend.connect() already called refresh_state(); this was a second identical
network round-trip at startup (~400ms wasted).
Fix 2: cancel() — remove snapshot_before + snapshot_after. _events_from_cancel
never reads 'before' or 'after' — two gratuitous round-trips per cancel with
zero benefit.
Fix 3: submit() (sync/legacy path) — drop both _backend_snapshot calls, pass
None like submit_async already does. Receipt executedQty fields take precedence;
_filled_size_from_snapshots returning 0.0 is the correct safe fallback.
117/117 tests pass (2 pre-existing pytest-ordering failures in TestMockSubscribe
are unrelated — asyncio.get_event_loop() contamination from other suite files,
25/25 pass when the file runs alone).
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>
Startup roundtrip now verifies persistence accounting inline:
- Wires PinkClickHousePersistence to a mock sink after EXIT
- Checks trade_events: exit_price != entry_price, pnl finite, capital finite
- Checks trade_exit_legs: pnl_leg, exit_qty, exit_price all populated
- Logs ALL FIELDS CORRECT / ISSUES DETECTED
Flatness check scoped to PINK's traded asset only (other strategies
may have open positions; those are reported informational, not failure).
Adds _normalize_asset() for TRX-USDT → TRXUSDT symbol normalization.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
G21/E23/A13 — exit_price used entry_price (every trade had exit_price==entry_price):
_write_trade_event: exit_price = fill_price_hint > intent.reference_price > decision.reference_price
_write_trade_exit_leg: same priority chain via fill_price_hint parameter
persist_result: extracts fill_price_hint from FULL_FILL/PARTIAL_FILL events in outcome
persist_fill_events: intent.reference_price = actual fill price → propagates correctly
A14 — entry_bar was active_leg_index (exit leg counter, not bar count):
_write_position_state: entry_bar = intent.bars_held (0 when intent is None)
A15 — persist_recovery_state used acc_dict as slot_dict (trade_id always ""):
Now reads kernel.slot(0).to_dict() when kernel is wired; trade_id from real slot
External-position exit_qty=0 fix:
_write_trade_exit_leg: when prev_size<=0 (no prior ENTER tracked), falls back to
initial_size or intent.target_size so exit legs for reconcile-detected positions are meaningful
exit_qty field added to trade_exit_legs rows (was computed but not emitted)
NaN tracing (_checked_float):
Introduces _checked_float() wrapper that logs WARNING + writes anomaly_events spool
row on NaN/inf in financial fields; applied to realized_pnl in exit paths
29 new persistence unit tests (mocked) + chaos/fuzz suite:
exit_price correctness, capital ordering, pnl_leg incremental, entry_bar,
recovery trade_id, external position exits, multi-leg, restart-mid-trade, NaN/None fields
164/164 total (97 flaws + 25 kernel reliability + 29 persistence + 13 phase4) green
FLAWS doc: pass 6 — G21/E23/A13/A14/A15 closed; 26 total fixed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CLI: python flat_and_start_pink.py [--flatten] [--no-start]
--flatten : cancel all orders + MARKET-close all positions (correct side
by positionAmt sign, not abs()); verifies account flat after
--no-start: flatten only, skip roundtrip
(no flags): startup roundtrip only — ENTER/EXIT via process_intent_async()
Startup roundtrip exercises the full N2/N3/N4 async hot path:
process_intent_async → submit_async → await backend.submit_intent →
BingX POST → on_venue_event(ORDER_ACK+FULL_FILL) → POSITION_OPEN → CLOSED
Min-order detection: queries /quote/contracts for tradeMinQty/minOrderQuantity;
fallback 10 units. Fixes the 0.001-TRX rejection that BingX returned.
Bugs fixed in flatten:
- positionAmt sign was lost via abs(); SHORT positions now correctly use BUY
(positionAmt < 0) vs SELL for LONG (positionAmt > 0) with reduceOnly=true
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TestFSMOccupancyAndRollback covers the invariants that prevent orphaned
exchange orders — the production failure mode where multiple positions
accumulated because slot state wasn't rolled back on submit failure:
- ENTRY_WORKING blocks new ENTER (different trade_id → SLOT_BUSY)
- POSITION_OPEN blocks new ENTER
- venue.submit raise → synthetic REJECTED → FSM back to IDLE
- After rollback slot immediately reusable
- N consecutive submit failures never strand the slot
- submit-fail then success → exactly 1 position, not N
- 20 rapid enter→exit cycles leave no residual state
- EXIT on IDLE always rejected (no phantom closes)
- 5 assets, 1 slot → only first accepted, rest SLOT_BUSY
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When venue.submit() raises (BingX timeout / network error), the Rust FSM
had already advanced to ORDER_REQUESTED/ENTRY_WORKING with no corresponding
exchange order — stranding the slot. Every subsequent ENTER for a different
asset hit SLOT_BUSY, preventing recovery without a restart. Restarts create
a fresh IDLE kernel, leaving the orphaned exchange position unmanaged.
Fix: catch submit exceptions, synthesise an ORDER_REJECT VenueEvent, feed it
through on_venue_event() so the FSM rolls back to IDLE atomically. The slot
is free on the next cycle with no orphan on the exchange.
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>
Completes the ctypes lifetime audit. All eight FFI call sites now
assign _to_rust_bytes() to a local var before passing to c_char_p,
ensuring the bytes object lives for the full duration of the Rust call.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: intent.timestamp was a naive datetime (no tzinfo). isoformat()
produces '2026-06-04T14:26:55.098914' (26 chars). The JSON prefix
'{"timestamp":"' is 14 chars → closing quote lands at column 41. Rust's
chrono::DateTime<Utc> serde rejects naive timestamps and serde_json reports
the error as 'premature end of input at line 1 column 41'.
Fix: _utc_isoformat() attaches UTC tzinfo before isoformat(), producing
'2026-06-04T14:26:55.098914+00:00' which chrono accepts.
Previous null-byte fix (_to_rust_bytes) and dangling-pointer fix (local vars)
remain correct and address real separate failure modes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs causing INVALID_INTENT_PARSE at FFI boundary:
1. Dangling pointer: ctypes.c_char_p stores a raw C pointer without
incrementing the Python refcount. Temporaries passed inline are freed
by CPython before the Rust FFI call executes, giving Rust a dangling
pointer whose freed memory looks like truncated JSON (column 41).
Fix: assign bytes to local vars (_pb/_mb/_vb) to hold refs alive.
2. venue.submit guard: process_intent() called venue.submit() even when
the kernel returned INVALID_INTENT, cascading a 30s BingX timeout
into a fatal crash. Fix: gate on outcome.accepted.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_to_rust_bytes() centralises all Python→Rust JSON serialisation:
- _json_null_clean() strips U+0000 from all string values recursively
- ensure_ascii=True guarantees no 0x00 in output bytes
- All _json() call sites migrated; mode/verbosity now .encode("ascii")
- 9 null-safety unit tests added to TestRustBytesNullSafety
Root cause: ctypes.c_char_p silently truncates at first 0x00 byte,
causing serde_json "premature end of input at column 41" on EXIT intents
with BNB-USDT leverage values. Long-term fix: Rust FFI (ptr, len) pairs.
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>
Twenty-first pass: no ABI compatibility check on Rust .so load stale binary
corrupts silently (X1 Critical), real_zinc_plane _write_region zeroes entire
buffer before write visible all-zero window (X2 Critical), no requirements.txt
setup.py pyproject.toml zero Python dependency declarations (X3 Critical),
RealZincControlPlane.update() no thread lock concurrent calls corrupt seq and
shared memory (X4 High), libc declared in Cargo.toml never used dead dependency
(X5 High), 5 test files hardcoded sys.path.insert non-portable (X6 High),
_decode_packet no try/except on json.loads partial body read crashes reader (X7
High), ExchangeEvent not exported from __init__.py package API inconsistency (X8
High), RealZincPlane and RealZincControlPlane collide on {prefix}_control region
name (X10 Medium). 375 total flaws across 21 passes.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Twentieth pass: int() on 3 env vars uncaught ValueError (W1 Critical),
DITA_V2_PREFIX default "dita_v2" multi-process shared memory corruption (W2
Critical), funding sign opposite Python V2 vs Rust same raw value opposite
capital effect (W3 Critical), listenKeyExpired frames silently swallowed
continue skips expiry check dead code (W4 Critical), RECV_WINDOW_MS no upper
bound replay attacks (W5 High), ACTIVE_SLOT_LIMIT stored never enforced by
Rust kernel (W6 High), no fill history fetched during WS reconnect gap-backfill
fills lost (W7 High), rate limit detection fails on HTTP 429 no matching
message instant retry (W8 High), CONTROL_PLANE=REAL_ZINC silently falls back
to in-memory (W9 High), all BingxHttpError mapped to REJECTED can't distinguish
errors (W10 High), os.environ bracket access vs .get() inconsistent (W11 High).
361 total flaws across 20 passes.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Nineteenth pass: DITAv2LauncherBundle.close() never calls kernel.close() Rust
handle leaks via __del__ (V1 Critical), BingxVenueAdapter no close/disconnect
ThreadPoolExecutor/HTTP never release (V2 Critical), 3 generators write same
output file last writer wins incompatible prologues (V4 Critical), generated
tests triple env-gated never run in CI dead code (V5 Critical), kernel.close()
destroys Rust handle immediately no drain no flush UAF risk (V6 Critical),
process_intent ENTER doesn't clear seen_event_ids old dedup pollutes new trade
(V3 High), no conftest/pytest.ini/asyncio_mode test discovery fragile (V9 High),
#[serde(default)] leverage:0.0 mark_price no .max(1.0) silent accounting error
(V8 Medium). 347 total flaws across 19 passes.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
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.
Ninth pass: VenueEvent.price=0 causes 100% PnL loss (L3), available_margin
set to wrong field in user stream (L4), wallet_balance defaults to 0 (L5),
14+ bugs fixed between backup and current code (L12), real pipeline never
tested by any test function (L13), no proxy support (L9), 5-min DNS cache
(L10). Backup diff reveals the current Rust kernel has ~14 bugs fixed vs
the backup version. 16 new flaws, 215 total.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
- I1 (Critical/Rust): apply_fill accumulated partial fills instead of
overwriting. WS events carry lastFilledQty (incremental); previous code
set slot.size = fill_size each time. Now accumulates via prev_filled.
initial_size set from intended_size on first fill, not from fill amount.
- G2 (Critical/Rust): into_c_string unwrap() panicked on any NUL byte in
serialized JSON. Now sanitizes NUL bytes before CString construction;
never panics.
- G3 (Critical/Rust): EXIT intent transition hardcoded prev_state=
POSITION_OPEN. Captured actual fsm_state before mutation so audit trail
is accurate when EXIT is received from non-standard states.
- I13 (High/Rust): stray venue event could reactivate a closed slot.
Added explicit slot.closed guard in on_venue_event — returns
TERMINAL_STATE with accepted=false before any FSM mutation.
- I18 (High/Python): sys.path.insert(0, ...) in real_zinc_plane.py and
real_control_plane.py gave Zinc adapter directory highest import
priority. Changed to sys.path.append() so existing path entries take
precedence.
35/35 offline tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Third and deepest pass across all module boundaries, data transforms, and
error paths. 30 new flaws found (F1-F30), including the highest-risk single
flaw: an unprotected on_venue_event loop that leaves slots unrecoverable on
any exception.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
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>
Automated, parametrized scenario battery driving the REAL PINK runtime
(policy->intent->PinkDirectRuntime->kernel->BingX VST->persistence) via crafted
snapshots, asserting 6 capital invariants per scenario: (1) per-fill Δcapital ==
realized; (2) end Δcapital == Σ per-fill realized (cumulative across cycles, since
slot.realized resets on ENTER); (3) exchange flat + no orders (signed); (4)
persistence parity (trade_events/account_events/trade_exit_legs vs kernel); (5)
notional ≤ capital×max_leverage; (6) guard correctness.
Controlled testnet: flat-start, single scenarios, reliable kernel close-all
pre-flatten (no cross-scenario cascade), ~$20 sizes, no autonomous loop, BLUE
untouched. Gated +PINK_CAPITAL_HARNESS.
Live VST result: 6 passed + 1 xfail.
- round_trip, sequential (multi-cycle continuity), exit_then_reentry, and 3 guard
paths (suppressed nonfinite-capital / sub-floor price, degenerate-snapshot HOLD)
all GREEN — capital accounting verified for the single-leg/sequential PINK paths.
- multi_leg XFAIL (documented): multi-leg partial reduce-only exits leave a lot-step
rounding residual on the venue (kernel believes flat, exchange has a remainder) —
a real capital/sizing-path finding for the rework, reliably reproduced.
This is the pre-cutover gate; the multi-leg residual + capital/sizing-path rework
are the next items before re-attempting the live cutover.
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>
L3 live validation surfaced a live-only defect: a working LIMIT order could not
be cancelled (MARKET never exercised cancel — synchronous fills).
Two coupled fixes:
- Rust FSM (lib.rs): propagate the venue's order id onto the active order for
ALL order types and event kinds (ACK/partial/full fill) whenever the exchange
provides one — orders are created at submit with an empty venue_order_id, so a
later cancel had no real id to reference. Only fills empty ids, never overwrites.
Requires recompiling libdita_v2_kernel.so.
- Backend (bingx_direct.py): add cancel(order) — a properly-signed DELETE by
orderId (clientOrderId fallback) with TRUTH-BASED confirmation: BingX can return
transient errors ("order not exist", dup-within-1s from an internal retry) even
when the order was removed, so the cancel succeeds iff the order is no longer
open on the venue. The venue adapter prefers this backend cancel over its raw
signed_delete fallback (which failed signature with an empty id).
Validated:
- Offline: 63 + new cancel-truth unit tests green (no regression post-recompile).
- Live VST: resting SHORT LIMIT (+5%) rests as ENTRY_WORKING, confirmed as a LIMIT
open order, cancel -> CANCEL_ACK -> IDLE, exchange flat (test_pink_limit_live.py).
- Live VST MARKET run-through re-validated post-recompile: PASS, exact capital
reconciliation, two-phase rows visible (ORDER_REQUESTED + ENTRY_FILLED/EXIT).
LIMIT remains execution-infra only; PINK policy stays MARKET. BLUE untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drives the FULL PINK stack against BingX VST (not kernel-direct):
DecisionEngine -> IntentEngine -> PinkDirectRuntime -> kernel -> BingX venue
-> AccountProjection -> PinkClickHousePersistence (captured).
Forces a real SHORT enter (STRUCTURAL_DISLOCATION) + fixed-TP exit and asserts:
the policy layer ran, all dolphin_pink row families were written (incl. terminal
trade_events + trade_exit_legs), exact capital reconciliation, exchange flat.
Verified live: 1 passed, terminal rows captured (on_venue_event settles inline
within process_intent for MARKET orders). Gated by +PINK_RUNTHROUGH.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sprint 2 (accounting + observability parity, PINK scope):
- Verified pink_clickhouse.py writes the 8 BLUE-legacy row families at
matching schema and that capital authority in pink_direct.step() is
solely kernel.account (no balance-poll overwrite in the hot loop).
- Report: prod/clean_arch/dita_v2/SPRINT2_ACCOUNTING_PARITY.md.
Sprint 3 offline groundwork (no exchange contact):
- Add _write_trade_exit_leg to pink_clickhouse.py: one BLUE-schema-faithful
trade_exit_legs row per exit leg, with isolated (non-cumulative) per-leg
deltas tracked via _leg_state (reset on ENTER). Closes the docstring gap.
- New offline suite test_pink_multi_exit_groundwork.py (3 passed):
* Flaw 4 — two-leg exit closes once, realized accrues per leg, closed
slot rejects further EXIT (no double-close).
* Overshoot invariant — a final EXIT requesting more than the remaining
size CLAMPS (size to 0, no oversell), retiring the Sprint 0 cumulative-
ratio risk empirically.
* trade_exit_legs delta + full BLUE column-set assertions.
- Persistence regression after edits: 10 passed.
BLUE untouched: no changes to dolphin.* / DOLPHIN_*_BLUE / nautilus_event_trader.py.
Live VST multi-leg run remains deferred pending explicit authorization.
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>