Commit Graph

68 Commits

Author SHA1 Message Date
Codex
bdc54fbeaa dita_v2(exec): INDETERMINATE submit is not REJECTED — unknown is never flat
A BingX read-timeout/reset/5xx after send means the answer was lost, not that
the order failed. Classify every submit failure by what it PROVES:
NOT_ATTEMPTED / REFUSED -> rollback sound; INDETERMINATE -> point-lookup our
own clientOrderId (read-only, bounded, never a reconcile); unresolved stays
UNKNOWN — no synthetic REJECT, no slot rollback, E-feed FILL settles truth.

- prod/bingx/http.py: BingxHttpError.effect + order_may_exist, 9 raise sites tagged
- adapters/bingx_direct.py: _lookup_own_order_by_client_id (never POSTs)
- dita_v2/venue.py: VenueIndeterminateError(VenuePostAckError) — existing fences catch it
- dita_v2/bingx_venue.py: both submit paths escalate INDETERMINATE receipts
- 14 tests incl. kernel no-rollback invariant + genuine-REFUSED contrast

Suite: 3416 passed, 19 skipped, 3 xfailed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 15:43:55 +02:00
Codex
cc7ac35169 dita_v2(test): pin indeterminate FSM state 2026-07-13 10:53:42 +02:00
Codex
ada14f04b3 dita_v2(test): assert semantic event emptiness 2026-07-13 10:45:42 +02:00
Codex
a3e2fc6528 dita_v2(test): preserve indeterminate post-ack state 2026-07-13 10:42:07 +02:00
Codex
3d75e4b59d dita_v2: unknown != flat (VenuePostAckError) + lossless telemetry lane
BUG CLASS (doc: BUGCLASS_INDETERMINATE_OUTCOME_20260713.md): an operation with an external
side effect has THREE outcomes — NOT_ATTEMPTED, ATTEMPTED_REFUSED, ATTEMPTED_INDETERMINATE
— and rollback is sound only for the first two. Collapsing the third into 'failed' is what
orphaned 6 live SHORTs: a post-ack TypeError reached rust_backend's 'except Exception ->
synthetic REJECTED -> FSM rollback', which asserted 'no order exists' about an order that
was already filled. Telemetry never had a veto; it hijacked the failure channel.

FIXES (no new seams, no re-architecture):
- venue.py: VenuePostAckError — typed channel meaning THE EFFECT EXISTS. Carries receipt.
- bingx_venue submit/submit_async: point-of-no-return fence. Post-ack bookkeeping failures
  raise VenuePostAckError instead of a bare exception.
- rust_backend (BOTH submit paths): catch VenuePostAckError FIRST -> no synthetic REJECT,
  no rollback. Slot stays working; E-feed FULL_FILL / reconcile settles the truth.

LOSSLESS TELEMETRY (HJ: 'DITAv2 exists precisely because seams dropped 40% of inputs'):
drop-oldest is data loss and is GONE. Exec path appends O(1) to an unbounded queue and
returns. A SEPARATE spiller thread (which never touches the plane, so a wedged plane cannot
starve it) parks the backlog above HWM into a durable append-only spool; the publisher
replays the spool when the plane recovers.
Proven: wedged-forever plane + 200k records -> 0 dropped, 195903 durable on disk, 4096 in
memory, 7.4 us/call on the exec path. Lossless AND memory-bounded. Healthy plane: 2000/2000.

STILL BROKEN, flagged to codex: the pre-ack branch rolls back on TIMEOUT — but a timeout is
the definition of INDETERMINATE (the order may have filled). Same bug class, older, live.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 10:31:39 +02:00
Codex
b13b54e769 dita_v2(test): enforce post-ack atomicity and suite integrity 2026-07-13 10:03:06 +02:00
Codex
40e85ab8d1 dita_v2(bingx_venue): telemetry OFF the exec path — bounded non-blocking lane
Standing steer (HJ, months): non-blocking behaviour + explicit execution priority.
Telemetry is not on the ladder at all, yet it sat ON the order path, inline and able to
raise — which is how it orphaned 6 live positions.

Exec path now does exactly one thing: pack primitives, append to a bounded ring, return.
No plane write, no snapshot construction, no I/O, no lock, no raise. Snapshot build +
plane publish move to a daemon drain lane. Ring is deque(maxlen=4096): drops OLDEST on
full and counts the drops — telemetry loss is always preferable to exec backpressure,
but it stays observable.

