Commit Graph

11 Commits

Author SHA1 Message Date
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
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
62553424ab PINK: fix vel_div always-None in HazelcastDataFeed — normalize_ng7_scan hoists vel_div to top-level data dict but feed was reading scan (inner result sub-dict) which has no vel_div. Now checks data.get('vel_div') first (top-level), then scan, then recomputes from multi_window_results. PINK was receiving velocity_divergence=None on every snapshot and never entering trades even when BLUE (same threshold) did.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 15:18:53 +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
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
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
e7eaa88ce1 PINK Phase 0 and 1: VST WS confirmed plus AccountSnapshotV2 account core 2026-06-01 20:11:03 +02:00
Codex
0c15a7698e PINK DITAv2 L3: fix live LIMIT cancel (kernel order-id propagation + truth-based cancel)
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>
2026-05-31 08:03:27 +02:00
Codex
55ed6902d8 PINK DITAv2 L0-L2: two-phase persistence + async-fill pump + LIMIT wiring
Execution-infra only (policy stays MARKET; algorithmic integrity untouched).

L0 — two-phase (request->result) persistence (pink_clickhouse.py):
- Split persist_step into persist_request (policy_events + trade_reconstruction
  ORDER_REQUESTED) and persist_result (state snapshot + per-fill lifecycle rows).
- Lifecycle rows (ENTRY_FILLED/EXIT/trade_events/trade_exit_legs) gated on
  evidence of an actual fill (FULL/PARTIAL_FILL event, closed slot, or size drop
  vs _leg_state) -> a resting LIMIT (ACK only) emits no terminal rows.
- Add persist_fill_events: synthesizes a minimal decision/intent from slot+event
  for async fills and routes through persist_result.

L1 — async-fill pump (pink_direct.py):
- PinkDirectRuntime.pump_venue_events(): venue.reconcile() -> kernel.on_venue_event
  (capital settles, FSM advances), persists applied fills; kernel dedups
  duplicates (no double-settle). Called at the start of step().

L2 — LIMIT placement (bingx_direct.py):
- submit_intent now honors _order_type/_limit_price from intent metadata
  (was hardcoded MARKET): LIMIT -> type=LIMIT + price + GTC; MARKET default;
  invalid limit price falls back to MARKET.

Offline: 63 passed (persistence/groundwork/pump/limit-payload/runtime/accounting/
flaws/kernel). MARKET path unchanged; resting LIMIT now correct end-to-end offline.
Live VST validation (L3) pending. BLUE untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 03:23:44 +02:00