VIBRISS_PARAMETER_GOVERNANCE_SPEC §10.6: ob_cascade.count_threshold (currently cascade_count>0 = ONE asset widens every TP x1.40), tp_widen_factor, withdrawal_velocity_threshold as governance candidates; adaptive/Dynamic-TP threshold marked fit for VIBRISS governance; TP_FLOOR joint-policy reward requirement. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
20 KiB
PINK / DITAv2 Accounting & Execution Fix — Spec and Dev Guide
Status: SPEC — ready for implementation agent
Date: 2026-06-11
Branch: exp/pink-ditav2-sprint0-20260530 (continue on it or fork fix/pink-accounting-consolidation)
Author of spec: forensic session 2026-06-11 (FET −$5,990.90 mis-book replay)
Prerequisite for: VIOLET rebuild (violet_subsecond_rebuild_plan memory / future plan session)
0. Why this exists — the incident in one paragraph
On 2026-06-11 PINK closed a FET-USDT short that the exchange settled at
≈ +$164 net (entry VWAP 0.1878, exit 0.1866, ~202K FET) but the kernel
booked −$5,990.90 and capital diverged −$6,154 from the exchange wallet.
Replay against dolphin_pink.trade_reconstruction slot images identified
three stacked defects, all in derivation code (none in exchange facts):
(1) fill events carried BingX's MARKET protective bound price (0.229,
+22% off tape) instead of the true fill price; (2) realized_pnl() and
mark_price() multiplied PnL by slot.leverage (exchange leverage — but
slot.size is exchange quantity, so every leg was 3× inflated); (3) the
Python settle baseline _last_settled_pnl resets empty on every restart,
so reconcile-adopted slots re-settle carried PnL. Exact replay of leg 1:
26,007 × (0.229−0.1878)/0.1878 × 0.1878 × 3 = −3,214.4652 ✓ matches the
booked increment to the cent.
A fourth structural finding: there are three parallel ledgers (Rust
AccountState K/E, Python AccountProjection — the one persistence reads,
fee-blind — and AccountProjectionV2, dead in the live path). This spec
consolidates to E-facts as ledger of record + K as integrity checksum +
one atomic published snapshot.
1. Scope and non-goals
IN SCOPE
- Commit + activate the Phase-0 fixes already in the working tree.
- E-anchored published capital; single atomic account snapshot.
- Per-trade PnL provenance (
exchange | kernel_estimate) end-to-end. - Sizer feedback off trade-realized PnL (not capital deltas).
- Persistence hygiene: duplicate row emission, silent async-insert loss,
event_seqstamping,bars_heldclamp, naive-UTC timestamps. - Kernel hardening leftovers:
resolve_slotno-match sentinel, FILL_SETTLED realized override of flagged estimate legs.
OUT OF SCOPE (separate tickets)
- BLUE's exit-path masking bug (LINK −$1,248,
TODO_TP_SCAN_CADENCE_BUGFIX.md) — BLUE stack, not DITAv2. - VIOLET fork, sub-second clock, venue price-feed port, cadence quantizer.
- ch_writer head-of-line poison-row parking redesign (mitigations land here; the full parking-lane design is its own task).
- prefect.db / ClickHouse TTL disk remediation.
HARD INVARIANTS — MUST NOT CHANGE
- Dual leverage:
slot.size= exchange quantity;slot.leverage= exchange leverage (1–3x cap, set at BingX API); our-leverage (conviction) =size × entry_price / capital, computed only atpink_direct._hz_publish(line ~911). PnL is therefore leverage-free:qty × Δprice, side-signed. Do not touch the conviction→exchange mapping (round_half_even_linear_0.5_to_9.0_to_1_to_exchange_cap) ortarget_sizecomputation. - Exits are never skipped (exec-router invariant set, §16 kernel ref).
- BLUE-parity policy contract:
DecisionEngine/IntentEngineinputs (MarketSnapshot + capital + slot state) unchanged in shape. - Namespace isolation: zero writes to
dolphin.*/dolphin_prodgreen.*or BLUE/PRODGREEN HZ maps. Re-verify withpink_ctl.py mode-verify. - Data cadences are sacred (operator rule 2026-06-10): never reduce a data cadence for throughput.
2. Phase 0 — Commit and activate the already-applied fixes
These changes exist UNCOMMITTED in the working tree as of 2026-06-11 ~16:30.
Verify each hunk, commit as one reviewed unit, then restart dolphin_pink.
0.1 prod/clean_arch/dita_v2/_rust_kernel/src/lib.rs
| Function | Change (already applied) |
|---|---|
KernelCore::realized_pnl (~line 1153) |
PnL = side-signed qty × (exit − entry); no leverage factor; returns 0 when entry<=0 ∨ exit_size<=0 ∨ exit_price<=0 ∨ !finite |
TradeSlot::mark_price (~line 394) |
no × leverage in unrealized; a mark NEVER becomes entry basis — missing basis flags metadata.entry_basis_missing=true, unrealized stays 0 |
KernelCore::fill_matches_order (new) |
identity match on venue_order_id / venue_client_id |
KernelCore::apply_fill |
entry/exit routing by ORDER IDENTITY first, FSM state second (!id_matches_exit / !id_matches_entry guards); entry basis = VWAP across entry fills ((prev_basis×prev_filled + price×fill)/accumulated); price-less exit fill reduces size, books 0 PnL, flags metadata.realized_skipped_no_price=true |
Rebuild required: cargo build --release in _rust_kernel/ (the .so is
only auto-built when missing — source/binary drift is a known hazard;
add the build to the commit checklist). cargo test: 32/32 green as of spec.
0.2 prod/clean_arch/dita_v2/bingx_venue.py
Fill events must carry a TRUE fill price or 0.0 — never the order's nominal
price / submit receipt.price (BingX MARKET bound price, ±20–25%):
_events_from_submitfill event (~line 585):_row_float(ack_row, "avgPrice","ap","lastFillPrice","L", default=0.0)_event_from_row(~line 697): fills use the same true-price chain; non-fill events (ACK/CANCEL/REJECT) may keep nominalpriceas info_fill_event_from_row(~line 736):"lastFillPrice","L","avgPrice","ap"
0.3 prod/clean_arch/dita_v2/rust_backend.py
reconcile_from_slots: seeds_last_settled_pnl[slot_id] = slot.realized_pnland_slot_was_closed[slot_id] = slot.closedfor every adopted slot.restore_state: same re-anchoring after successful restore.
0.4 Adjacent fixes riding the same commit
prod/ch_writer.py: insert URLs append&date_time_input_format=best_effort; flush errors log at WARNING (first 10 + every 100th), counter_flush_errors.prod/clean_arch/dita_v2/blue_parity.pyprice_of: hyphen-tolerant fallback (FET-USDT→FETUSDT) — fixes the unmanaged-position block.prod/clickhouse/users.xml:date_time_input_format=best_effortfor thedolphinuser (NOTE: running CH container did not honor it even after restart — the container does not mount compose configs; effective on next compose recreation. The client-side URL param is the operative fix.)prod/tests/test_dita_v2_kernel.py: partial→full fill test updated to incrementalfilled_sizesemantics (BingX WSlastFilledQty).
0.5 Phase 0 gates
cargo testin_rust_kernel: 32/32.pytest prod/tests/test_dita_v2_kernel.py: 7/7.pytest prod/clean_arch/dita_v2/test_exec_router_runtime.py test_venue_reconcile.py test_orphan_prevention.py prod/tests/test_pink_async_fill_pump.py prod/clean_arch/dita_v2/test_account_core_v2.py test_bingx_bugs.py: 134/134.- KNOWN pre-existing failures (NOT introduced by this work — verified by
hunk-revert): 4 tests in
prod/tests/test_dita_v2_bingx_adapter.py(snapshot-fill emission broke when syncsubmit()started passing None snapshots on 2026-06-10). Fix or quarantine them explicitly in this phase — do not let them mask new regressions. - Restart
dolphin_pinkat a FLAT moment; verify in logs: norealized_skipped_no_pricestorms, noentry_basis_missingon fresh entries, first round-trip books PnL within ±(fees+slippage) ofGET /openApi/swap/v2/user/incomefor the same trade.
3. Phase 1 — E-anchored published capital
Goal: the capital that persistence/HZ/sizer see is exchange-anchored; K never publishes.
3.1 prod/clean_arch/dita_v2/account.py
- Add to
AccountSnapshot:capital_source: str("e_anchored" | "k_bridged" | "seed"),e_wallet_balance: float,event_seq: int. - New method
AccountProjection.anchor_to_exchange(wallet_balance: float, available_margin: float, event_seq: int): setscapital = wallet_balance(guard>0and finite — the zero-wb frame lesson),capital_source = "e_anchored", recomputes equity.settle()remains for the BRIDGE case only: between anchors, capital += realized (capital_source="k_bridged"). settle(realized_pnl, fees): stop ignoring fees —capital += realized_pnl − fees(today fees only accumulate infees_paid; published capital ignores them between reseeds).
3.2 prod/clean_arch/runtime/pink_direct.py
- The existing reseed path (balance-bearing ACCOUNT_UPDATE →
kernel.reset_and_seed(wb)) additionally callskernel.account.anchor_to_exchange(...)— one anchoring action, two ledgers consistent. - Boot seed (launcher
exchange_balance_capitalblock, pink_direct ~line 262) goes throughanchor_to_exchangeinstead of direct attribute writes.
3.3 Gates
- New unit tests (
prod/tests/test_pink_account_anchor.py): anchor sets capital/source; zero/negative/NaN wb rejected; settle bridges with fees; anchor after bridge snaps to wb exactly. - Shadow check (live, 24 h on VST): published capital vs
GET /openApi/swap/v2/user/balancepolled 1/min — max |Δ| outside a trade-settlement window ≤ $0.01; during settlement ≤ pending-fee bound.
4. Phase 2 — Single atomic snapshot, ledger consolidation
Goal: one immutable, versioned account snapshot; the two redundant ledgers demoted/removed.
4.1 prod/clean_arch/dita_v2/account.py
- Make the published snapshot immutable-replace:
AccountProjectionbuilds a new frozenAccountSnapshot(carryevent_seq) on every mutation and swaps a single reference (GIL-atomic). Readers must takesnap = kernel.account.snapshotonce per use (audit call sites:pink_clickhouse.py,hazelcast_projection.pyHZ writer,pink_direct). AccountProjectionV2: DELETE, or move toprod/clean_arch/dita_v2/ _attic/with a module docstring pointing here. Its only live-path import isexchange_event.py— migrate that import or the dataclasses it uses (EPositionis genuinely useful; keep it inaccount.py).- The Rust
AccountStateK-ledger STAYS — demoted by documentation and by Phase 1 (it no longer feeds published capital): its jobs are reconcile classification (R1-style),capital_frozen, and E-dark bridging. Update the module docstring to say exactly this.
4.2 prod/clean_arch/persistence/pink_clickhouse.py
- Read capital/equity/peak/trade_seq from the single snapshot reference; no recomputation.
- Add columns to emitted rows (and the matching
ALTER TABLEDDLs underprod/clickhouse/pink/08_provenance.sql— apply DDLs to CH BEFORE deploying code that emits them; the missing-table head-of-line jam of 2026-06-11 is the cautionary tale):account_events,status_snapshots:capital_source LowCardinality(String) DEFAULT '',account_event_seq UInt64 DEFAULT 0trade_events,trade_exit_legs:pnl_source LowCardinality(String) DEFAULT ''(exchange|kernel_estimate)
bars_held: clamp tomax(0, …)at row-build time (UInt16 column; negative values currently 400 on trade_events / silently vanish on async tables).- Timestamps: route every
tsthrough one helper emitting naive-UTC microsecond ISO (no+00:00) — best_effort already tolerates both, but rows must stop depending on a parser setting.
4.3 Duplicate-emission fix (same file)
Every CH row is currently emitted twice (visible in any query). Hunt the
double call: instrument _sink() with a per-(table, content-hash) debug
counter in a test, then trace the two call paths (suspect: persist_result
invoked both from the runtime step and from the fill pump for the same
event). Fix at the caller level; do NOT dedupe by content in the sink
(masks real double-events). Regression test: one simulated round trip →
exactly one row per logical event per table.
4.4 prod/ch_writer.py
wait_for_async_insert:"1"for ALLdolphin_pinktables (accounting rows must never be silently lost; the spool absorbs latency). Keep0acceptable only for high-volume shadow tables if measured necessary — document any exception inline.- Mitigation for head-of-line (full redesign out of scope): after
attempts > 1000on a row, log ERROR with the CH response body once per 100 attempts (today the reject reason is invisible without manual replay).
4.5 Gates
- Full offline suite (the 533+ DITAv2/PINK set) green, minus the Phase-0 quarantined adapter tests if still open.
- One live VST round trip: every table gets exactly one row per event;
pnl_source/capital_sourcepopulated; CHsystem.text_logshows zero parse rejections fordolphin_pink.
5. Phase 3 — Sizer feedback off trade-realized PnL
THE one seam where this refactor can silently change alpha behavior.
5.1 prod/clean_arch/runtime/pink_direct.py — _sizer_trade_feedback (~line 1453)
Today: pnl = acc.capital − self._sizer_entry_capital (capital delta).
Under E-anchored capital this absorbs funding, fees of other activity, and
foreign fills from the shared VST account (PRODGREEN collision class).
Change to:
pnl = slot_realized_for_trade(trade_id) # Σ slot.realized_pnl legs, i.e.
# kernel estimate, overridden by
# exchange rp when settled (5.2)
Source: the closing slot dict already carries realized_pnl; use it (minus
the fees recorded for the trade when available) instead of the capital
delta. Keep the magnitude semantics the sizer expects (sign + rough size —
per the existing comment, bucket/streak multipliers only need that).
5.2 Exchange override (E-led repair) — bingx_user_stream.py + rust_backend.py
- The WS
FILL_SETTLEDpath already carries the exchange's realized (rp) and fee (n, sign-flipped at boundary per BingX quirks memory). Extend the kernel account-event payload withtrade_id, and on receipt:- if the matching slot leg was flagged
realized_skipped_no_price, ADD the exchange realized toslot.realized_pnl(repair) and clear the flag; settle the increment through the normal baseline mechanism; - else record
pnl_source="exchange"for the trade-event row (the estimate stays as the booked figure unless |estimate−rp| exceeds a tolerance — then log ERROR + emit ananomaly_eventsrow; do NOT silently re-book).
- if the matching slot leg was flagged
- Rust: add
dita_kernel_repair_realized(slot_id, amount)FFI (or fold the repair intoon_account_eventwithslot_idin payload). Keep it idempotent via the existing account-event dedup.
5.3 Gates
- Unit: feedback receives trade-realized, not capital delta (simulate a foreign-fill capital jump mid-trade → feedback unaffected).
- Unit: price-less exit leg + later FILL_SETTLED repair → slot realized
equals exchange
rp; settle baseline consistent (no double-settle). - Parity:
test_blue_parity.py,test_alpha_blue_untouched_g7.pygreen (sizer behavior unchanged for normal fills).
6. Phase 4 — Kernel hardening leftovers
6.1 lib.rs — resolve_slot (~line 1099)
Falls back to slot 0 when nothing matches. Change: return
Option<usize>; on None, on_venue_event returns
UNRESOLVED_SLOT (diagnostic exists already) without mutating any slot,
severity WARNING, event recorded in outcome details. Python callers: the
runtime treats UNRESOLVED_SLOT as a logged no-op (the _fill_is_ours
filter remains first-line defense; this is kernel-side defense for
venue-agnostic reuse).
NOTE: several tests construct events with slot_id=-1 expecting slot-0
fallback — update them to pass explicit slot_id=0 (behavioral test
change; list each in the PR description).
6.2 ID-less fill routing (documentation + metric, not code)
BingX WS omits clientOrderId, so identity routing can't always engage.
Add a counter metric (fills_routed_by_state_total) via an
anomaly_events row per occurrence, severity INFO — gives VIOLET the data
to justify per-venue synthetic ids later. No FSM behavior change.
6.3 Gates
- New Rust tests: unresolved event mutates nothing; entry-id fill during EXIT_WORKING routes to entry (already covered by Phase-0 routing — add the explicit case); price-less exit leg books 0 + flag.
7. Test matrix (run-order for the implementing agent)
| Stage | Command (env: PYTHONPATH=/mnt/dolphinng5_predict:/mnt/dolphinng5_predict/nautilus_dolphin, venv /home/dolphin/siloqy_env/bin/python3) |
Pass bar |
|---|---|---|
| Rust unit | cargo test --release in _rust_kernel/ |
100% |
| Kernel FSM | pytest prod/tests/test_dita_v2_kernel.py |
100% |
| Bridge/accounting | pytest prod/tests/test_pink_ditav2_kernel_bridge.py test_pink_ditav2_accounting_invariants.py prod/clean_arch/dita_v2/test_account_core_v2.py |
100% |
| Runtime/reconcile | pytest prod/clean_arch/dita_v2/test_venue_reconcile.py test_orphan_prevention.py test_exec_router_runtime.py prod/tests/test_pink_async_fill_pump.py test_pink_direct_runtime.py |
100% |
| Chaos | pytest prod/tests/test_pink_ditav2_chaos_harness.py + test_dita_v2_e2e_functional.py |
100% |
| Parity | pytest prod/clean_arch/dita_v2/test_blue_parity.py test_alpha_blue_untouched_g7.py |
100% |
| Adapter | pytest prod/tests/test_dita_v2_bingx_adapter.py |
100% after Phase-0 item 4 resolution |
| LIVE VST E2E | python prod/ops/dita_v2_live_bingx_smoke.py --pink --symbol TRXUSDT |
suite green |
| Golden replays (NEW — write these) | prod/tests/test_pink_accounting_golden.py |
see below |
| Shadow soak | 24–48 h on VST | capital vs balance ≤ $0.01 idle |
Golden replay tests (the heart of the acceptance)
Feed the kernel the recorded FET event sequence (entry fills 195,259 + 7,017 @ 0.1878; exit fills 26,007 + remainder; the poisoned variant with price=0.229 and the clean variant with 0.1866):
- Clean prices → realized =
(0.1878−0.1866) × 202,276 ≈ +242.7gross. - Poisoned price (0.229) reaching the kernel anyway → with the adapter fix
it must arrive as 0.0 → leg books 0 +
realized_skipped_no_price; after synthetic FILL_SETTLED rp=+164 → slot realized = +164,pnl_source=exchange. - Restart mid-position (save_state/restore_state + reconcile_from_slots) → next venue event settles ONLY the incremental PnL.
- VWAP: two entry fills at different prices → basis = weighted average.
- Dual-leverage invariant: same fills at exchange-leverage 1 vs 3 → identical realized PnL; only margin fields differ.
8. Rollout & rollback
- Each phase = one PR-sized commit, gates green before the next.
- Activation requires
supervisorctl restart dolphin_pink— restart at a FLAT moment (checkDOLPHIN_STATE_PINK+ exchange positions). The restart-reconcile path is itself under test here; first restart after Phase 0 should be watched live. - Rollback =
git revertof the phase commit + rebuild.so+ restart. The Rust.soMUST be rebuilt on both apply and revert — stale-binary drift is how the incremental-fill change sat uncompiled until 2026-06-11. - CH DDLs are additive (
ADD COLUMN ... DEFAULT) — no destructive migrations anywhere in this spec; rollback leaves unused columns, which is fine. - PINK is VST (virtual funds) — it is the canary by construction. Nothing
in this spec touches BLUE files (verify with
git diff --name-onlyagainst the §38.7 checklist).
9. Done criteria (the whole spec)
- All phases merged; full matrix green; golden replays green.
- 48 h VST soak: zero UNEXPLAINED reconcile errors; published capital
tracks exchange balance; every closed trade's
trade_events.pnlwithin fees+slippage of the exchange income record, withpnl_sourcepopulated. pink_ctl.py mode-verifypasses (namespace isolation intact).- SYSTEM BIBLE §38 addendum updated (one paragraph: E-led ledger, K as
checksum, provenance fields) +
DITA_V2_KERNEL_REFERENCE.md§"Capital simplification" rewritten to match reality.