Measured: 1000 exec-path calls against a 2s-BLOCKING plane = 3.65 ms total (3.65 us/call).
Inline, that was 2000 s of stall. Wedged lane + 50k pushes -> ring pinned at 4096,
45903 counted drops, exec path never backpressured. Healthy plane still gets 5/5.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 08:58:52 +02:00
Codex
832c85b233 dita_v2(bingx_venue): make _publish_telemetry TOTAL — telemetry cannot raise into the order path
The pre-existing guard covered only publish(). The attribute/coercion block above it
(int()/str()/.metadata.get() on intent+order) was UNGUARDED, and kwarg binding happens
before the body runs at all — so a signature skew sailed straight past a defence that
existed precisely to prevent this, and orphaned 6 live positions.

Everything that can raise now lives inside one try. Failures log loud and are swallowed;
the order path is never affected. Proven against: exploding plane, poisoned intent
coercion (the formerly-unguarded region), and absent plane.

ARCHITECTURAL DEBT (raised by HJ, not fixed here): telemetry has no business being called
inline on the exec path at all. Correct shape = fire-and-forget enqueue drained by the M6
journal lane. This commit makes it harmless, not absent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 08:54:01 +02:00
Codex
aec0b4670f dita_v2(test): repair test_double_buffer control test — suite was un-collectable
Line 154 held mutant debris: KernelControlSnapshot(mode="live", mode="live"") — duplicate
kwarg + unterminated string. Committed since a55501b, it made the ENTIRE dita_v2 suite fail
collection, so nobody has run it. That is why no test caught the bingx_venue telemetry
signature skew that orphaned 6 live positions tonight.

The test was fantasy besides: asserted read.arming == "DARK" (no such field) and
mode == "live" (KernelMode is NORMAL|DEBUG). Rewritten to assert a real invariant —
the second write must flip the buffer and become visible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 08:11:54 +02:00
Codex
dc1df325cb dita_v2: port M5 native-artifact provenance UP to canonical upstream
M5 (native_artifact.py + rust_backend build_verified_artifact/verify_artifact hooks)
existed ONLY in the uv/exec-refactor worktree. Canonical never received it, so
vendor_sync.sh downgraded the vendored copy back to a raw 'cargo build' — the same
mechanism by which M1's relock clobbered the bingx_venue telemetry fix and orphaned
6 live positions. Canonical is the source of truth; M5 belongs here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 05:28:05 +02:00
Codex
b46ebd2f82 dita_v2(bingx_venue): restore _publish_telemetry order_id/client_order_id kwargs + make post-ack telemetry non-fatal
M1 re-vendor (89a1f1a) clobbered the T0-DEVIATION fix (20ad493): submit()/submit_async()
pass order_id=/client_order_id= post-ack, the signature dropped them. Every ENTER raised
TypeError AFTER the venue POST returned 200 OK -> rust_backend synthesised REJECTED ->
FSM rollback, while the venue kept the position. Flight-4: 8 bridged promotions, 6 orphan
SHORTs live on VST with a kernel that believes it is flat.

Two fixes:
1. signature accepts order_id/client_order_id again; snapshot prefers them (order is None
   on the submit path, so the ack row is the only id source).
2. post-ack telemetry wrapped: observability can never again veto an accepted order.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 05:08:12 +02:00
Codex
9fe989b502 malkhut(perf): DuckDB in-memory materialization — sub-µs reads, zero DuckDB overhead
Architecture: DuckDB for persistence + full in-memory materialization for reads.
All reads served from Python dicts (sub-microsecond). DuckDB only hit on writes.

Performance evolution (get_asset benchmark):
  V0 (raw DuckDB):    876µs per call
  V1 (LRU cache):     2.3µs per call (380x)
  V2 (in-memory):     0.2µs per call (4380x)

All reads now sub-microsecond:
  get_asset:      0.2µs (was 876µs)
  query(blockian): 6.6µs (was 2.2ms)
  query(sector):   6.6µs (was 3.2ms)
  exchange lookup: 12.5µs (was 1.5ms)
  full scan:       5.9µs (was 1.8ms)
  behavior:        0.4µs

Write path: sync_from_profiles batch-inserts all data, then materializes
into Python dicts. Resync: 76ms (was 210ms, 2.8x faster).

Data integrity: DuckDB WAL provides crash recovery. In-memory dicts are
reconstructed from DB on every sync/close-reopen cycle. Zero data loss.
2026-07-12 19:38:24 +02:00
Codex
df40e62104 dita_v2(zinc): account-truth plane API — publish/read/wait_on/notify_account
Formalizes the CEX-010 E-feed account plane (protocol + InMemoryZincPlane +
real plane). This code flew 33h on UV-PRIME flight-1 via the vendored copy
(T0-DEVIATION mid-edit vendoring) but was never committed upstream — the
2026-07-11 clean vendor_sync regressed it and broke the E-feed at boot
(AttributeError publish_account). Committing the flight-proven bytes closes
the vendor-law gap. Authorship: pre-existing WIP in /mnt working tree
(T0-era, likely codex), committed verbatim by Fable for vendor integrity.
2026-07-11 22:46:33 +02:00
Codex
7bc13a6eb2 dita_v2+adapters: phantom-position root-cause fixes (UV audit §8)
bingx_direct.submit_intent: (1) empty/orderId-less ack (VST offline-symbol
hole: code==0 + empty data) -> REJECTED, never fabricate fill from
target_size; (2) honor upstream u- client id (intent.metadata
promo_client_id/client_order_id) over self-minted p- id.
rust_backend: EXIT asset-mismatch guard both sync+async paths — EXIT for
asset X while slot holds Y -> NO_OPEN_POSITION/EXIT_ASSET_MISMATCH instead
of asset-blind close (THETA 76bbf8ee cross-slot contamination).
9 regression tests, mutation-RED verified (guard flip).
2026-07-11 22:29:02 +02:00
Codex
3d313290b3 uv(pass-a-r1): 10000x test coverage — races, edges, seams, fuzz.
20 new tests: thread-safety, tripwire invariants (no cumulative drift), poison,
compat seam (reconcile_ok==tripwire_ok), mutation litmus (grep-based K-field ban),
Hypothesis fuzz (500 random streams), vendor drift gate.

Fix: apply_balance_update now resets tripwire_ok=True (non-fill events default healthy).
26 total (20+6).
2026-07-10 10:48:04 +02:00
Codex
3da5d8866d uv(pass-a-r1): K-purge — Rev-4-FINAL compliance. A1: reconcile_ok replaced by stateless per-event tripwire (tripwire_ok + tripwire_last_event_id on AccountStateSnapshot). Detection latency = 1 event. A2: ASEXAccountV2.k_capital -> _k_capital_internal. 6 acceptance tests. 2026-07-09 17:17:29 +02:00
Codex
81520af83c dita_v2: fix arithmetic in test_asex_account mixed-ops funding expectation (2x50x0.5=50, not 100)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 08:03:36 +02:00
Codex
a7522bf8d1 dita_v2: reconcile SOA — vendored thread-safety hardening ∪ venue plane
3-way merge (base=pre-venue-plane upstream, ours=violet-main vendored hardening,
theirs=upstream venue plane): RLock atomic-snapshot wrapping (account.py,
real_control_plane, real_zinc_plane), lazy __getattr__ imports (__init__),
account-core test coverage — merged with venue_region telemetry. Zero conflicts;
all files AST-verified. Ends the two-way vendor drift found in
DITAV2_SOA_SURVEY_20260702 §5 / UV_DITAV2_SOA_VERDICT_20260703.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 01:15:17 +02:00
Codex
cfb3d7cf4f dita_v2: backport asex_account.py (from violet main) + test (from PASS9) to canonical upstream
Heals reverse vendor drift: the ASEx AccountProjectionV2 adapter was committed on
the vendored copy (violet.git main 824c5cf) instead of upstream, violating the
edit-upstream-then-sync doctrine. Test was orphaned on the PASS9 worktree.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 01:00:40 +02:00
Codex
9bef1f6b01 dita_v2: VenueTelemetrySnapshot + Zinc venue plane (4th shm partition) + local Rust target dir
Upstream-canonical commit of the ~366 lines that sat uncommitted in the working
tree (surveyed in DITAV2_SOA_SURVEY_20260702). Venue telemetry published at every
venue boundary (submit/cancel/reconcile) over the zinc venue_region; SOA per
UV DITAv2 adjudication 2026-07-03.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 00:42:20 +02:00
Codex
84e4a50e3f repo hygiene: track the PINK launcher import closure
67 production .py modules that the running PINK service imports but which
were never committed: prod/bingx/ (HTTP client, market/user streams,
journal, config), prod/clean_arch/ adapters/persistence/runtime/dita/dita_v2
production modules and their co-located tests. Rule going forward: every
module imported by launch_dolphin_pink.py / pink_direct.py must appear in
git ls-files. Excludes _backup dirs, __pycache__, and non-code files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 15:09:32 +02:00
Codex
f4ff1cd9b7 blue_parity: hyphen-tolerant price_of (first add of module)
PinkAssetPicker/PinkAlphaSizer (BLUE-parity IRP picker + sizer, 2026-06-10)
plus the 2026-06-11 fix: price_of falls back to the dehyphenated symbol —
reconcile/fill paths write venue format (FET-USDT) into the slot while the
scan universe keys are Binance-style (FETUSDT); without the fallback an
adopted slot had no price and TP/SL/MAX_HOLD never evaluated (the unmanaged
FET position incident).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 15:03:19 +02:00
Codex
4929087f7a PINK kernel fix-of-fixes: slot-PnL repair plumbing
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>
2026-06-12 14:58:30 +02:00
Codex
d280407327 PINK Phases 1-4: E-anchored capital, atomic snapshot, sizer feedback, kernel hardening
Phase 1: account.py anchor_to_exchange, capital_source provenance, settle
includes fees in capital delta.
Phase 2: atomic snapshot swap, CH provenance DDL (08_provenance.sql),
naive-UTC timestamps, ch_writer wait_for_async_insert=1 for all tables,
head-of-line stuck-row logging at WARNING per 100 attempts.
Phase 3: sizer feedback uses slot realized_pnl (not capital delta),
FILL_SETTLED repairs slot-level PnL for price-less exit legs.
Phase 4: resolve_slot returns Option<usize>, UNRESOLVED_SLOT diagnostic.
bars_held clamped to max(0, ...) at row-build time.
2026-06-11 21:44:24 +02:00
Codex
2c9da8f592 PINK Phase 0: FET -$5,990 fix batch — leverage-free PnL, true fill prices, reconcile baseline anchors
Defects fix (FET -$5,990 replay, 2026-06-11):
- realized_pnl() and mark_price(): PnL = qty × Δprice, side-signed; no ×leverage inflation (was 3× every leg).
- BingX MARKET fill events carry true fill price (avgPrice/lastFillPrice), never the order's nominal price (protective bound ±20-25% from mark, poisoned PnL to -$5,990 on a +$164 round-trip).
- Fill routing by ORDER IDENTITY first, FSM state second — late entry-remainder fills during EXIT_WORKING no longer misclassify as exits.
- Entry basis = VWAP across entry fills, not last fill price.
- reconcile_from_slots / restore_state: re-anchor _last_settled_pnl / _slot_was_closed to adopted slot state (cross-restart double-book of carried PnL).
- ACCOUNT_UPDATE with wallet_balance=0 dropped (margin-only frames no longer zero e_available_margin).
- Foreign-fill skip on shared VST account (PRODGREEN collision filter).
- exec_router TTL: entry-requote venue-truth gate (recent own fill + live exchange position probes prevent double-entry).
- bingx_direct: openOrders fetched BEFORE positions (sequential ordering prevents dangerous tear → double-entries).
- Dual-leverage translation via map_internal_conviction_to_exchange_leverage() (strategy conviction → integer at-exchange leverage, bankers rounding).
- BLUE-parity alpha components wired: asset picker (IRP universe ranking) + alpha sizer (cubic-convex dynamic leverage, 0.5-8.0 range).
- ch_writer: date_time_input_format=best_effort on insert URLs; flush error logging at WARNING with counter.
- blue_parity.price_of(): hyphen-tolerant fallback (FET-USDT → FETUSDT).
- Fill test updated to incremental filled_size semantics (BingX WS lastFilledQty).
- Env-override base URLs, supervisord autorestart, per-asset DC histories, single-slot invariant, fill-attribution filter.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
2026-06-11 20:53:49 +02:00
Codex
c16b5aaaa4 PINK: fix trade_seq always-zero — unique trade_id per position
trade_seq was missing from the kernel account snapshot dict, so the
intent engine always computed trade_id = "BTCUSDT-T-000000000001".
The Rust FSM SLOT_BUSY guard only fires on *different* trade_ids; with
the same ID it resets the slot and submits a new exchange order on each
ENTER signal tick (~86 duplicate orders observed in one session).

Fix:
- Add _slot_was_closed dict to ExecutionKernel; set False on ENTER
  accepted (both sync/async), True on on_venue_event when slot.closed
- Increment account.snapshot.trade_seq on the IDLE→CLOSED transition
- Expose trade_seq in snapshot()["account"] so DecisionContext carries
  the correct counter → intent engine generates unique IDs per trade

451/451 tests green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 18:23:31 +02:00
Codex
0eac51d2e9 PINK: docs v7 + reset_and_seed startup fix — 451/451 tests green
- SYSTEM_BIBLE.md → v7.0: documents fee-sign fix (Defect A), opening-fee
  fix (Defect B), WARN-unfreeze, orphan prevention, reset_and_seed startup,
  and vel_div env-override.
- CAPITAL_BOOKKEEPING_DESIGN.md: status updated to PHASE-1 BUGFIXES APPLIED;
  sections 8.1-8.4 (applied fixes + 34-test coverage) were already present.
- rust_backend.py: expose dita_kernel_reset_and_seed() via _RustKernelLib +
  ExecutionKernel.reset_and_seed(); zeros stale K-accumulators at startup so
  K=E=live_capital → delta=0 → capital_frozen=False on every clean restart.
- pink_direct.py: call kernel.reset_and_seed(live_capital) after
  _restore_kernel_snapshot() so BingX is always the ledger of record.
- launch_dolphin_pink.py: DOLPHIN_PINK_VEL_DIV_THRESHOLD env-var override
  for on-exchange debugging; BLUE unaffected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 12:33:50 +02:00
Codex
e38ec77221 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>
2026-06-08 11:08:31 +02:00
Codex
7e83a5c5c5 PINK: fix event-loop corruption in open_positions/open_orders + 23-test orphan suite
Root cause: open_positions()/open_orders() called _backend_snapshot() ->
_call_backend() -> _run() -> pool.submit(asyncio.run, coro) which spawned a
temporary event loop in a worker thread. httpx AsyncClient created inside that
temp loop, loop closed immediately. All subsequent HTTP calls raised Event loop
is closed or asyncio.locks.Event bound to different loop. Crash triggered WS
stream reconnects; each reconnect re-ran reconcile with N>1 BingX positions and
orphaned all but the largest.

Fix: open_positions()/open_orders() now read backend._state (populated by
await backend.connect() in the main loop). Fallback to _backend_snapshot()
for callers without a connected backend.

Fixes test_bingx_bugs::TestConnectNoDoubleRefresh: connect() is now async.

New test_orphan_prevention.py: 23 tests covering all 5 orphan mechanisms:
  A. open_positions/open_orders use backend._state, never hit thread pool
  B. connect() awaitable, backend.connect() runs in main event loop
  C. Reconcile guard: >1 position logs ERROR and takes only largest
  D. clientOrderId p-action-base36-rand4 on every order
  E. EXIT sizing capped to kernel slot_size

391 passed, 2 skipped, 0 failed across all 14 test files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 20:53:41 +02:00
Codex
10a44d86b1 PINK: wire CH persistence to monitor + add missing friction DDL
- 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>
2026-06-06 11:10:49 +02:00
Codex
33d8e855c8 PINK: fix EXIT position not closing — 3 root causes, 368/368 tests green
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>
2026-06-06 01:39:35 +02:00
Codex
535eea855d PINK: cancel_async, S2 task guard, 29 new regression tests — 346/346 green
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>
2026-06-05 16:02:13 +02:00
Codex
f2596e1155 PINK: S3 dead-snapshot removal — connect/cancel/submit overhead cuts
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>
2026-06-05 14:07:45 +02:00
Codex
c864e9c550 PINK: S1 leverage cache, S2 background refresh, Gap 1/2/3 fee+slippage logging
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>
2026-06-05 12:25:12 +02:00
Codex
714913bab6 PINK: flat_and_start_pink — persistence check + asset-scoped flatness test
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>
2026-06-05 09:38:59 +02:00
Codex
f7ee491f15 PINK: FLAWS doc — backfill SHA b30205c for pass-6 persistence entries
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-05 09:32:01 +02:00
Codex
b30205ceb6 PINK: fix persistence layer — exit_price, entry_bar, recovery, external exits, NaN tracing
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>
2026-06-05 09:30:30 +02:00
Codex
025d381623 PINK: flat_and_start_pink.py — flatten BingX VST + async startup check
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>
2026-06-05 07:37:56 +02:00
Codex
feaf75e70f PINK: FLAWS doc — backfill real SHA f3a5f21 for pass-5 entries
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 21:03:58 +02:00
Codex
f3a5f21460 PINK: async submit + process_intent hot path; async/race flaw audit (pass 5)
N2/N3/N4 (3x Critical async bugs):
- BingxVenueAdapter.submit_async(): awaits backend.submit_intent() directly
  in caller's event loop — no thread-pool, no asyncio.run(), no _backend_snapshot()
- ExecutionKernel.process_intent_async(): same FSM guard logic as sync version;
  replaces venue.submit() with await venue.submit_async(); sync process_intent()
  untouched so all 122 tests stay green
- pink_direct.step() line 952: process_intent() -> await process_intent_async()

restore_state JSON parse (test fix):
- ExecutionKernel.restore_state() wraps Rust FFI in try/except JSONDecodeError
  returns False; matches documented contract; test_restore_corrupt_json_rejected passes

FLAWS doc: pass 5 table added; 21 total fixed; Z6/N5 marked resolved

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 21:02:26 +02:00
Codex
a9ba407ae2 PINK: fix reconcile 30s deadlock — async def + direct await
Root cause: _run() → pool.submit(asyncio.run, coro).result(30s) created a
new event loop in a thread-pool thread; aiohttp session is main-loop-bound
→ silent deadlock every step cycle. BingX VST is healthy (544ms gather).

Fix: async def reconcile() + await self.backend.refresh_state() in main loop.
pump_venue_events() already handles isawaitable → zero caller changes.
include_history=False (symbol=None skips history anyway).
Tests: 13/13 passing (async contract, 3 fault paths, <2s timing, gather-10).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 18:46:19 +02:00
Codex
d7e272e148 PINK: FSM occupancy & rollback test suite (9 new tests, 97/97 green)
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>
2026-06-04 18:31:50 +02:00
Codex
a5894a7196 PINK: FSM rollback on venue.submit failure via synthetic REJECTED event
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>
2026-06-04 18:17:31 +02:00
Codex
f78cc0d3f9 PINK: fix last c_char_p temporary in set_slot_json
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>
2026-06-04 17:00:16 +02:00
Codex
acffc783e6 PINK: fix naive datetime → INVALID_INTENT_PARSE at column 41
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>
2026-06-04 16:45:33 +02:00
Codex
55197b2047 PINK: fix ctypes dangling-pointer + venue.submit guard
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>
2026-06-04 16:14:42 +02:00
Codex
a89e766da1 PINK: fix ctypes c_char_p null-byte truncation (INVALID_INTENT_PARSE)
_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>
2026-06-03 18:30:10 +02:00
Codex
29d44c338e PINK: TUI Hz fix + DC gate + ACB boost + 10 new tests (104/104 green)
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>
2026-06-03 14:00:48 +02:00
Codex
8d85d75ded PINK DITAv2: Hz writes + vol_ok gate + leverage logging + 8 new tests (94/94 green) 2026-06-03 13:26:36 +02:00
Codex
0f2d3f556d PINK DITAv2 flaw doc: V1+V2+V3+W10 fix markers + pass 3/4 tables
Integrates flaw doc updates from the side chain (post-Pass20) onto branch HEAD:
- V1/V2/V3 rows marked ✅ FIXED 8d9762c
- W10 row marked ✅ FIXED e90d542
- Pass 3 fixes table (V1/V2/V3 detail)
- Pass 4 fixes table (W10 detail)
- Header: "17 total fixed"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 22:12:28 +02:00