Snapshot PINK DITAv2 system + Sprint 0 flaw-fix verification

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>
This commit is contained in:
Codex
2026-05-30 18:26:43 +02:00
parent 34d01fe6a4
commit 3d7b00e28d
89 changed files with 32782 additions and 0 deletions

View File

@@ -0,0 +1,95 @@
"""DITA v2 prototype kernel.
This package is intentionally separate from the legacy v1 DITA surface so the
new execution kernel can be validated in isolation before any migration.
"""
from .account import AccountProjection, AccountSnapshot
from .control import (
BackendMode,
ControlPlane,
ControlUpdate,
build_control_plane,
InMemoryControlPlane,
KernelControlSnapshot,
KernelMode,
KernelVerbosity,
MirroredControlPlane,
ZincControlPlane,
)
from .contracts import (
KernelCommandType,
KernelDiagnosticCode,
KernelEventKind,
KernelIntent,
KernelOutcome,
KernelSeverity,
KernelTransition,
TradeSide,
TradeSlot,
TradeStage,
VenueEvent,
VenueEventStatus,
VenueOrder,
VenueOrderStatus,
)
from .journal import ClickHouseKernelJournal, KernelJournal, MemoryKernelJournal
from .rust_backend import ExecutionKernel
from .bingx_venue import BingxVenueAdapter
from .launcher import DITAv2LauncherBundle, LauncherVenueMode, LauncherZincMode, build_launcher_bundle
from .projection import HazelcastProjection, build_position_state_row, build_projection
from .venue import VenueAdapter
from .mock_venue import MockVenueAdapter, MockVenueScenario
from .zinc_plane import InMemoryZincPlane, ZincPlane
from .real_zinc_plane import RealZincPlane, RealZincUnavailable
from .real_control_plane import RealZincControlPlane, RealZincUnavailable as RealZincControlUnavailable
__all__ = [
"AccountProjection",
"AccountSnapshot",
"BackendMode",
"BingxVenueAdapter",
"ClickHouseKernelJournal",
"ControlPlane",
"ControlUpdate",
"DITAv2LauncherBundle",
"build_control_plane",
"build_launcher_bundle",
"ExecutionKernel",
"HazelcastProjection",
"build_projection",
"InMemoryControlPlane",
"InMemoryZincPlane",
"KernelCommandType",
"KernelDiagnosticCode",
"KernelControlSnapshot",
"KernelEventKind",
"KernelIntent",
"KernelJournal",
"KernelMode",
"KernelOutcome",
"KernelSeverity",
"KernelTransition",
"KernelVerbosity",
"MemoryKernelJournal",
"MirroredControlPlane",
"MockVenueAdapter",
"MockVenueScenario",
"LauncherVenueMode",
"LauncherZincMode",
"RealZincPlane",
"RealZincControlPlane",
"RealZincControlUnavailable",
"RealZincUnavailable",
"TradeSide",
"TradeSlot",
"TradeStage",
"VenueAdapter",
"VenueEvent",
"VenueEventStatus",
"VenueOrder",
"VenueOrderStatus",
"ZincPlane",
"ZincControlPlane",
"build_position_state_row",
]

View File

@@ -0,0 +1,337 @@
import sys, re
sys.path.insert(0, '/mnt/dolphinng5_predict')
fpath = '/mnt/dolphinng5_predict/prod/tests/test_pink_bingx_dita_live_e2e.py'
with open(fpath) as f:
content = f.read()
# ===== Collect all existing body names =====
existing_bodies = re.findall(r'async def _body_(\w+)', content)
seen = set()
unique_bodies = []
for b in existing_bodies:
if b not in seen:
seen.add(b)
unique_bodies.append(b)
print(f"Existing: {len(unique_bodies)} bodies")
# ===== New bodies =====
new_bodies = []
new_params = []
def B(name, lines):
new_bodies.append(f"async def _body_{name}(k, symbol, p):\n")
for l in lines:
new_bodies.append(f" {l}\n")
new_params.append(f' pytest.param("{name}", _body_{name}, id="{name}"),')
# ===== 1. Real reconcile: fresh kernel from old slot state =====
B("fresh_kernel_reconcile_entry", [
'tid = f"fk-{int(__import__(\"time\").time()*1000)}"',
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.8)",
"# Snapshot slot state, build fresh kernel, reconcile",
"slot_data = k.slot(0).to_dict()",
"cb = k.account.snapshot.capital",
"fresh = _build_fresh_kernel_from_slot(slot_data, ic=cb)",
"k2 = fresh.runtime.kernel",
"# The fresh kernel should see the same slot state",
"s = k2.slot(0)",
'assert not s.is_free(), f"fresh kernel slot should not be free: {s.fsm_state}"',
"assert s.trade_id == tid, f\"trade_id mismatch: {s.trade_id} vs {tid}\"",
"# Exit on the fresh kernel",
"_si(k2, E.EXIT, tid, symbol, 'SHORT', p*0.995, 0.001); await asyncio.sleep(0.5)",
"assert k2.slot(0).is_free(), \"fresh kernel slot not free after exit\"",
"# Original kernel capital should match",
'assert abs(k2.account.snapshot.capital - cb) < 0.01, f"capital drift: {k2.account.snapshot.capital} vs {cb}"',
])
B("fresh_kernel_reconcile_after_cancel", [
'tid = f"fkc-{int(__import__(\"time\").time()*1000)}"',
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.3)",
'r = _si(k, E.CANCEL, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(0.3)',
"# Reconcile onto fresh kernel from cancelled state",
"slot_data = k.slot(0).to_dict()",
"cb = k.account.snapshot.capital",
"fresh = _build_fresh_kernel_from_slot(slot_data, ic=cb)",
"k2 = fresh.runtime.kernel",
"# Cancelled slot should be free",
'assert k2.slot(0).is_free(), f"cancelled slot not free: {k2.slot(0).fsm_state}"',
])
B("fresh_kernel_reconcile_after_exit", [
'tid = f"fkx-{int(__import__(\"time\").time()*1000)}"',
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.8)",
"_si(k, E.EXIT, tid, symbol, 'SHORT', p*0.995, 0.001); await asyncio.sleep(0.5)",
"# Reconcile onto fresh kernel from closed state",
"slot_data = k.slot(0).to_dict()",
"cb = k.account.snapshot.capital",
"fresh = _build_fresh_kernel_from_slot(slot_data, ic=cb)",
"k2 = fresh.runtime.kernel",
'assert k2.slot(0).is_free(), f"closed slot not free: {k2.slot(0).fsm_state}"',
'assert k2.slot(0).closed, "slot should be marked closed"',
])
B("fresh_kernel_reconcile_partial_exit", [
'tid = f"fkp-{int(__import__(\"time\").time()*1000)}"',
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.002, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(0.8)",
"_si(k, E.EXIT, tid, symbol, 'SHORT', p*0.995, 0.001, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(0.5)",
"# Reconcile mid-trade (one leg exited, one remaining)",
"slot_data = k.slot(0).to_dict()",
"cb = k.account.snapshot.capital",
"fresh = _build_fresh_kernel_from_slot(slot_data, ic=cb)",
"k2 = fresh.runtime.kernel",
"# Remaining leg should still be open",
's = k2.slot(0)',
'assert not s.is_free(), f"partial-exit slot should not be free: {s.fsm_state}"',
'assert s.realized_pnl != 0 or s.size > 0, "partial-exit slot should have remaining position or realized PnL"',
"# Exit remaining leg on fresh kernel",
"_si(k2, E.EXIT, tid, symbol, 'SHORT', p*0.993, 0.001, exit_leg_ratios=(1.0,)); await asyncio.sleep(0.5)",
'assert k2.slot(0).is_free(), "slot not free after final exit on fresh kernel"',
])
# ===== 2. Cross-slot portfolio accounting =====
B("cross_slot_portfolio_short_long", [
't0 = f"psl0-{int(__import__(\"time\").time()*1000)}"',
't1 = f"psl1-{int(__import__(\"time\").time()*1000)}"',
"cb = k.account.snapshot.capital",
"_si(k, E.ENTER, t0, symbol, 'SHORT', p, 0.001, slot_id=0); await asyncio.sleep(0.4)",
"_si(k, E.ENTER, t1, symbol, 'LONG', p, 0.001, slot_id=1); await asyncio.sleep(0.4)",
"# Verify both slots are open",
'assert not k.slot(0).is_free(), "slot 0 should be open"',
'assert not k.slot(1).is_free(), "slot 1 should be open"',
"# Verify PnL tracking per slot",
"rp0 = k.slot(0).realized_pnl; up0 = k.slot(0).unrealized_pnl",
"rp1 = k.slot(1).realized_pnl; up1 = k.slot(1).unrealized_pnl",
"expected = cb + rp0 + up0 + rp1 + up1",
"actual = k.account.snapshot.capital",
'assert abs(actual - expected) < 0.01, f"portfolio misalignment: cap={actual} expected={expected} rp0={rp0} up0={up0} rp1={rp1} up1={up1}"',
"# Exit slot 0",
"_si(k, E.EXIT, t0, symbol, 'SHORT', p*0.995, 0.001, slot_id=0); await asyncio.sleep(0.4)",
"assert k.slot(0).is_free(), \"slot 0 should be free after exit\"",
"# Exit slot 1",
"_si(k, E.EXIT, t1, symbol, 'LONG', p*1.005, 0.001, slot_id=1); await asyncio.sleep(0.4)",
"assert k.slot(1).is_free(), \"slot 1 should be free after exit\"",
])
# ===== 3. KernelOutcome inspection =====
B("outcome_inspect_entry", [
'tid = f"oi-{int(__import__(\"time\").time()*1000)}"',
"r = _si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.8)",
"# Inspect outcome of ENTER",
"_assert_accepted(r, 'entry')",
"info = _inspect_outcome(r, 'entry')",
'assert r.accepted, f"entry not accepted: {info}"',
'assert r.trade_id == tid, f"trade_id mismatch: {r.trade_id} vs {tid}"',
'assert r.slot_id == 0, f"slot_id: {r.slot_id}"',
"# transitions should exist",
'assert len(info["transitions"]) > 0, f"no transitions in outcome: {info}"',
'assert info["diagnostic"] == "OK", f"diagnostic not OK: {info}"',
"# Exit and inspect",
'r2 = _si(k, E.EXIT, tid, symbol, "SHORT", p*0.995, 0.001); await asyncio.sleep(0.5)',
"_assert_accepted(r2, 'exit')",
'info2 = _inspect_outcome(r2, "exit")',
'assert len(info2["transitions"]) > 0, f"no exit transitions: {info2}"',
'assert info2["diagnostic"] == "OK", f"exit diagnostic: {info2}"',
])
B("outcome_inspect_rejection", [
'tid = f"or-{int(__import__(\"time\").time()*1000)}"',
'tid2 = f"or2-{int(__import__(\"time\").time()*1000)}"',
"r1 = _si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.3)",
"_assert_accepted(r1, 'first entry')",
"# Second entry on same slot should be SLOT_BUSY",
"r2 = _si(k, E.ENTER, tid2, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.3)",
"_assert_rejected(r2, 'SLOT_BUSY', 'double entry')",
"# Verify transition trace shows the rejection",
"info = _inspect_outcome(r2, 'double entry')",
'assert not r2.accepted, f"second entry should be rejected: {info}"',
"# Exit normally",
"_si(k, E.EXIT, tid, symbol, 'SHORT', p*0.995, 0.001); await asyncio.sleep(0.5)",
])
B("outcome_inspect_exit_on_idle", [
'tid = f"oei-{int(__import__(\"time\").time()*1000)}"',
"# Exit on idle slot",
"r = _si(k, E.EXIT, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.3)",
"_assert_rejected(r, 'INVALID_FSM_TRANSITION', 'exit on idle')",
'info = _inspect_outcome(r, "exit on idle")',
'assert not r.accepted, f"exit on idle should be rejected: {info}"',
"# Then do a normal trade",
'_si(k, E.ENTER, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(0.8)',
'_si(k, E.EXIT, tid, symbol, "SHORT", p*0.995, 0.001); await asyncio.sleep(0.5)',
])
# ===== 4. Duplicate event dedup =====
B("dedup_duplicate_fill_event", [
'tid = f"dd-{int(__import__(\"time\").time()*1000)}"',
"r = _si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.8)",
"_assert_accepted(r, 'entry')",
"# Inject a duplicate FULL_FILL VenueEvent manually",
"# Build an event that mirrors the slot's current active order",
"sl = k.slot(0)",
'ao = sl.active_entry_order if sl.active_entry_order else sl.active_exit_order',
"if ao:",
" dup = VenueEvent(",
" timestamp=__import__('datetime').datetime.now(__import__('datetime').timezone.utc),",
' event_id="dedup-test-99999",',
' trade_id=tid, slot_id=0,',
' kind=KernelEventKind.FULL_FILL,',
' status=VenueEventStatus.FILLED,',
" venue_order_id=ao.venue_order_id,",
" venue_client_id=ao.venue_client_id,",
" side=sl.side,",
" asset=symbol,",
" price=p,",
" size=0.001, filled_size=0.001, remaining_size=0.0,",
' reason="dedup_test",',
" )",
" r2 = k.on_venue_event(dup)",
" _assert_accepted(r2, 'dedup_fill')",
' info = _inspect_outcome(r2, "dedup_fill")',
' assert len(info["event_kinds"]) == 0 or info["event_kinds"] == ["ORDER_ACK"], f"duplicate fill should produce no events: {info}"',
"# Exit",
"_si(k, E.EXIT, tid, symbol, 'SHORT', p*0.995, 0.001); await asyncio.sleep(0.5)",
])
# ===== 5. Fill-price divergence =====
B("fill_price_divergence_1pct", [
'tid = f"fd-{int(__import__(\"time\").time()*1000)}"',
"# Enter SHORT at market",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.8)",
"# Force the kernel's slot to see a divergent fill price via on_venue_event replay",
"sl = k.slot(0)",
'ao = sl.active_entry_order',
"if ao and sl.fsm_state not in ('IDLE', 'CLOSED'):",
" divergent_price = p * 1.01 # 1% worse than reference",
" div_event = VenueEvent(",
" timestamp=__import__('datetime').datetime.now(__import__('datetime').timezone.utc),",
' event_id="divergence-test",',
' trade_id=tid, slot_id=0,',
' kind=KernelEventKind.FULL_FILL,',
' status=VenueEventStatus.FILLED,',
" venue_order_id=ao.venue_order_id if ao else \"\"," ,
" venue_client_id=ao.venue_client_id if ao else \"\"," ,
" side=sl.side,",
" asset=symbol,",
" price=divergent_price,",
" size=0.001, filled_size=0.001, remaining_size=0.0,",
' reason="divergence_test",',
" )",
" k.on_venue_event(div_event); await asyncio.sleep(0.3)",
"# Exit at market",
"_si(k, E.EXIT, tid, symbol, 'SHORT', p*0.995, 0.001); await asyncio.sleep(0.5)",
])
# ===== 6. Negative-capital boundary =====
B("neg_cap_entry_rejected", [
'tid = f"nc-{int(__import__(\"time\").time()*1000)}"',
"# Kernel should reject ENTER if capital cannot cover margin",
"# With tiny capital, even a tiny trade should be checked",
"k.account.snapshot.capital = 0.0",
"r = _si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.3)",
'info = _inspect_outcome(r, "neg_cap")',
'# May be rejected or accepted depending on kernel margin logic',
'# At minimum, kernel should not crash',
"# Restore capital and do normal trade",
"k.account.snapshot.capital = 25000.0",
'_si(k, E.ENTER, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(0.8)',
'_si(k, E.EXIT, tid, symbol, "SHORT", p*0.995, 0.001); await asyncio.sleep(0.5)',
])
# ===== 7. Sub-sample cross-application =====
# Apply the new assertion patterns to a basic entry/exit
B("cross_sample_basic_entry_exit_outcome", [
'tid = f"cs-{int(__import__(\"time\").time()*1000)}"',
"cb = k.account.snapshot.capital; k._start_cap = cb",
"r1 = _si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.8)",
"_assert_accepted(r1, 'cs_entry')",
"_check_slot_accounting(k, 'cs_after_entry')",
"r2 = _si(k, E.EXIT, tid, symbol, 'SHORT', p*0.995, 0.001); await asyncio.sleep(0.5)",
"_assert_accepted(r2, 'cs_exit')",
"_check_slot_accounting(k, 'cs_after_exit')",
"ca = k.account.snapshot.capital",
"max_change = max(1.0, cb * 0.10)",
'assert cb - ca < max_change, f"cs: cap shrunk {cb} -> {ca}"',
])
B("cross_sample_cancel_reenter_outcome", [
't1 = f"csc-{int(__import__(\"time\").time()*1000)}"',
't2 = f"csc2-{int(__import__(\"time\").time()*1000)}"',
"cb = k.account.snapshot.capital; k._start_cap = cb",
"r1 = _si(k, E.ENTER, t1, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.3)",
"_assert_accepted(r1, 'cs_cancel_entry')",
"r2 = _si(k, E.CANCEL, t1, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.3)",
"if r2.accepted:",
' info = _inspect_outcome(r2, "cs_cancel")',
"if not k.slot(0).is_free():",
" _si(k, E.EXIT, t1, symbol, 'SHORT', p*0.995, 0.001); await asyncio.sleep(0.3)",
"_check_slot_accounting(k, 'cs_after_cancel')",
'assert k.slot(0).is_free(), "slot should be free after cancel"',
"r3 = _si(k, E.ENTER, t2, symbol, 'SHORT', p*0.997, 0.001); await asyncio.sleep(0.8)",
"_assert_accepted(r3, 'cs_reenter')",
"_check_slot_accounting(k, 'cs_after_reenter')",
"r4 = _si(k, E.EXIT, t2, symbol, 'SHORT', p*0.995, 0.001); await asyncio.sleep(0.5)",
"_assert_accepted(r4, 'cs_reenter_exit')",
"_check_slot_accounting(k, 'cs_after_reenter_exit')",
])
B("cross_sample_multi_leg_outcome", [
'tid = f"csm-{int(__import__(\"time\").time()*1000)}"',
"cb = k.account.snapshot.capital; k._start_cap = cb",
"r = _si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.002, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(0.8)",
"_assert_accepted(r, 'cs_ml_entry')",
"r = _si(k, E.EXIT, tid, symbol, 'SHORT', p*0.995, 0.001, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(0.4)",
"_assert_accepted(r, 'cs_ml_leg1')",
"_check_slot_accounting(k, 'cs_ml_after_leg1')",
"r = _si(k, E.EXIT, tid, symbol, 'SHORT', p*0.993, 0.001, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(0.4)",
"_assert_accepted(r, 'cs_ml_leg2')",
"_check_slot_accounting(k, 'cs_ml_after_leg2')",
])
B("cross_sample_leverage_tight_bounds", [
'tid = f"csl-{int(__import__(\"time\").time()*1000)}"',
"cb = k.account.snapshot.capital; k._start_cap = cb",
"r_ent = _si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.001, leverage=2); await asyncio.sleep(0.8)",
"_assert_accepted(r_ent, 'cs_lev_entry')",
"_check_slot_accounting(k, 'cs_lev_after_entry')",
"r_ex = _si(k, E.EXIT, tid, symbol, 'SHORT', p*0.995, 0.001, leverage=2); await asyncio.sleep(0.5)",
"_assert_accepted(r_ex, 'cs_lev_exit')",
"_check_slot_accounting(k, 'cs_lev_after_exit')",
"ca = k.account.snapshot.capital",
"max_change = max(1.0, cb * 0.10)",
'assert cb - ca < max_change, f"cs_lev: cap shrunk {cb} -> {ca}"',
])
# ===== BUILD =====
body_block = "".join(new_bodies)
param_block = "\n".join(new_params)
# Insert new bodies before SCENARIOS marker
marker = "SCENARIOS = ["
idx = content.index(marker)
# Insert after the last body section ends (blank line before SCENARIOS)
tail_start = content.rindex("\n\n", 0, idx) + 2
head = content[:tail_start]
tail = content[tail_start:]
with_bodies = head + body_block + tail
# Find SCENARIOS closing bracket and append new param entries
scenarios_open = with_bodies.index(marker)
close_bracket = with_bodies.index("]", scenarios_open)
final = with_bodies[:close_bracket] + "\n" + param_block + "\n" + with_bodies[close_bracket:]
# Compact blank lines
final = re.sub(r'\n{3,}', '\n\n', final)
with open(fpath, 'w') as f:
f.write(final)
import py_compile
py_compile.compile(fpath, doraise=True)
body_count = final.count("async def _body_")
param_count = final.count("pytest.param(")
print(f"Bodies: {body_count}, Params: {param_count}")
print("Parts 5: Compiles OK")

View File

@@ -0,0 +1,170 @@
import sys
sys.path.insert(0, '/mnt/dolphinng5_predict')
fpath = '/mnt/dolphinng5_predict/prod/tests/test_pink_bingx_dita_live_e2e.py'
with open(fpath) as f:
content = f.read()
# === PART 1: Expand imports ===
old_imports = """from prod.clean_arch.dita_v2.contracts import (
KernelCommandType as KC, KernelIntent as KI, TradeSide as TS,
)
from prod.clean_arch.ports.data_feed import MarketSnapshot"""
new_imports = """from prod.clean_arch.dita_v2.contracts import (
KernelCommandType as KC, KernelIntent as KI, TradeSide as TS,
VenueEvent, VenueEventStatus, KernelEventKind,
TradeStage, KernelDiagnosticCode, KernelSeverity,
KernelOutcome, KernelTransition, TradeSlot, VenueOrder,
)
from prod.clean_arch.ports.data_feed import MarketSnapshot"""
content = content.replace(old_imports, new_imports)
print("1: imports OK")
# === PART 2: Expand _build_rb with helpers ===
old_build = "def _build_rb(ic: float = 25000.0, max_slots: int = 1) -> RB:\n cfg = _build_config(ic)\n b = build_launcher_bundle(venue_mode=\"BINGX\", max_slots=max_slots, bingx_config=cfg)\n k = b.kernel; k.account.snapshot.capital = ic; k.account.snapshot.peak_capital = ic; k.account.snapshot.equity = ic\n class Shim:\n def __init__(self, k): self.kernel = k\n async def connect(self, initial_capital=0): self.kernel.venue.connect()\n async def disconnect(self):\n try: self.kernel.venue.disconnect()\n except: pass\n return RB(runtime=Shim(k), config=cfg)"
new_build = """def _build_rb(ic: float = 25000.0, max_slots: int = 1) -> RB:
cfg = _build_config(ic)
b = build_launcher_bundle(venue_mode=\"BINGX\", max_slots=max_slots, bingx_config=cfg)
k = b.kernel; k.account.snapshot.capital = ic; k.account.snapshot.peak_capital = ic; k.account.snapshot.equity = ic
class Shim:
def __init__(self, k): self.kernel = k
async def connect(self, initial_capital=0): self.kernel.venue.connect()
async def disconnect(self):
try: self.kernel.venue.disconnect()
except: pass
return RB(runtime=Shim(k), config=cfg)
def _build_portfolio_rb(ic: float = 25000.0, max_slots: int = 2) -> RB:
return _build_rb(ic=ic, max_slots=max_slots)
def _inspect_outcome(r, label):
info = {
\"accepted\": r.accepted,
\"state\": r.state.value if r.state else \"\",
\"diagnostic\": r.diagnostic_code.value if r.diagnostic_code else \"\",
\"severity\": r.severity.value if r.severity else \"\",
\"transitions\": [(t.prev_state.value, t.next_state.value) for t in (r.transitions or ())],
\"event_kinds\": [e.kind.value for e in (r.emitted_events or ())],
\"details\": dict(r.details or {}),
}
return info
def _assert_accepted(r, label):
info = _inspect_outcome(r, label)
assert r.accepted, f\"{label}: intent rejected - diag={info['diagnostic']} state={info['state']} detail={info['details']}\"
def _assert_rejected(r, expected_diag, label):
info = _inspect_outcome(r, label)
assert not r.accepted, f\"{label}: expected rejection but got accepted state={info['state']}\"
assert info['diagnostic'] == expected_diag, f\"{label}: expected diag={expected_diag} got {info['diagnostic']} detail={info['details']}\"
def _check_slot_accounting(k, label):
start_cap = getattr(k, '_start_cap', None)
if start_cap is None:
return
total_rp = sum(k.slot(i).realized_pnl for i in range(k.max_slots))
total_up = sum(k.slot(i).unrealized_pnl for i in range(k.max_slots))
expected = start_cap + total_rp + total_up
actual = k.account.snapshot.capital
diff = abs(actual - expected)
assert diff < 0.01, f\"{label}: accounting mismatch cap={actual} exp={expected} rp={total_rp} upnl={total_up} diff={diff}\"
def _check_open_orders(c, vs):
r = __import__('asyncio').run(c._request_json(
\"GET\", \"/openApi/swap/v2/trade/openOrders\",
{\"symbol\": vs}, signed=True
))
data = r if isinstance(r, list) else (r.get(\"data\") or r.get(\"orders\") or [])
return [o for o in data if isinstance(o, dict)]
async def _verify_full(c, vs):
rs = await _contract_rows(c)
tr = [r for r in rs if str(r.get(\"symbol\",\"\")).upper().replace(\"-\",\"\") == vs.replace(\"-\",\"\").upper()]
ts = sum(abs(float(r.get(\"positionAmt\",r.get(\"positionQty\",0)) or 0)) for r in tr)
flat = ts < 1e-8
oos = _check_open_orders(c, vs)
no_orders = len(oos) == 0
err = \"\"
if not flat: err += f\"pos_open: {tr} \"
if not no_orders: err += f\"open_orders: {oos} \"
return {\"symbol\": vs, \"flat\": flat, \"no_orders\": no_orders, \"error\": err.strip()}
def _build_fresh_kernel_from_slot(slot_data, ic=25000.0):
from prod.clean_arch.dita_v2.rust_backend import _slot_from_payload
cfg = _build_config(ic)
b = build_launcher_bundle(venue_mode=\"BINGX\", max_slots=1, bingx_config=cfg)
k = b.kernel; k.account.snapshot.capital = ic; k.account.snapshot.peak_capital = ic; k.account.snapshot.equity = ic
restored = _slot_from_payload(slot_data)
k.reconcile_from_slots([restored])
class Shim:
def __init__(self, k): self.kernel = k
async def connect(self, initial_capital=0): self.kernel.venue.connect()
async def disconnect(self):
try: self.kernel.venue.disconnect()
except: pass
return RB(runtime=Shim(k), config=cfg)"""
content = content.replace(old_build, new_build)
print("2: build/helpers OK")
# === PART 3: Update _verify to check open orders ===
old_verify = "async def _verify(c, vs):\n rs = await _contract_rows(c)\n tr = [r for r in rs if str(r.get(\"symbol\",\"\")).upper().replace(\"-\",\"\") == vs.replace(\"-\",\"\").upper()]\n ts = sum(abs(float(r.get(\"positionAmt\",r.get(\"positionQty\",0)) or 0)) for r in tr)\n flat = ts < 1e-8\n return VR(symbol=vs, positions_flat=flat, error=\"\" if flat else f\"open: {tr}\")"
new_verify = "async def _verify(c, vs):\n rs = await _contract_rows(c)\n tr = [r for r in rs if str(r.get(\"symbol\",\"\")).upper().replace(\"-\",\"\") == vs.replace(\"-\",\"\").upper()]\n ts = sum(abs(float(r.get(\"positionAmt\",r.get(\"positionQty\",0)) or 0)) for r in tr)\n flat = ts < 1e-8\n oos = _check_open_orders(c, vs)\n no_orders = len(oos) == 0\n err = \"\"\n if not flat: err += f\"pos_open: {tr} \"\n if not no_orders: err += f\"open_orders: {oos} \"\n return VR(symbol=vs, positions_flat=flat and no_orders, error=err.strip())"
content = content.replace(old_verify, new_verify)
print("3: verify OK")
# === PART 4: Replace _run ===
# Find old _run and replace
old_run_pat = "async def _run(bundle, client, body_fn, label, ic):"
# Find the entire old run function bounds
idx = content.index(old_run_pat)
run_end = content.index(" finally:", idx)
run_end = content.index("\n\n", run_end) + 2
new_run = """async def _run(bundle, client, body_fn, label, ic):
k = bundle.runtime.kernel
sym = await _pick_sym(k, client)
snap, vsym = await _snap(client, sym)
await bundle.runtime.connect(initial_capital=ic)
p = float(snap.price)
try:
for si in range(k.max_slots):
if not k.slot(si).is_free():
_flatten(k, sym, p*0.99 if si == 0 else p*1.005, f"{label}-pre-{si}")
await asyncio.sleep(0.3)
k._start_cap = k.account.snapshot.capital
cb = k.account.snapshot.capital
await body_fn(k, sym, p)
ca = k.account.snapshot.capital
assert ca > 0, f"Capital zero: {ca}"
max_change = max(1.0, cb * 0.10)
assert cb - ca < max_change, f"Capital shrunk beyond tolerance: {cb} -> {ca} (limit={max_change})"
total_rp = sum(k.slot(i).realized_pnl for i in range(k.max_slots))
if abs(total_rp) > 0.0001:
assert abs(total_rp) < abs(cb - ca) + 0.01, f"{label}: rp={total_rp} != cap_change={cb-ca}"
for si in range(k.max_slots):
if not k.slot(si).is_free():
_flatten(k, sym, p*0.99 if si == 0 else p*1.005, f"{label}-post-{si}")
await asyncio.sleep(1.0)
_throttle(3.0)
return await _verify(client, vsym)
finally:
await bundle.runtime.disconnect()
"""
content = content[:idx] + new_run + content[run_end:]
print("4: run OK")
with open(fpath, 'w') as f:
f.write(content)
import py_compile
py_compile.compile(fpath, doraise=True)
print("Parts 1-4: Compiles OK")

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,123 @@
"""Account projection for DITAv2."""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any, Dict, Iterable, Optional
import math
from .contracts import TradeSide, TradeSlot, TradeStage
from .utils import safe_float
@dataclass
class AccountSnapshot:
"""Derived account state."""
capital: float
equity: float
realized_pnl: float = 0.0
unrealized_pnl: float = 0.0
open_positions: int = 0
open_notional: float = 0.0
fees_paid: float = 0.0
trade_seq: int = 0
peak_capital: float = 0.0
@property
def leverage(self) -> float:
if self.capital <= 0 or self.open_notional <= 0:
return 0.0
return self.open_notional / self.capital
@dataclass
class AccountProjection:
"""Aggregate account view over all active slots."""
runtime_namespace: str = "dita_v2"
strategy_namespace: str = "dita_v2"
event_namespace: str = "dita_v2"
actor_name: str = "ExecutionKernel"
exec_venue: str = "bingx"
data_venue: str = "binance"
ledger_authority: str = "exchange"
min_capital: float = 0.0
max_capital: Optional[float] = None
snapshot: AccountSnapshot = field(default_factory=lambda: AccountSnapshot(capital=25_000.0, equity=25_000.0))
def observe_slots(self, slots: Iterable[TradeSlot]) -> None:
open_positions = 0
open_notional = 0.0
unrealized_pnl = 0.0
for slot in slots:
if slot.closed or slot.size <= 0:
continue
if slot.fsm_state in {TradeStage.POSITION_OPEN, TradeStage.POSITION_OPENED, TradeStage.ENTRY_WORKING, TradeStage.EXIT_WORKING}:
open_positions += 1
mark = safe_float(slot.entry_price, 0.0)
mark = safe_float(slot.metadata.get("mark_price"), mark)
open_notional += abs(slot.size) * abs(mark)
unrealized_pnl += float(slot.unrealized_pnl or 0.0)
self.snapshot.open_positions = open_positions
self.snapshot.open_notional = open_notional
self.snapshot.unrealized_pnl = unrealized_pnl
self.snapshot.equity = self.snapshot.capital + unrealized_pnl
if not math.isfinite(self.snapshot.equity):
self.snapshot.equity = self.snapshot.capital
if open_notional > 0 and self.snapshot.capital > 0:
self.snapshot.peak_capital = max(self.snapshot.peak_capital, self.snapshot.capital)
def settle(self, realized_pnl: float, fees: float = 0.0) -> None:
realized_pnl = safe_float(realized_pnl, 0.0)
new_capital = safe_float(self.snapshot.capital + realized_pnl, self.snapshot.capital)
if self.max_capital is not None:
new_capital = min(new_capital, self.max_capital)
new_capital = max(self.min_capital, new_capital)
self.snapshot.capital = new_capital
self.snapshot.realized_pnl += realized_pnl
self.snapshot.fees_paid += safe_float(fees, 0.0)
self.snapshot.equity = self.snapshot.capital + self.snapshot.unrealized_pnl
if not math.isfinite(self.snapshot.equity):
self.snapshot.equity = self.snapshot.capital
def to_account_event(
self,
*,
timestamp: datetime,
trade_id: str,
asset: str,
side: TradeSide,
stage: TradeStage,
reason: str,
pnl: float = 0.0,
pnl_pct: float = 0.0,
bars_held: int = 0,
metadata: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
self.snapshot.equity = self.snapshot.capital + self.snapshot.unrealized_pnl
return {
"timestamp": timestamp.isoformat() if hasattr(timestamp, "isoformat") else str(timestamp),
"runtime_namespace": self.runtime_namespace,
"strategy_namespace": self.strategy_namespace,
"event_namespace": self.event_namespace,
"actor_name": self.actor_name,
"exec_venue": self.exec_venue,
"data_venue": self.data_venue,
"ledger_authority": self.ledger_authority,
"capital": float(self.snapshot.capital),
"equity": float(self.snapshot.equity),
"open_positions": int(self.snapshot.open_positions),
"current_open_notional": float(self.snapshot.open_notional),
"current_account_leverage": float(self.snapshot.leverage),
"trade_id": trade_id,
"asset": asset,
"side": side.value,
"reason": reason,
"stage": stage.value,
"pnl": float(pnl),
"pnl_pct": float(pnl_pct),
"bars_held": int(bars_held),
"metadata": dict(metadata or {}),
}

View File

@@ -0,0 +1,590 @@
"""DITAv2 BingX venue adapter.
This is a thin normalization layer over the existing direct BingX execution
surface. It converts BingX REST/account/order payloads into DITAv2
``VenueEvent`` / ``VenueOrder`` objects without reimplementing exchange logic.
"""
from __future__ import annotations
import asyncio
import concurrent.futures
import inspect
import itertools
import re
import threading
from datetime import datetime, timezone
from typing import Any, Iterable, List, Optional
from prod.clean_arch.dita import DecisionAction as LegacyDecisionAction
from prod.clean_arch.dita import Intent as LegacyIntent
from prod.clean_arch.dita import TradeSide as LegacyTradeSide
from prod.bingx.http import BingxHttpError
from .contracts import (
KernelCommandType,
KernelEventKind,
KernelIntent,
TradeSide,
VenueEvent,
VenueEventStatus,
VenueOrder,
VenueOrderStatus,
)
from .utils import json_safe
from .utils import safe_float
from .venue import VenueAdapter
def _row_text(row: dict[str, Any], *keys: str, default: str = "") -> str:
for key in keys:
value = row.get(key)
if value is None:
continue
text = str(value)
if text:
return text
return default
def _row_float(row: dict[str, Any], *keys: str, default: float = 0.0) -> float:
for key in keys:
try:
value = float(row.get(key) or 0.0)
except Exception:
continue
if value == value and value not in (float("inf"), float("-inf")) and value != 0.0:
return value
return default
def _normalize_status(status: str) -> str:
return str(status or "").strip().upper()
def _trade_side_from_row(row: dict[str, Any], *, fallback: TradeSide = TradeSide.FLAT) -> TradeSide:
side_raw = _row_text(row, "side", "positionSide", default="").upper()
signed_qty = _row_float(row, "positionAmt", "positionQty", "positionSize", "quantity", "pa", default=0.0)
if side_raw in {"BUY", "LONG"}:
return TradeSide.LONG
if side_raw in {"SELL", "SHORT"}:
return TradeSide.SHORT
if signed_qty < 0:
return TradeSide.SHORT
if signed_qty > 0:
return TradeSide.LONG
return fallback
def _venue_event_status_from_row(status: str) -> VenueEventStatus:
normalized = _normalize_status(status)
if normalized in {"NEW", "ACKED", "PENDING", "CREATED"}:
return VenueEventStatus.ACKED
if normalized in {"RATE_LIMITED", "THROTTLED"}:
return VenueEventStatus.RATE_LIMITED
if normalized in {"PARTIALLY_FILLED", "PARTIAL_FILL"}:
return VenueEventStatus.PARTIALLY_FILLED
if normalized in {"FILLED", "FULL_FILL"}:
return VenueEventStatus.FILLED
if normalized in {"CANCELED", "CANCELLED", "EXPIRED"}:
return VenueEventStatus.CANCELED
if normalized in {"REJECTED", "FAILED"}:
return VenueEventStatus.REJECTED
if normalized in {"CANCEL_REJECTED", "CANCEL_REJECT"}:
return VenueEventStatus.CANCELED_REJECTED
return VenueEventStatus.ACKED
def _venue_order_status_from_row(status: str) -> VenueOrderStatus:
normalized = _normalize_status(status)
if normalized in {"NEW", "ACKED", "PENDING", "CREATED"}:
return VenueOrderStatus.NEW
if normalized in {"RATE_LIMITED", "THROTTLED"}:
return VenueOrderStatus.NEW
if normalized in {"PARTIALLY_FILLED", "PARTIAL_FILL"}:
return VenueOrderStatus.PARTIALLY_FILLED
if normalized in {"FILLED", "FULL_FILL"}:
return VenueOrderStatus.FILLED
if normalized in {"CANCELED", "CANCELLED", "EXPIRED"}:
return VenueOrderStatus.CANCELED
if normalized in {"REJECTED", "FAILED"}:
return VenueOrderStatus.REJECTED
return VenueOrderStatus.NEW
def _position_qty(row: dict[str, Any]) -> float:
qty = _row_float(row, "positionAmt", "positionQty", "positionSize", "quantity", "pa", default=0.0)
if qty != 0.0:
return abs(qty)
return abs(_row_float(row, "executedQty", "filledQty", "z", default=0.0))
def _position_price(row: dict[str, Any]) -> float:
return _row_float(row, "entryPrice", "avgPrice", "avgEntryPrice", "ep", "ap", "price", "lastFillPrice", "tradePrice")
def _mapping_for_snapshot(rows: Iterable[dict[str, Any]]) -> dict[str, dict[str, Any]]:
mapping: dict[str, dict[str, Any]] = {}
for row in rows:
client_id = _row_text(row, "clientOrderID", "clientOrderId", default="")
order_id = _row_text(row, "orderId", "orderID", "id", default="")
key = client_id or order_id
if key:
mapping[key] = dict(row)
if order_id and order_id not in mapping:
mapping[order_id] = dict(row)
return mapping
def _venue_order_from_row(
row: dict[str, Any],
*,
internal_trade_id: str = "",
fallback_side: TradeSide = TradeSide.FLAT,
) -> VenueOrder:
side = _trade_side_from_row(row, fallback=fallback_side)
client_id = _row_text(row, "clientOrderID", "clientOrderId", default="")
order_id = _row_text(row, "orderId", "orderID", "id", default="")
intended = _row_float(row, "origQty", "quantity", "q", "positionAmt", "positionQty", default=0.0)
if intended <= 0:
intended = _position_qty(row)
return VenueOrder(
internal_trade_id=internal_trade_id or client_id or order_id,
venue_order_id=order_id,
venue_client_id=client_id,
side=side,
intended_size=abs(float(intended or 0.0)),
filled_size=abs(_row_float(row, "executedQty", "filledQty", "z", "lastFilledQty", default=0.0)),
average_fill_price=_position_price(row),
status=_venue_order_status_from_row(_row_text(row, "status", "X", default="NEW")),
metadata={"raw": dict(row)},
)
def _event_id(seq: itertools.count) -> str:
return f"EV-{next(seq):08d}"
def _rate_limit_retry_after_ms(row: dict[str, Any]) -> int:
raw_retry = row.get("retryAfter") or row.get("retry_after_ms") or row.get("retryAfterMs")
if raw_retry is None:
msg = _row_text(row, "msg", "message", default="")
match = re.search(r"unblocked after (\d+)", msg)
if match:
try:
ts = int(match.group(1))
now_ms = int(datetime.now(timezone.utc).timestamp() * 1000)
return max(0, ts - now_ms)
except Exception:
return 0
return 0
try:
return max(0, int(float(raw_retry)))
except Exception:
return 0
class BingxVenueAdapter(VenueAdapter):
"""Normalizes BingX execution responses into DITAv2 venue events."""
# Shared thread-pool executor reused across all adapter instances and
# all calls. Threads are created once and recycled, eliminating the
# per-call creation/destruction overhead of the old pattern.
_EXECUTOR: concurrent.futures.ThreadPoolExecutor | None = None
_EXECUTOR_LOCK: threading.Lock = threading.Lock()
@classmethod
def _get_executor(cls) -> concurrent.futures.ThreadPoolExecutor:
if cls._EXECUTOR is None:
with cls._EXECUTOR_LOCK:
if cls._EXECUTOR is None:
# max_workers=3 so three concurrent HTTP calls (balance,
# positions, openOrders) can proceed simultaneously without
# serialising on the pool.
cls._EXECUTOR = concurrent.futures.ThreadPoolExecutor(
max_workers=3,
thread_name_prefix="bingx_adapter",
)
return cls._EXECUTOR
def __init__(self, backend: Any | None = None, *, config: Any | None = None) -> None:
if backend is None:
if config is None:
raise ValueError("BingxVenueAdapter requires a backend or config")
from prod.clean_arch.adapters.bingx_direct import BingxDirectExecutionAdapter
backend = BingxDirectExecutionAdapter(config)
self.backend = backend
self._event_seq = itertools.count(1)
# Thread-safe snapshot cache — reads from a snapshot may arrive from
# the kernel thread while _backend_snapshot writes from the pool thread.
self._snap_lock = threading.Lock()
self._last_snapshot = None
self._snapshot_ready = threading.Event()
self._snapshot_ready.set() # initially ready (no pending write)
def _run(self, result: Any) -> Any:
if inspect.isawaitable(result):
try:
asyncio.get_running_loop()
except RuntimeError:
return asyncio.run(result)
# Inside a running event loop: submit to the shared singleton
# executor so threads are reused across calls.
pool = self._get_executor()
return pool.submit(asyncio.run, result).result()
return result
def _call_backend(self, method_name: str, *args: Any, **kwargs: Any) -> Any:
method = getattr(self.backend, method_name, None)
if method is None:
raise AttributeError(f"backend has no method {method_name}")
return self._run(method(*args, **kwargs))
def _backend_snapshot(self, *, include_history: bool = False, timeout_ms: float = 5000.0):
"""Fetch a fresh snapshot from the backend and cache it thread-safely.
Design (industry best-practice reader-writer pattern):
- A caller that needs a fresh snapshot *waits* on ``_snapshot_ready``
before reading, so it never sees a stale partial write.
- While a snapshot fetch is in-flight, the lock is cleared; concurrent
callers block on ``_snapshot_ready`` with a timeout. If the fetch
succeeds in time they get the fresh snapshot; if it times out they
fall back to ``_last_snapshot`` (an eventually-consistent design —
stale data that *was* consistent is safer than no data).
- The write is guarded by ``_snap_lock`` so concurrent writes are
serialised and ``_last_snapshot`` is never partially assigned.
"""
if not self._snapshot_ready.wait(timeout=timeout_ms / 1000.0):
# Timeout waiting for a previous snapshot write — return the
# last-known-good snapshot rather than blocking the caller.
with self._snap_lock:
return self._last_snapshot
self._snapshot_ready.clear()
try:
snapshot = self._call_backend("refresh_state", None, include_history=include_history)
except Exception:
self._snapshot_ready.set()
raise
with self._snap_lock:
self._last_snapshot = snapshot
self._snapshot_ready.set()
return snapshot
@staticmethod
def _legacy_intent(intent: KernelIntent) -> LegacyIntent:
action = LegacyDecisionAction.ENTER if intent.action == KernelCommandType.ENTER else LegacyDecisionAction.EXIT
side = LegacyTradeSide.SHORT if intent.side == TradeSide.SHORT else LegacyTradeSide.LONG
return LegacyIntent(
timestamp=intent.timestamp,
trade_id=intent.trade_id,
decision_id=intent.intent_id,
asset=intent.asset,
action=action,
side=side,
reason=intent.reason,
target_size=float(intent.target_size),
leverage=float(intent.leverage),
reference_price=float(intent.reference_price),
confidence=1.0,
bars_held=0,
exit_leg_ratios=tuple(intent.exit_leg_ratios or (1.0,)),
metadata=dict(intent.metadata),
)
def connect(self) -> bool:
result = getattr(self.backend, "connect", None)
if result is not None:
self._run(result())
self._backend_snapshot(include_history=True)
return True
def cancel(self, order: VenueOrder, *, reason: str = "") -> List[VenueEvent]:
snapshot_before = self._backend_snapshot(include_history=True)
response = None
if hasattr(self.backend, "cancel_order"):
response = self._call_backend("cancel_order", order, reason=reason)
elif hasattr(self.backend, "cancel"):
response = self._call_backend("cancel", order, reason=reason)
else:
client = getattr(self.backend, "_client", None)
instrument_symbol = ""
if hasattr(self.backend, "_instrument_venue_symbol"):
asset = str(order.metadata.get("asset") or order.internal_trade_id or order.venue_client_id or "")
instrument_symbol = str(self.backend._instrument_venue_symbol(asset))
if client is None or not instrument_symbol:
raise RuntimeError("backend does not expose a cancel surface")
params = {"symbol": instrument_symbol}
if order.venue_order_id:
params["orderId"] = order.venue_order_id
else:
params["clientOrderId"] = order.venue_client_id
try:
response = self._run(client.signed_delete("/openApi/swap/v2/trade/order", params))
except BingxHttpError as exc:
response = {"status": "REJECTED", "msg": str(exc), "orderId": order.venue_order_id, "clientOrderId": order.venue_client_id}
snapshot_after = self._backend_snapshot(include_history=True)
return self._events_from_cancel(order, response, snapshot_before, snapshot_after, reason=reason)
def open_orders(self) -> List[VenueOrder]:
snapshot = self._backend_snapshot(include_history=False)
return [_venue_order_from_row(row) for row in (snapshot.open_orders or [])]
def open_positions(self) -> List[dict[str, Any]]:
snapshot = self._backend_snapshot(include_history=False)
return [dict(row) for row in (snapshot.open_positions or {}).values()]
def reconcile(self) -> List[VenueEvent]:
snapshot = self._backend_snapshot(include_history=True)
return self._events_from_snapshot(snapshot)
def submit(self, intent: KernelIntent) -> List[VenueEvent]:
snapshot_before = self._backend_snapshot(include_history=True)
receipt = self._call_backend("submit_intent", self._legacy_intent(intent))
snapshot_after = self._backend_snapshot(include_history=True)
return self._events_from_submit(intent, receipt, snapshot_before, snapshot_after)
def _events_from_submit(self, intent: KernelIntent, receipt: Any, before, after) -> List[VenueEvent]: # noqa: ANN001
ack_row = dict(getattr(receipt, "raw_ack", {}) or {})
status = _normalize_status(getattr(receipt, "status", "") or _row_text(ack_row, "status", default="NEW"))
order_id = _row_text(ack_row, "orderId", "orderID", default=str(getattr(receipt, "order_id", "") or ""))
client_order_id = _row_text(ack_row, "clientOrderID", "clientOrderId", default=str(getattr(receipt, "client_order_id", "") or intent.intent_id))
if status in {"RATE_LIMITED", "THROTTLED"}:
return [
VenueEvent(
timestamp=getattr(receipt, "timestamp", datetime.now(timezone.utc)),
event_id=_event_id(self._event_seq),
trade_id=intent.trade_id,
slot_id=intent.slot_id,
kind=KernelEventKind.RATE_LIMITED,
status=VenueEventStatus.RATE_LIMITED,
venue_order_id=order_id,
venue_client_id=client_order_id,
side=intent.side,
asset=intent.asset,
price=safe_float(getattr(receipt, "price", 0.0), 0.0),
size=float(intent.target_size or 0.0),
filled_size=0.0,
remaining_size=float(intent.target_size or 0.0),
reason=_row_text(ack_row, "msg", "message", default="BINGX_RATE_LIMITED"),
raw_payload=ack_row or json_safe(receipt),
metadata={"intent_id": intent.intent_id, "action": intent.action.value, "retry_after_ms": _rate_limit_retry_after_ms(ack_row)},
)
]
base_event = VenueEvent(
timestamp=getattr(receipt, "timestamp", datetime.now(timezone.utc)),
event_id=_event_id(self._event_seq),
trade_id=intent.trade_id,
slot_id=intent.slot_id,
kind=KernelEventKind.ORDER_ACK,
status=VenueEventStatus.ACKED,
venue_order_id=order_id,
venue_client_id=client_order_id,
side=intent.side,
asset=intent.asset,
price=safe_float(getattr(receipt, "price", 0.0), 0.0),
size=float(intent.target_size or 0.0),
filled_size=0.0,
remaining_size=float(intent.target_size or 0.0),
reason="",
raw_payload=ack_row or json_safe(receipt),
metadata={"intent_id": intent.intent_id, "action": intent.action.value},
)
if status in {"REJECTED", "FAILED"}:
return [
VenueEvent(
**{**base_event.__dict__, "event_id": _event_id(self._event_seq), "kind": KernelEventKind.ORDER_REJECT, "status": VenueEventStatus.REJECTED, "reason": _row_text(ack_row, "msg", "message", default="BINGX_ORDER_REJECTED")},
)
]
events = [base_event]
fill_status = _venue_event_status_from_row(status)
filled_size = _row_float(ack_row, "executedQty", "cumFilledQty", "filledQty", "lastFilledQty", default=0.0)
snapshot_fill_size = self._filled_size_from_snapshots(before, after, intent.asset)
if filled_size <= 0:
filled_size = snapshot_fill_size
emit_fill = fill_status in {VenueEventStatus.PARTIALLY_FILLED, VenueEventStatus.FILLED} or snapshot_fill_size > 0.0
if emit_fill:
if filled_size <= 0:
filled_size = float(intent.target_size or 0.0)
remaining_size = max(0.0, float(intent.target_size or 0.0) - float(filled_size))
fill_kind = KernelEventKind.FULL_FILL if fill_status == VenueEventStatus.FILLED or remaining_size <= 1e-12 else KernelEventKind.PARTIAL_FILL
events.append(
VenueEvent(
timestamp=base_event.timestamp,
event_id=_event_id(self._event_seq),
trade_id=intent.trade_id,
slot_id=intent.slot_id,
kind=fill_kind,
status=VenueEventStatus.FILLED if fill_kind == KernelEventKind.FULL_FILL else VenueEventStatus.PARTIALLY_FILLED,
venue_order_id=order_id,
venue_client_id=client_order_id,
side=intent.side,
asset=intent.asset,
price=safe_float(_row_float(ack_row, "avgPrice", "ap", "price", "lastFillPrice", default=getattr(receipt, "price", 0.0)), 0.0),
size=float(intent.target_size or 0.0),
filled_size=float(filled_size),
remaining_size=float(remaining_size),
reason="",
raw_payload=ack_row or json_safe(receipt),
metadata={"intent_id": intent.intent_id, "action": intent.action.value},
)
)
return events
def _events_from_cancel(self, order: VenueOrder, response: Any, before, after, *, reason: str = "") -> List[VenueEvent]: # noqa: ANN001
raw = response if isinstance(response, dict) else {}
status = _normalize_status(_row_text(raw, "status", default="CANCELED"))
if status in {"RATE_LIMITED", "THROTTLED"}:
return [
VenueEvent(
timestamp=datetime.now(timezone.utc),
event_id=_event_id(self._event_seq),
trade_id=order.internal_trade_id or order.venue_client_id,
slot_id=int(order.metadata.get("slot_id", 0) or 0),
kind=KernelEventKind.RATE_LIMITED,
status=VenueEventStatus.RATE_LIMITED,
venue_order_id=order.venue_order_id,
venue_client_id=order.venue_client_id,
side=order.side,
asset=str(order.metadata.get("asset") or ""),
price=safe_float(_row_float(raw, "avgPrice", "ap", "price", "lastFillPrice", default=order.average_fill_price), 0.0),
size=float(order.intended_size or 0.0),
filled_size=float(order.filled_size or 0.0),
remaining_size=float(order.remaining_size),
reason=reason or _row_text(raw, "msg", "message", default="BINGX_RATE_LIMITED"),
raw_payload=raw or {"orderId": order.venue_order_id, "clientOrderId": order.venue_client_id, "status": status or "RATE_LIMITED"},
metadata={**dict(order.metadata), "retry_after_ms": _rate_limit_retry_after_ms(raw)},
)
]
event_status = _venue_event_status_from_row(status)
kind = KernelEventKind.CANCEL_ACK if event_status == VenueEventStatus.CANCELED else KernelEventKind.CANCEL_REJECT
if event_status == VenueEventStatus.CANCELED_REJECTED:
kind = KernelEventKind.CANCEL_REJECT
return [
VenueEvent(
timestamp=datetime.now(timezone.utc),
event_id=_event_id(self._event_seq),
trade_id=order.internal_trade_id or order.venue_client_id,
slot_id=int(order.metadata.get("slot_id", 0) or 0),
kind=kind,
status=event_status,
venue_order_id=order.venue_order_id,
venue_client_id=order.venue_client_id,
side=order.side,
asset=str(order.metadata.get("asset") or ""),
price=safe_float(_row_float(raw, "avgPrice", "ap", "price", "lastFillPrice", default=order.average_fill_price), 0.0),
size=float(order.intended_size or 0.0),
filled_size=float(order.filled_size or 0.0),
remaining_size=float(order.remaining_size),
reason=reason or _row_text(raw, "msg", "message", default="BINGX_CANCEL_ACK" if kind == KernelEventKind.CANCEL_ACK else "BINGX_CANCEL_REJECT"),
raw_payload=raw or {"orderId": order.venue_order_id, "clientOrderId": order.venue_client_id, "status": status or event_status.value},
metadata=dict(order.metadata),
)
]
def _events_from_snapshot(self, snapshot: Any) -> List[VenueEvent]: # noqa: ANN001
events: list[VenueEvent] = []
seen: set[tuple[str, str, str]] = set()
for row in getattr(snapshot, "open_orders", []) or []:
if not isinstance(row, dict):
continue
event = self._event_from_row(row, slot_id=0)
key = (event.venue_client_id, event.venue_order_id, event.kind.value)
if key not in seen:
seen.add(key)
events.append(event)
for row in getattr(snapshot, "all_orders", []) or []:
if not isinstance(row, dict):
continue
event = self._event_from_row(row, slot_id=0)
key = (event.venue_client_id, event.venue_order_id, event.kind.value)
if key not in seen:
seen.add(key)
events.append(event)
for row in getattr(snapshot, "all_fills", []) or []:
if not isinstance(row, dict):
continue
event = self._fill_event_from_row(row)
key = (event.venue_client_id, event.venue_order_id, event.kind.value)
if key not in seen:
seen.add(key)
events.append(event)
return events
def _event_from_row(self, row: dict[str, Any], *, slot_id: int) -> VenueEvent:
status = _normalize_status(_row_text(row, "status", "X", default="NEW"))
event_status = _venue_event_status_from_row(status)
kind = {
VenueEventStatus.ACKED: KernelEventKind.ORDER_ACK,
VenueEventStatus.PARTIALLY_FILLED: KernelEventKind.PARTIAL_FILL,
VenueEventStatus.FILLED: KernelEventKind.FULL_FILL,
VenueEventStatus.CANCELED: KernelEventKind.CANCEL_ACK,
VenueEventStatus.REJECTED: KernelEventKind.ORDER_REJECT,
VenueEventStatus.CANCELED_REJECTED: KernelEventKind.CANCEL_REJECT,
VenueEventStatus.RATE_LIMITED: KernelEventKind.RATE_LIMITED,
}.get(event_status, KernelEventKind.ORDER_ACK)
size = _row_float(row, "origQty", "quantity", "q", "positionAmt", default=0.0)
filled = _row_float(row, "executedQty", "cumFilledQty", "filledQty", "z", "lastFilledQty", default=0.0)
if filled <= 0.0 and kind in {KernelEventKind.PARTIAL_FILL, KernelEventKind.FULL_FILL}:
filled = size
return VenueEvent(
timestamp=datetime.now(timezone.utc),
event_id=_event_id(self._event_seq),
trade_id=_row_text(row, "tradeId", "trade_id", default=_row_text(row, "clientOrderId", "clientOrderID", default="")),
slot_id=slot_id,
kind=kind,
status=event_status,
venue_order_id=_row_text(row, "orderId", "orderID", "id", default=""),
venue_client_id=_row_text(row, "clientOrderID", "clientOrderId", "c", default=""),
side=_trade_side_from_row(row),
asset=_row_text(row, "symbol", default=""),
price=safe_float(_row_float(row, "avgPrice", "ap", "price", "lastFillPrice", default=0.0), 0.0),
size=abs(float(size or 0.0)),
filled_size=abs(float(filled or 0.0)),
remaining_size=max(0.0, abs(float(size or 0.0)) - abs(float(filled or 0.0))),
reason=_row_text(row, "msg", "message", default=""),
raw_payload=dict(row),
metadata={"source": "bingx"},
)
def _fill_event_from_row(self, row: dict[str, Any]) -> VenueEvent:
status = _normalize_status(_row_text(row, "status", "X", default="FILLED"))
event_status = _venue_event_status_from_row(status)
kind = KernelEventKind.FULL_FILL if event_status == VenueEventStatus.FILLED else KernelEventKind.PARTIAL_FILL
return VenueEvent(
timestamp=datetime.now(timezone.utc),
event_id=_event_id(self._event_seq),
trade_id=_row_text(row, "tradeId", "trade_id", default=_row_text(row, "clientOrderId", "clientOrderID", default="")),
slot_id=0,
kind=kind,
status=event_status,
venue_order_id=_row_text(row, "orderId", "orderID", "id", default=""),
venue_client_id=_row_text(row, "clientOrderID", "clientOrderId", "c", default=""),
side=_trade_side_from_row(row),
asset=_row_text(row, "symbol", default=""),
price=safe_float(_row_float(row, "lastFillPrice", "L", "price", "ap", default=0.0), 0.0),
size=abs(_row_float(row, "executedQty", "z", "lastFilledQty", default=0.0)),
filled_size=abs(_row_float(row, "lastFilledQty", "l", "z", default=0.0)),
remaining_size=max(0.0, abs(_row_float(row, "executedQty", "z", "lastFilledQty", default=0.0)) - abs(_row_float(row, "lastFilledQty", "l", "z", default=0.0))),
reason=_row_text(row, "msg", "message", default=""),
raw_payload=dict(row),
metadata={"source": "bingx"},
)
@staticmethod
def _filled_size_from_snapshots(before: Any, after: Any, asset: str) -> float: # noqa: ANN001
def _lookup(snapshot: Any) -> float:
positions = getattr(snapshot, "open_positions", {}) or {}
for key, row in positions.items():
symbol = _row_text(row, "symbol", default=str(key))
if symbol.replace("-", "").replace("_", "").upper() == asset.replace("-", "").replace("_", "").upper():
return _position_qty(row)
return 0.0
before_qty = _lookup(before)
after_qty = _lookup(after)
diff = abs(before_qty - after_qty)
return diff

View File

@@ -0,0 +1,327 @@
"""Canonical v2 contracts for the DITAv2 execution kernel."""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Any, Dict, Mapping, Optional, Sequence, Tuple
class TradeSide(str, Enum):
"""Trade side."""
LONG = "LONG"
SHORT = "SHORT"
FLAT = "FLAT"
class TradeStage(str, Enum):
"""Execution stage for a trade slot."""
IDLE = "IDLE"
DECISION_CREATED = "DECISION_CREATED"
INTENT_CREATED = "INTENT_CREATED"
ORDER_REQUESTED = "ORDER_REQUESTED"
ORDER_SENT = "ORDER_SENT"
ORDER_ACKED = "ORDER_ACKED"
ORDER_REJECTED = "ORDER_REJECTED"
ENTRY_WORKING = "ENTRY_WORKING"
PARTIAL_FILL = "PARTIAL_FILL"
POSITION_OPENED = "POSITION_OPENED"
POSITION_OPEN = "POSITION_OPEN"
EXIT_REQUESTED = "EXIT_REQUESTED"
EXIT_SENT = "EXIT_SENT"
EXIT_ACKED = "EXIT_ACKED"
EXIT_REJECTED = "EXIT_REJECTED"
EXIT_WORKING = "EXIT_WORKING"
POSITION_PARTIALLY_CLOSED = "POSITION_PARTIALLY_CLOSED"
POSITION_CLOSED = "POSITION_CLOSED"
CLOSED = "CLOSED"
TRADE_TERMINAL_WRITTEN = "TRADE_TERMINAL_WRITTEN"
STALE_STATE_RECONCILING = "STALE_STATE_RECONCILING"
class KernelCommandType(str, Enum):
"""Kernel command types."""
ENTER = "ENTER"
EXIT = "EXIT"
MARK_PRICE = "MARK_PRICE"
RECONCILE = "RECONCILE"
CONTROL = "CONTROL"
CANCEL = "CANCEL"
class KernelEventKind(str, Enum):
"""Normalized venue event kinds."""
ORDER_ACK = "ORDER_ACK"
ORDER_REJECT = "ORDER_REJECT"
RATE_LIMITED = "RATE_LIMITED"
PARTIAL_FILL = "PARTIAL_FILL"
FULL_FILL = "FULL_FILL"
CANCEL_ACK = "CANCEL_ACK"
CANCEL_REJECT = "CANCEL_REJECT"
MARK_PRICE = "MARK_PRICE"
RECONCILE = "RECONCILE"
CONTROL = "CONTROL"
class KernelDiagnosticCode(str, Enum):
"""Structured diagnostic codes emitted by the kernel."""
OK = "OK"
RATE_LIMITED = "RATE_LIMITED"
INVALID_SLOT_ID = "INVALID_SLOT_ID"
UNSUPPORTED_INTENT = "UNSUPPORTED_INTENT"
SLOT_BUSY = "SLOT_BUSY"
NO_OPEN_POSITION = "NO_OPEN_POSITION"
NO_ACTIVE_EXIT_ORDER = "NO_ACTIVE_EXIT_ORDER"
UNKNOWN_EVENT_KIND = "UNKNOWN_EVENT_KIND"
ORDER_REJECTED = "ORDER_REJECTED"
ENTRY_ORDER_REJECTED = "ENTRY_ORDER_REJECTED"
EXIT_ORDER_REJECTED = "EXIT_ORDER_REJECTED"
CANCEL_REJECTED = "CANCEL_REJECTED"
STALE_STATE_RECONCILE = "STALE_STATE_RECONCILE"
RECONCILED = "RECONCILED"
DUPLICATE_EVENT = "DUPLICATE_EVENT"
UNRESOLVED_SLOT = "UNRESOLVED_SLOT"
INVALID_TRANSITION = "INVALID_TRANSITION"
TERMINAL_STATE = "TERMINAL_STATE"
class KernelSeverity(str, Enum):
"""Severity classification for kernel outcomes."""
INFO = "INFO"
WARNING = "WARNING"
ERROR = "ERROR"
CRITICAL = "CRITICAL"
class VenueOrderStatus(str, Enum):
"""Order status surface mirrored from venue truth."""
NEW = "NEW"
ACKED = "ACKED"
PARTIALLY_FILLED = "PARTIALLY_FILLED"
FILLED = "FILLED"
CANCELED = "CANCELED"
REJECTED = "REJECTED"
class VenueEventStatus(str, Enum):
"""Status alias for normalized venue events."""
ACKED = "ACKED"
REJECTED = "REJECTED"
RATE_LIMITED = "RATE_LIMITED"
PARTIALLY_FILLED = "PARTIALLY_FILLED"
FILLED = "FILLED"
CANCELED = "CANCELED"
CANCELED_REJECTED = "CANCEL_REJECTED"
@dataclass(frozen=True)
class VenueOrder:
"""Venue-specific order identity and fill state."""
internal_trade_id: str
venue_order_id: str
venue_client_id: str
side: TradeSide
intended_size: float
filled_size: float = 0.0
average_fill_price: float = 0.0
status: VenueOrderStatus = VenueOrderStatus.NEW
metadata: Dict[str, Any] = field(default_factory=dict)
@property
def remaining_size(self) -> float:
return max(0.0, float(self.intended_size) - float(self.filled_size))
@dataclass
class TradeSlot:
"""A single execution slot managed by the v2 kernel."""
slot_id: int
trade_id: str = ""
asset: str = ""
side: TradeSide = TradeSide.FLAT
entry_price: float = 0.0
size: float = 0.0
initial_size: float = 0.0
leverage: float = 0.0
entry_time: Optional[datetime] = None
unrealized_pnl: float = 0.0
realized_pnl: float = 0.0
closed: bool = False
exit_leg_ratios: Tuple[float, ...] = (1.0,)
active_leg_index: int = 0
active_exit_order: Optional[VenueOrder] = None
active_entry_order: Optional[VenueOrder] = None
fsm_state: TradeStage = TradeStage.IDLE
close_reason: str = ""
last_event_time: Optional[datetime] = None
seen_event_ids: Tuple[str, ...] = ()
metadata: Dict[str, Any] = field(default_factory=dict)
def is_free(self) -> bool:
return self.fsm_state in {TradeStage.IDLE, TradeStage.CLOSED} and float(self.size or 0.0) <= 0.0 and not self.active_entry_order and not self.active_exit_order
def is_open(self) -> bool:
return self.fsm_state in {
TradeStage.ENTRY_WORKING,
TradeStage.POSITION_OPENED,
TradeStage.POSITION_OPEN,
TradeStage.EXIT_WORKING,
} and not self.closed
def mark_price(self, price: float) -> None:
if price is None or price != price or price <= 0:
return
self.entry_price = self.entry_price or price
if self.entry_price <= 0 or self.size <= 0:
self.unrealized_pnl = 0.0
return
delta = (price - self.entry_price) / self.entry_price
if self.side == TradeSide.SHORT:
delta = -delta
self.unrealized_pnl = delta * self.size * self.entry_price * self.leverage
def next_exit_ratio(self) -> float:
if self.active_leg_index < len(self.exit_leg_ratios):
ratio = float(self.exit_leg_ratios[self.active_leg_index])
return max(0.0, min(1.0, ratio))
return 1.0
def consume_exit_leg(self) -> float:
ratio = self.next_exit_ratio()
self.active_leg_index = min(self.active_leg_index + 1, max(len(self.exit_leg_ratios), 1))
return ratio
def remaining_size(self) -> float:
return max(0.0, float(self.size))
def attach_entry_order(self, order: VenueOrder) -> None:
self.active_entry_order = order
def attach_exit_order(self, order: VenueOrder) -> None:
self.active_exit_order = order
def to_dict(self) -> Dict[str, Any]:
def _order_dict(order: Optional[VenueOrder]) -> Optional[Dict[str, Any]]:
if order is None:
return None
return {
"internal_trade_id": order.internal_trade_id,
"venue_order_id": order.venue_order_id,
"venue_client_id": order.venue_client_id,
"side": order.side.value,
"intended_size": float(order.intended_size or 0.0),
"filled_size": float(order.filled_size or 0.0),
"average_fill_price": float(order.average_fill_price or 0.0),
"status": order.status.value,
"metadata": dict(order.metadata),
}
return {
"slot_id": self.slot_id,
"trade_id": self.trade_id,
"asset": self.asset,
"side": self.side.value,
"entry_price": float(self.entry_price or 0.0),
"size": float(self.size or 0.0),
"initial_size": float(self.initial_size or 0.0),
"leverage": float(self.leverage or 0.0),
"entry_time": self.entry_time.isoformat() if hasattr(self.entry_time, "isoformat") else None,
"unrealized_pnl": float(self.unrealized_pnl or 0.0),
"realized_pnl": float(self.realized_pnl or 0.0),
"closed": bool(self.closed),
"exit_leg_ratios": [float(r) for r in self.exit_leg_ratios],
"active_leg_index": int(self.active_leg_index or 0),
"active_exit_order": _order_dict(self.active_exit_order),
"active_entry_order": _order_dict(self.active_entry_order),
"fsm_state": self.fsm_state.value,
"close_reason": self.close_reason,
"last_event_time": self.last_event_time.isoformat() if hasattr(self.last_event_time, "isoformat") else None,
"seen_event_ids": list(self.seen_event_ids),
"metadata": dict(self.metadata),
}
@dataclass(frozen=True)
class KernelIntent:
"""Command emitted by the algo and written to the hot-path intent region."""
timestamp: datetime
intent_id: str
trade_id: str
slot_id: int
asset: str
side: TradeSide
action: KernelCommandType
reference_price: float
target_size: float
leverage: float
exit_leg_ratios: Tuple[float, ...] = (1.0,)
reason: str = ""
metadata: Dict[str, Any] = field(default_factory=dict)
stage: TradeStage = TradeStage.INTENT_CREATED
@dataclass(frozen=True)
class VenueEvent:
"""Normalized venue truth mapped into DITAv2 semantics."""
timestamp: datetime
event_id: str
trade_id: str
slot_id: int
kind: KernelEventKind
status: VenueEventStatus
venue_order_id: str = ""
venue_client_id: str = ""
side: TradeSide = TradeSide.FLAT
asset: str = ""
price: float = 0.0
size: float = 0.0
filled_size: float = 0.0
remaining_size: float = 0.0
reason: str = ""
raw_payload: Dict[str, Any] = field(default_factory=dict)
metadata: Dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class KernelTransition:
"""Durable kernel transition used for debug journaling."""
timestamp: datetime
trade_id: str
slot_id: int
prev_state: TradeStage
next_state: TradeStage
trigger: str
intent_id: str = ""
event_id: str = ""
control_mode: str = ""
control_verbosity: str = ""
details: Dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class KernelOutcome:
"""Result of applying a command or venue event."""
accepted: bool
slot_id: int
trade_id: str
state: TradeStage
diagnostic_code: KernelDiagnosticCode = KernelDiagnosticCode.OK
severity: KernelSeverity = KernelSeverity.INFO
transitions: Tuple[KernelTransition, ...] = ()
emitted_events: Tuple[VenueEvent, ...] = ()
details: Dict[str, Any] = field(default_factory=dict)

View File

@@ -0,0 +1,217 @@
"""Runtime control plane for DITAv2."""
from __future__ import annotations
from dataclasses import asdict, dataclass, replace
from enum import Enum
import os
import threading
import time
from typing import Any, Dict, Mapping, Optional, Protocol
from .utils import json_safe
class KernelMode(str, Enum):
NORMAL = "NORMAL"
DEBUG = "DEBUG"
class KernelVerbosity(str, Enum):
QUIET = "QUIET"
VERBOSE = "VERBOSE"
TRACE = "TRACE"
class BackendMode(str, Enum):
MOCK = "MOCK"
BINGX = "BINGX"
@dataclass(frozen=True)
class KernelControlSnapshot:
"""Control plane state shared across the kernel."""
mode: KernelMode = KernelMode.NORMAL
verbosity: KernelVerbosity = KernelVerbosity.QUIET
backend_mode: BackendMode = BackendMode.MOCK
debug_clickhouse_enabled: bool = True
trace_transitions: bool = False
mirror_to_hazelcast: bool = True
active_slot_limit: int = 10
reconcile_on_restart: bool = True
runtime_namespace: str = "dita_v2"
strategy_namespace: str = "dita_v2"
event_namespace: str = "dita_v2"
actor_name: str = "ExecutionKernel"
exec_venue: str = "bingx"
data_venue: str = "binance"
ledger_authority: str = "exchange"
mock_fidelity_mode: str = "bingx_exact_shape"
def as_dict(self) -> Dict[str, Any]:
return dict(asdict(self))
@dataclass(frozen=True)
class ControlUpdate:
"""Partial update to the control plane."""
mode: Optional[KernelMode] = None
verbosity: Optional[KernelVerbosity] = None
backend_mode: Optional[BackendMode] = None
debug_clickhouse_enabled: Optional[bool] = None
trace_transitions: Optional[bool] = None
mirror_to_hazelcast: Optional[bool] = None
active_slot_limit: Optional[int] = None
reconcile_on_restart: Optional[bool] = None
runtime_namespace: Optional[str] = None
strategy_namespace: Optional[str] = None
event_namespace: Optional[str] = None
actor_name: Optional[str] = None
exec_venue: Optional[str] = None
data_venue: Optional[str] = None
ledger_authority: Optional[str] = None
mock_fidelity_mode: Optional[str] = None
def apply(self, snapshot: KernelControlSnapshot) -> KernelControlSnapshot:
payload = {
key: value
for key, value in asdict(self).items()
if value is not None
}
return replace(snapshot, **payload)
class ControlPlane(Protocol):
"""Kernel control plane interface."""
def read(self) -> KernelControlSnapshot:
...
def update(self, update: ControlUpdate) -> KernelControlSnapshot:
...
def mirror(self) -> Mapping[str, Any]:
...
def wait(self, timeout_ms: int = 1000) -> bool:
...
def notify(self) -> None:
...
class InMemoryControlPlane:
"""Local control plane used for tests and the Python prototype."""
def __init__(self, snapshot: Optional[KernelControlSnapshot] = None):
self._snapshot = snapshot or KernelControlSnapshot()
self._mirror: Dict[str, Any] = {}
self._seq = 0
self._observed_seq = 0
self._signal = threading.Condition()
def read(self) -> KernelControlSnapshot:
return self._snapshot
def update(self, update: ControlUpdate) -> KernelControlSnapshot:
with self._signal:
self._snapshot = update.apply(self._snapshot)
self._mirror = self._snapshot.as_dict()
self._seq += 1
self._signal.notify_all()
return self._snapshot
def mirror(self) -> Mapping[str, Any]:
return dict(self._mirror)
def wait(self, timeout_ms: int = 1000) -> bool:
timeout_s = None if timeout_ms is None or timeout_ms < 0 else max(0.0, timeout_ms / 1000.0)
deadline = None if timeout_s is None else time.monotonic() + timeout_s
with self._signal:
observed = self._observed_seq
while self._seq == observed:
if deadline is None:
self._signal.wait()
continue
remaining = deadline - time.monotonic()
if remaining <= 0:
return False
self._signal.wait(timeout=remaining)
self._observed_seq = self._seq
return True
def notify(self) -> None:
with self._signal:
self._seq += 1
self._signal.notify_all()
class ZincControlPlane(InMemoryControlPlane):
"""In-memory stand-in for a Zinc-backed control region.
The class keeps the interface explicit so a real Zinc binding can be
dropped in later without changing kernel code.
"""
def __init__(self, snapshot: Optional[KernelControlSnapshot] = None):
super().__init__(snapshot=snapshot)
self.region: Dict[str, Any] = self._snapshot.as_dict()
def update(self, update: ControlUpdate) -> KernelControlSnapshot:
snapshot = super().update(update)
self.region = snapshot.as_dict()
return snapshot
def read(self) -> KernelControlSnapshot:
return self._snapshot
class MirroredControlPlane:
"""Control plane that mirrors updates to an external durable sink."""
def __init__(self, inner: ControlPlane, mirror_sink: Optional[Any] = None):
self.inner = inner
self.mirror_sink = mirror_sink
def read(self) -> KernelControlSnapshot:
return self.inner.read()
def update(self, update: ControlUpdate) -> KernelControlSnapshot:
snapshot = self.inner.update(update)
if self.mirror_sink is not None:
self.mirror_sink("dita_control_plane", dict(snapshot.as_dict()))
return snapshot
def mirror(self) -> Mapping[str, Any]:
return self.inner.mirror()
def build_control_plane(
snapshot: Optional[KernelControlSnapshot] = None,
*,
prefer_real_zinc: Optional[bool] = None,
prefix: str = "dita_v2",
) -> ControlPlane:
"""Build the active control plane with an operator-visible switch.
The default remains the in-process Zinc stand-in so existing tests and
callers stay stable. Setting ``DITA_V2_CONTROL_PLANE=REAL_ZINC`` or passing
``prefer_real_zinc=True`` opts into the shared-memory control plane when
the Zinc adapter is available.
"""
env_choice = os.environ.get("DITA_V2_CONTROL_PLANE", "").strip().upper()
real_requested = prefer_real_zinc if prefer_real_zinc is not None else env_choice in {"REAL", "REAL_ZINC", "SHARED", "SHARED_MEM"}
if real_requested:
try:
from .real_control_plane import RealZincControlPlane
plane = RealZincControlPlane(prefix=prefix, create=True)
if snapshot is not None:
plane.update(ControlUpdate(**{key: value for key, value in snapshot.as_dict().items()}))
return plane
except Exception:
pass
return ZincControlPlane(snapshot=snapshot)

View File

@@ -0,0 +1,438 @@
#!/usr/bin/env python3
"""Write the complete 68-test live e2e file. Bodies receive (k, symbol, p) where p is a float."""
import ast, os
SCENARIOS = [] # (name, code_lines)
def S(name, lines):
SCENARIOS.append((name, lines))
# ---- Original 9 ----
S("simple_entry_exit", [
"tid = f's-{int(time.time()*1000)}'",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(1)",
"_si(k, E.EXIT, tid, symbol, 'SHORT', p*0.995, 0.001); await asyncio.sleep(1)",
])
S("multi_leg_exit", [
"tid = f'ml-{int(time.time()*1000)}'",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.002, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(1)",
"_si(k, E.EXIT, tid, symbol, 'SHORT', p*0.995, 0.001, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(1)",
"_si(k, E.EXIT, tid, symbol, 'SHORT', p*0.993, 0.001, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(1)",
])
S("cancel_entry_order", [
"tid = f'ce-{int(time.time()*1000)}'",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.3)",
"_si(k, E.CANCEL, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(1)",
])
S("entry_hold_exit", [
"tid = f'h-{int(time.time()*1000)}'",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(3)",
"_si(k, E.EXIT, tid, symbol, 'SHORT', p*0.995, 0.001); await asyncio.sleep(1)",
])
S("entry_exit_at_loss", [
"tid = f'l-{int(time.time()*1000)}'",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(1)",
"_si(k, E.EXIT, tid, symbol, 'SHORT', p*1.005, 0.001); await asyncio.sleep(1)",
])
S("two_sequential_cycles", [
"t1 = f'2c1-{int(time.time()*1000)}'; t2 = f'2c2-{int(time.time()*1000)}'",
"_si(k, E.ENTER, t1, symbol, 'SHORT', p, 0.001); await asyncio.sleep(1)",
"_si(k, E.EXIT, t1, symbol, 'SHORT', p*0.995, 0.001); await asyncio.sleep(1)",
"_si(k, E.ENTER, t2, symbol, 'SHORT', p*0.995, 0.001); await asyncio.sleep(1)",
"_si(k, E.EXIT, t2, symbol, 'SHORT', p*0.99, 0.001); await asyncio.sleep(1)",
])
S("entry_then_recover", [
"tid = f'r-{int(time.time()*1000)}'",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(1)",
"await bundle.runtime.disconnect()",
"await bundle.runtime.connect(initial_capital=k.account.snapshot.capital)",
"await asyncio.sleep(1)",
])
S("long_entry_exit", [
"tid = f'ln-{int(time.time()*1000)}'",
"_si(k, E.ENTER, tid, symbol, 'LONG', p, 0.001); await asyncio.sleep(1)",
"_si(k, E.EXIT, tid, symbol, 'LONG', p*1.005, 0.001); await asyncio.sleep(1)",
])
# ---- Cancel combos ----
S("cancel_idempotent", [
"tid = f'ci-{int(time.time()*1000)}'",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.5)",
"_si(k, E.CANCEL, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.3)",
"_si(k, E.CANCEL, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(1)",
])
S("double_cancel", [
"tid = f'dc-{int(time.time()*1000)}'",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.3)",
"_si(k, E.CANCEL, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.3)",
"_si(k, E.CANCEL, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(1)",
])
S("cancel_then_exit", [
"tid = f'ctx-{int(time.time()*1000)}'",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.5)",
"_si(k, E.CANCEL, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.3)",
"if not k.slot(0).is_free():",
" _si(k, E.EXIT, tid, symbol, 'SHORT', p*0.995, 0.001); await asyncio.sleep(1)",
])
S("exit_then_cancel_exit", [
"tid = f'exc-{int(time.time()*1000)}'",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(1)",
"_si(k, E.EXIT, tid, symbol, 'SHORT', p*0.995, 0.001); await asyncio.sleep(0.3)",
"_si(k, E.CANCEL, tid, symbol, 'SHORT', p*0.995, 0.001); await asyncio.sleep(1)",
])
S("exit_then_reentry", [
"t1 = f'er1-{int(time.time()*1000)}'; t2 = f'er2-{int(time.time()*1000)}'",
"_si(k, E.ENTER, t1, symbol, 'SHORT', p, 0.001); await asyncio.sleep(1)",
"_si(k, E.EXIT, t1, symbol, 'SHORT', p*0.995, 0.001); await asyncio.sleep(0.3)",
"_si(k, E.ENTER, t2, symbol, 'SHORT', p*0.995, 0.001); await asyncio.sleep(1)",
])
S("limit_cancel", [
"tid = f'lc-{int(time.time()*1000)}'",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p*0.9, 0.001); await asyncio.sleep(0.5)",
"_si(k, E.CANCEL, tid, symbol, 'SHORT', p*0.9, 0.001); await asyncio.sleep(1)",
])
# ---- X4 ----
S("x4_partial_hold_exit", [
"tid = f'ph-{int(time.time()*1000)}'; sz = 0.003",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, sz, exit_leg_ratios=(0.3,1.0)); await asyncio.sleep(1)",
"_si(k, E.EXIT, tid, symbol, 'SHORT', p*0.995, sz*0.3, exit_leg_ratios=(0.3,1.0)); await asyncio.sleep(1)",
"_si(k, E.EXIT, tid, symbol, 'SHORT', p*0.993, sz*0.7, exit_leg_ratios=(0.3,1.0)); await asyncio.sleep(1)",
])
S("x4_three_leg", [
"tid = f'3l-{int(time.time()*1000)}'; sz = 0.004",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, sz, exit_leg_ratios=(0.25,0.25,1.0)); await asyncio.sleep(1)",
"_si(k, E.EXIT, tid, symbol, 'SHORT', p*0.995, sz*0.25, exit_leg_ratios=(0.25,0.25,1.0)); await asyncio.sleep(1)",
"_si(k, E.EXIT, tid, symbol, 'SHORT', p*0.993, sz*0.25, exit_leg_ratios=(0.25,0.25,1.0)); await asyncio.sleep(1)",
"_si(k, E.EXIT, tid, symbol, 'SHORT', p*0.99, sz*0.5, exit_leg_ratios=(0.25,0.25,1.0)); await asyncio.sleep(1)",
])
S("x4_cancel_fill_partial", [
"tid = f'cfp-{int(time.time()*1000)}'",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.002); await asyncio.sleep(0.5)",
"_si(k, E.CANCEL, tid, symbol, 'SHORT', p, 0.002); await asyncio.sleep(0.3)",
"if not k.slot(0).is_free():",
" _si(k, E.EXIT, tid, symbol, 'SHORT', p*0.995, 0.001); await asyncio.sleep(0.5)",
"if not k.slot(0).is_free():",
" _si(k, E.EXIT, tid, symbol, 'SHORT', p*0.993, 0.001); await asyncio.sleep(1)",
])
S("x4_rapid_three", [
"for i in range(3):",
" tid = f'r3-{i}-{int(time.time()*1000)}'",
" _si(k, E.ENTER, tid, symbol, 'SHORT', p*(1-i*0.005), 0.001); await asyncio.sleep(0.8)",
" _si(k, E.EXIT, tid, symbol, 'SHORT', p*0.995*(1-i*0.005), 0.001); await asyncio.sleep(0.8)",
])
S("x4_diff_symbol", [
"tid = f'ds-{int(time.time()*1000)}'",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(1)",
"sym2 = 'BTCUSDT' if symbol != 'BTCUSDT' else 'ETHUSDT'",
"_si(k, E.EXIT, tid, sym2, 'SHORT', p, 0.001); await asyncio.sleep(0.5)",
])
S("x4_alternating", [
"t1 = f'as1-{int(time.time()*1000)}'; t2 = f'as2-{int(time.time()*1000)}'",
"_si(k, E.ENTER, t1, symbol, 'SHORT', p, 0.001); await asyncio.sleep(1)",
"sym2 = 'BTCUSDT' if symbol != 'BTCUSDT' else 'ETHUSDT'",
"try:",
" p2 = float(json.loads(urllib.request.urlopen('https://open-api-vst.bingx.com/openApi/swap/v2/quote/price?symbol='+sym2.replace('USDT','-USDT'), timeout=5).read())['data']['price'])",
"except: p2 = p",
"_si(k, E.ENTER, t2, sym2, 'LONG', p2, 0.001); await asyncio.sleep(1)",
"_si(k, E.EXIT, t1, symbol, 'SHORT', p*0.995, 0.001); await asyncio.sleep(1)",
"_si(k, E.EXIT, t2, sym2, 'LONG', p2*1.005, 0.001); await asyncio.sleep(1)",
])
S("x4_multi_flatten", [
"tid = f'mf-{int(time.time()*1000)}'",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(1)",
"for i in range(3):",
" if k.slot(0).is_free(): break",
" _flatten(k, symbol, p*0.99, f'mf{i}'); await asyncio.sleep(0.5)",
])
S("x4_three_leg_25_50_25", [
"tid = f'x4a-{int(time.time()*1000)}'; sz = 0.004",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, sz, exit_leg_ratios=(0.25,0.5,1.0)); await asyncio.sleep(1)",
"_si(k, E.EXIT, tid, symbol, 'SHORT', p*0.995, sz*0.25, exit_leg_ratios=(0.25,0.5,1.0)); await asyncio.sleep(1)",
"_si(k, E.EXIT, tid, symbol, 'SHORT', p*0.993, sz*0.5, exit_leg_ratios=(0.25,0.5,1.0)); await asyncio.sleep(1)",
"_si(k, E.EXIT, tid, symbol, 'SHORT', p*0.99, sz*0.25, exit_leg_ratios=(0.25,0.5,1.0)); await asyncio.sleep(1)",
])
S("x4_enter_exit_hold_twice", [
"t1 = f'x4b1-{int(time.time()*1000)}'",
"_si(k, E.ENTER, t1, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.5)",
"_si(k, E.EXIT, t1, symbol, 'SHORT', p*0.995, 0.001); await asyncio.sleep(0.5)",
"t2 = f'x4b2-{int(time.time()*1000)}'",
"_si(k, E.ENTER, t2, symbol, 'SHORT', p*0.995, 0.001); await asyncio.sleep(0.5)",
"_si(k, E.EXIT, t2, symbol, 'SHORT', p*0.99, 0.001); await asyncio.sleep(0.5)",
"t3 = f'x4b3-{int(time.time()*1000)}'",
"_si(k, E.ENTER, t3, symbol, 'SHORT', p*0.99, 0.001); await asyncio.sleep(0.5)",
"_si(k, E.EXIT, t3, symbol, 'SHORT', p*0.985, 0.001); await asyncio.sleep(0.5)",
])
S("x4_cancel_then_double_exit", [
"tid = f'x4c-{int(time.time()*1000)}'; sz = 0.002",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, sz, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(0.5)",
"_si(k, E.CANCEL, tid, symbol, 'SHORT', p, sz); await asyncio.sleep(0.3)",
"if not k.slot(0).is_free():",
" _si(k, E.EXIT, tid, symbol, 'SHORT', p*0.995, sz*0.5, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(0.5)",
"if not k.slot(0).is_free():",
" _si(k, E.EXIT, tid, symbol, 'SHORT', p*0.993, sz*0.5, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(0.5)",
])
# ---- 2 sides x 2 profit x 4 patterns = 16 doubled ----
for side, side_str, ep in [("short","SHORT",0.995), ("long","LONG",1.005)]:
for prof, pname, xp in [(True,"profit",ep), (False,"loss",1/ep)]:
for pat, pat_suffix, lines in [
("basic", "", [
f"_si(k, E.ENTER, tid, symbol, '{side_str}', p, 0.001); await asyncio.sleep(0.8)",
f"_si(k, E.EXIT, tid, symbol, '{side_str}', p*{xp}, 0.001); await asyncio.sleep(0.8)",
]),
("partial", "_partial", [
"sz = 0.002",
f"_si(k, E.ENTER, tid, symbol, '{side_str}', p, sz, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(0.8)",
f"_si(k, E.EXIT, tid, symbol, '{side_str}', p*{ep}, sz*0.5, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(0.8)",
f"_si(k, E.EXIT, tid, symbol, '{side_str}', p*{xp}, sz*0.5, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(0.8)",
]),
("cancel", "_cancel", [
f"_si(k, E.ENTER, tid, symbol, '{side_str}', p, 0.001); await asyncio.sleep(0.3)",
f"_si(k, E.CANCEL, tid, symbol, '{side_str}', p, 0.001); await asyncio.sleep(0.3)",
"if not k.slot(0).is_free():",
f" _si(k, E.EXIT, tid, symbol, '{side_str}', p*{xp}, 0.001); await asyncio.sleep(0.8)",
]),
("double_exit", "_double_exit", [
f"_si(k, E.ENTER, tid, symbol, '{side_str}', p, 0.001); await asyncio.sleep(0.8)",
f"_si(k, E.EXIT, tid, symbol, '{side_str}', p*{xp}, 0.001); await asyncio.sleep(0.3)",
"if not k.slot(0).is_free():",
f" _si(k, E.EXIT, tid, symbol, '{side_str}', p*{xp}*0.995, 0.001); await asyncio.sleep(0.5)",
]),
]:
pfx = f"{pat[0]}{side[0]}{chr(112) if prof else chr(108)}"
S(f"{pat}_{side}_{pname}", [
f"tid = f'{pfx}-{{{{int(time.time()*1000)}}}}'",
*lines,
])
# ---- Triple seq x 4 SHORT + 4 LONG ----
for i in range(4):
S(f"triple_seq_{i}", [
"for j in range(3):",
f" tid = f'ts{i}-j-{{{{int(time.time()*1000)}}}}'",
" _si(k, E.ENTER, tid, symbol, 'SHORT', p*(1-j*0.003), 0.001); await asyncio.sleep(0.7)",
" _si(k, E.EXIT, tid, symbol, 'SHORT', p*0.995*(1-j*0.003), 0.001); await asyncio.sleep(0.7)",
])
for i in range(4):
S(f"triple_seq_long_{i}", [
"for j in range(3):",
f" tid = f'tsl{i}-j-{{{{int(time.time()*1000)}}}}'",
" _si(k, E.ENTER, tid, symbol, 'LONG', p*(1+j*0.003), 0.001); await asyncio.sleep(0.7)",
" _si(k, E.EXIT, tid, symbol, 'LONG', p*1.005*(1+j*0.003), 0.001); await asyncio.sleep(0.7)",
])
# ---- Cancel+reenter x 4 SHORT + 4 LONG ----
for i in range(4):
S(f"cancel_reenter_{i}", [
f"t1 = f'cr{i}a-{{{{int(time.time()*1000)}}}}'; t2 = f'cr{i}b-{{{{int(time.time()*1000)}}}}'",
"_si(k, E.ENTER, t1, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.3)",
"_si(k, E.CANCEL, t1, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.3)",
"_si(k, E.ENTER, t2, symbol, 'SHORT', p*0.995, 0.001); await asyncio.sleep(0.8)",
"if not k.slot(0).is_free():",
" _si(k, E.EXIT, t2, symbol, 'SHORT', p*0.99, 0.001); await asyncio.sleep(0.5)",
])
for i in range(4):
S(f"cancel_reenter_long_{i}", [
f"t1 = f'crl{i}a-{{{{int(time.time()*1000)}}}}'; t2 = f'crl{i}b-{{{{int(time.time()*1000)}}}}'",
"_si(k, E.ENTER, t1, symbol, 'LONG', p, 0.001); await asyncio.sleep(0.3)",
"_si(k, E.CANCEL, t1, symbol, 'LONG', p, 0.001); await asyncio.sleep(0.3)",
"_si(k, E.ENTER, t2, symbol, 'LONG', p*1.005, 0.001); await asyncio.sleep(0.8)",
"if not k.slot(0).is_free():",
" _si(k, E.EXIT, t2, symbol, 'LONG', p*1.01, 0.001); await asyncio.sleep(0.5)",
])
# ---- Leg ratios x 8 ----
for i, ratios in enumerate([
(0.1,1.0), (0.33,0.33,1.0), (0.5,0.5,1.0), (0.75,1.0),
(0.2,0.3,0.5,1.0), (0.4,0.6,1.0), (0.15,0.85,1.0), (0.25,0.25,0.5,1.0),
]):
rat_str = ",".join(str(r) for r in ratios)
code = [f"tid = f'lr{i}-{{{{int(time.time()*1000)}}}}'; sz = 0.004",
f"_si(k, E.ENTER, tid, symbol, 'SHORT', p, sz, exit_leg_ratios=({rat_str})); await asyncio.sleep(1)"]
for leg in range(len(ratios) - 1):
r = ratios[leg]
code.append(f"_si(k, E.EXIT, tid, symbol, 'SHORT', p*0.995*(1-{leg}*0.002), sz*{r}, exit_leg_ratios=({rat_str})); await asyncio.sleep(0.8)")
code.append(f"_si(k, E.EXIT, tid, symbol, 'SHORT', p*0.99, sz*{ratios[-1]}, exit_leg_ratios=({rat_str})); await asyncio.sleep(0.8)")
S(f"leg_ratio_{i}", code)
# ---- Breakeven x 4 ----
for i in range(4):
S(f"breakeven_{i}", [
f"tid = f'be{i}-{{{{int(time.time()*1000)}}}}'",
"_si(k, E.ENTER, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.8)",
"_si(k, E.EXIT, tid, symbol, 'SHORT', p, 0.001); await asyncio.sleep(0.8)",
])
# =====================================================================
# Assemble
# =====================================================================
HEADER = '''#!/usr/bin/env python3
"""PINK DITAv2 Live BingX Testnet E2E — 68 combinatorial scenarios.
Kernel-direct tests: bodies receive (k, symbol, p). Capital integrity
asserted. Exchange state confirmed flat.
"""
from __future__ import annotations
import asyncio, json, os, socket, time, urllib.request
import urllib.parse
from dataclasses import dataclass
from typing import Any, Optional
import pytest
from prod.bingx.http import BingxHttpClient
from prod.bingx.config import BingxExecClientConfig, BingxEnvironment
from prod.clean_arch.dita_v2.launcher import build_launcher_bundle
from prod.clean_arch.dita_v2.contracts import (
KernelCommandType as KC, KernelIntent as KI, TradeSide as TS,
)
from prod.clean_arch.ports.data_feed import MarketSnapshot
E = KC
# Force IPv4 for httpx (IPv6 resolution fails in this env)
_orig_gai = socket.getaddrinfo
def _ipv4_gai(host, port, family=0, type=0, proto=0, flags=0):
return _orig_gai(host, port, socket.AF_INET, type, proto, flags)
socket.getaddrinfo = _ipv4_gai
# ---- env gates ----
if not os.environ.get("BINGX_SMOKE_LIVE"):
pytest.skip("BINGX_SMOKE_LIVE not set", allow_module_level=True)
if not os.environ.get("BINGX_SMOKE_ALLOW_TRADE"):
pytest.skip("BINGX_SMOKE_ALLOW_TRADE not set", allow_module_level=True)
if not os.environ.get("PINK_DITA_E2E"):
pytest.skip("PINK_DITA_E2E not set", allow_module_level=True)
# ---- helpers ----
@dataclass
class VR:
symbol: str; positions_flat: bool = True; error: str = ""
@dataclass
class RB:
runtime: Any; config: Any
def _build_config(ic: float = 25000.0) -> BingxExecClientConfig:
return BingxExecClientConfig(
api_key=os.environ["BINGX_API_KEY"], secret_key=os.environ["BINGX_SECRET_KEY"],
environment=BingxEnvironment.VST, allow_mainnet=False, recv_window_ms=5000,
default_leverage=1, exchange_leverage_cap=3, prefer_websocket=False,
use_reduce_only=True, sizing_mode="testnet", journal_strategy="pink",
journal_db="dolphin_pink")
def _build_rb(ic: float = 25000.0) -> RB:
cfg = _build_config(ic)
b = build_launcher_bundle(venue_mode="BINGX", max_slots=1, bingx_config=cfg)
k = b.kernel; k.account.snapshot.capital = ic; k.account.snapshot.peak_capital = ic; k.account.snapshot.equity = ic
class Shim:
def __init__(self, k): self.kernel = k
async def connect(self, initial_capital=0): self.kernel.venue.connect()
async def disconnect(self):
try: self.kernel.venue.disconnect()
except: pass
return RB(runtime=Shim(k), config=cfg)
async def _contract_rows(c):
r = await c._request_json("GET", "/openApi/swap/v2/user/positions", {}, signed=True)
return r if isinstance(r, list) else (r.get("data") or r.get("positions") or [])
async def _pick_sym(k, c):
rs = await _contract_rows(c)
oss = {str(r.get("symbol","")).replace("-","").upper() for r in rs}
sym = next((x for x in ["TRXUSDT","XRPUSDT","ADAUSDT","DOGEUSDT"] if x not in oss), "TRXUSDT")
return sym
async def _snap(c, sym):
vs = sym[:3]+"-USDT"
pr = await c._request_json("GET", "/openApi/swap/v2/quote/price", {"symbol": vs}, signed=False)
d = pr.get("data") or pr; rp = float(d.get("price") or d.get("lastPrice") or 0)
return MarketSnapshot(timestamp=__import__("datetime").datetime.now(__import__("datetime").timezone.utc),
symbol=sym, price=rp, bid=rp*0.9995, ask=rp*1.0005), vs
async def _verify(c, vs):
rs = await _contract_rows(c)
tr = [r for r in rs if str(r.get("symbol","")).upper().replace("-","") == vs.replace("-","").upper()]
ts = sum(abs(float(r.get("positionAmt",r.get("positionQty",0)) or 0)) for r in tr)
flat = ts < 1e-8
return VR(symbol=vs, positions_flat=flat, error="" if flat else f"open: {tr}")
def _si(k, act, tid, asset, side_str, price, size, **kw):
ds = TS.SHORT if side_str.upper() == "SHORT" else TS.LONG
return k.process_intent(KI(
timestamp=__import__("datetime").datetime.now(__import__("datetime").timezone.utc),
intent_id=tid, trade_id=tid, slot_id=0, asset=asset, side=ds, action=act,
reference_price=price, target_size=size, leverage=kw.pop("leverage",1.0),
exit_leg_ratios=kw.pop("exit_leg_ratios",(1.0,)),
reason=kw.pop("reason",f"auto_{act.value.lower()}"), metadata=kw))
def _flatten(k, sym, price, label):
if k.slot(0).is_free(): return
_si(k, E.EXIT, f"fl{label}-{int(time.time()*1000)}", sym, "SHORT", price, 0.001)
async def _run(bundle, client, body_fn, label, ic):
k = bundle.runtime.kernel
sym = await _pick_sym(k, client)
snap, vsym = await _snap(client, sym)
await bundle.runtime.connect(initial_capital=ic)
p = float(snap.price)
try:
_flatten(k, sym, p, f"{label}-pre")
await asyncio.sleep(0.3)
cb = k.account.snapshot.capital
await body_fn(k, sym, p)
ca = k.account.snapshot.capital
assert ca > 0, f"Capital zero: {ca}"
assert ca < cb * 10, f"Capital bounds: {cb} -> {ca}"
if not k.slot(0).is_free():
_flatten(k, sym, p*0.99, f"{label}-post")
await asyncio.sleep(1.0)
return await _verify(client, vsym)
finally:
await bundle.runtime.disconnect()
'''
lines = [HEADER]
# Scenario bodies
lines.append("\n# =====================================================================\n# Scenario bodies\n# =====================================================================\n")
for name, code_lines in SCENARIOS:
lines.append(f"async def _body_{name}(k, symbol, p):")
for cl in code_lines:
lines.append(f" {cl}")
lines.append("")
# Test functions
lines.append("\n# =====================================================================\n# Test functions\n# =====================================================================\n")
lines.append('''@pytest.fixture(scope="session")
def _live_client():
return BingxHttpClient(_build_config())
''')
for name, _ in SCENARIOS:
lines.append(f'''
def test_pink_ditav2_{name}(_live_client) -> None:
bundle = _build_rb()
ic = bundle.runtime.kernel.account.snapshot.capital
r = asyncio.run(_run(bundle, _live_client, _body_{name}, "{name}", ic))
assert r.positions_flat, name + ": " + r.error
''')
full = '\n'.join(lines)
try:
ast.parse(full)
count = full.count("def test_pink_ditav2_")
print(f"Syntax OK — {count} tests, {len(full)} chars")
out_path = os.path.join('/mnt/dolphinng5_predict', 'prod/tests/test_pink_bingx_dita_live_e2e.py')
with open(out_path, 'w') as f:
f.write(full)
print(f"Written OK ({count} tests)")
except SyntaxError as e:
print(f"Syntax error L{e.lineno}: {e.msg}")
fl = full.split('\n')
for i in range(max(0,e.lineno-5), min(len(fl), e.lineno+3)):
print(f" {i+1}: {fl[i]}")

View File

@@ -0,0 +1,688 @@
#!/usr/bin/env python3
"""Regenerate the complete PINK DITAv2 live BingX e2e test file from scratch."""
import ast, os
BASE = '/mnt/dolphinng5_predict'
OUT = os.path.join(BASE, 'prod/tests/test_pink_bingx_dita_live_e2e.py')
# =====================================================================
# Static prologue — imports, helpers, env check
# =====================================================================
PROLOGUE = r'''#!/usr/bin/env python3
"""PINK DITAv2 Live BingX Testnet E2E — combinatorial scenarios.
Each test:
1. Picks a live VST symbol with price
2. Submits KernelIntent directly (bypasses DecisionEngine)
3. Asserts capital integrity (positive, within bounds)
4. Confirms exchange state is flat after exit
"""
from __future__ import annotations
import asyncio
import json
import os
import time
import urllib.parse
import urllib.request
from dataclasses import dataclass, field
from decimal import Decimal
from typing import Any, Optional
import pytest
import requests
from prod.bingx.http import BingxHttpClient
from prod.bingx.config import BingxExecClientConfig, BingxEnvironment
from prod.bingx.schemas import BingxContract
from prod.clean_arch.dita_v2.launcher import build_launcher_bundle
from prod.clean_arch.dita_v2.contracts import (
KernelCommandType,
KernelDiagnosticCode,
KernelIntent,
KernelOutcome,
TradeSide,
)
from prod.clean_arch.ports.data_feed import MarketSnapshot
from prod.clean_arch.dita import DecisionConfig, DecisionEngine, IntentEngine
from prod.clean_arch.runtime.pink_direct import PinkDirectRuntime
from prod.clean_arch.projection import build_projection
from prod.clean_arch.adapters.hazelcast_feed import HazelcastDataFeed
# ---- env gates ----
if not os.environ.get("BINGX_SMOKE_LIVE"):
pytest.skip("BINGX_SMOKE_LIVE not set — skipping live tests", allow_module_level=True)
if not os.environ.get("BINGX_SMOKE_ALLOW_TRADE"):
pytest.skip("BINGX_SMOKE_ALLOW_TRADE not set — skipping live trade tests", allow_module_level=True)
if not os.environ.get("PINK_DITA_E2E"):
pytest.skip("PINK_DITA_E2E not set — skipping PINK DITAv2 e2e tests", allow_module_level=True)
_INTER_TEST_DELAY_S = 3.0
def _wait_for_quota() -> None:
"""Block until the exchange rate-limit quota allows a burst."""
time.sleep(_INTER_TEST_DELAY_S)
def _normalize(symbol: str) -> str:
return symbol.replace("-", "").upper()
async def _contract_rows(client: BingxHttpClient) -> list[dict]:
url = "https://open-api-vst.bingx.com/openApi/swap/v2/user/positions"
rows = await client._request_json("GET", url, {}, signed=True)
data = rows if isinstance(rows, list) else (rows.get("data") or rows.get("positions") or [])
return data
async def _build_live_snapshot(client: BingxHttpClient, vsymbol: str) -> MarketSnapshot:
vsym_dash = vsymbol.replace("USDT", "-USDT")
price_resp = await client._request_json("GET", "https://open-api-vst.bingx.com/openApi/swap/v2/quote/price", {"symbol": vsym_dash}, signed=False)
d = price_resp.get("data") or price_resp
raw_price = d.get("price") or d.get("lastPrice") or 0
price = Decimal(str(raw_price))
return MarketSnapshot(
timestamp=time.time(), price=price, bid=price * Decimal("0.9995"),
ask=price * Decimal("1.0005"), volume=Decimal("0"),
)
@dataclass
class _VerificationResult:
symbol: str
positions_flat: bool = True
error: str = ""
async def _query_exchange_positions(client: BingxHttpClient, venue_symbol: str) -> list[dict]:
"""Fetch live positions from BingX and return rows for venue_symbol."""
rows = _contract_rows(client)
return [r for r in rows if str(r.get("symbol", "")).upper().replace("-", "") == venue_symbol.replace("-", "").upper()]
async def _verify_exchange_state(
client: BingxHttpClient, venue_symbol: str, expect_open: bool = False,
) -> _VerificationResult:
pos_rows = await _query_exchange_positions(client, venue_symbol)
total_size = sum(abs(float(r.get("positionAmt", r.get("positionQty", 0)) or 0)) for r in pos_rows)
flat = total_size < 1e-8
if expect_open and flat:
return _VerificationResult(symbol=venue_symbol, positions_flat=False, error="expected open position but flat")
if not expect_open and not flat:
return _VerificationResult(symbol=venue_symbol, positions_flat=False, error=f"expected flat but open: {pos_rows}")
return _VerificationResult(symbol=venue_symbol, positions_flat=True)
@dataclass
class _RuntimeBundle:
runtime: PinkDirectRuntime
config: BingxExecClientConfig
def _build_bingx_config(initial_capital: float) -> BingxExecClientConfig:
return BingxExecClientConfig(
api_key=os.environ["BINGX_API_KEY"],
secret_key=os.environ["BINGX_SECRET_KEY"],
environment=BingxEnvironment.VST,
allow_mainnet=False,
recv_window_ms=5000,
default_leverage=1,
exchange_leverage_cap=3,
prefer_websocket=False,
use_reduce_only=True,
sizing_mode="testnet",
journal_strategy="pink",
journal_db="dolphin_pink",
)
def _build_runtime_bundle(initial_capital: float) -> _RuntimeBundle:
"""Build a direct kernel bundle."""
cfg = _build_bingx_config(initial_capital)
bundle = build_launcher_bundle(venue_mode="BINGX", max_slots=1, bingx_config=cfg)
k = bundle.kernel
k.account.snapshot.capital = initial_capital
k.account.snapshot.peak_capital = initial_capital
k.account.snapshot.equity = initial_capital
return _RuntimeBundle(runtime=_RuntimeShim(kernel=k), config=cfg)
class _RuntimeShim:
"""Minimal runtime wrapper — exposes .kernel + sync connect/disconnect."""
def __init__(self, kernel): self.kernel = kernel
async def connect(self, initial_capital=0): self.kernel.venue.connect()
async def disconnect(self):
try: self.kernel.venue.disconnect()
except Exception: pass
def _build_full_runtime(initial_capital: float) -> PinkDirectRuntime:
"""Build a fully wired PinkDirectRuntime (data feed, engine, persistence)."""
cfg = _build_bingx_config(initial_capital)
bundle = build_launcher_bundle(venue_mode="BINGX", max_slots=1, bingx_config=cfg)
feed = HazelcastDataFeed(
prefix="dita_v2",
hz_client=build_projection(prefer_real_hazelcast=False),
)
engine = DecisionEngine(DecisionConfig(initial_capital=initial_capital))
intent_engine = IntentEngine(initial_capital=initial_capital)
rt = PinkDirectRuntime(
data_feed=feed, kernel=bundle.kernel,
decision_engine=engine, intent_engine=intent_engine,
)
rt.kernel.account.snapshot.capital = initial_capital
rt.kernel.account.snapshot.peak_capital = initial_capital
rt.kernel.account.snapshot.equity = initial_capital
return rt
async def _pick_live_symbol(
kernel: Any, client: BingxHttpClient,
) -> tuple[str, MarketSnapshot, str]:
"""Pick a live VST symbol that isn't already in a position."""
pos_rows = _contract_rows(client)
open_syms = set()
for r in pos_rows:
sym = str(r.get("symbol", "")).replace("-", "").upper()
if sym:
open_syms.add(sym)
candidates = ["TRXUSDT", "XRPUSDT", "ADAUSDT", "DOGEUSDT"]
preferred = [c for c in candidates if c not in open_syms]
sym = preferred[0] if preferred else candidates[0]
vsym = sym[:3] + "-USDT" if sym.endswith("USDT") and len(sym) > 6 else sym[:3] + "-USDT"
snap = _build_live_snapshot(client, vsym)
return sym, snap, vsym
def _submit_intent_direct(
kernel: Any,
action: KernelCommandType,
trade_id: str,
asset: str,
side_str: str,
price: float,
size: float,
**kw,
) -> KernelOutcome:
ds = TradeSide.SHORT if side_str.upper() == "SHORT" else TradeSide.LONG
intent = KernelIntent(
timestamp=__import__("datetime").datetime.now(__import__("datetime").timezone.utc),
intent_id=trade_id,
trade_id=trade_id,
slot_id=0,
asset=asset,
side=ds,
action=action,
reference_price=price,
target_size=size,
leverage=kw.pop("leverage", 1.0),
exit_leg_ratios=kw.pop("exit_leg_ratios", (1.0,)),
reason=kw.pop("reason", f"auto_{action.value.lower()}"),
metadata=kw,
)
return kernel.process_intent(intent)
def _flatten_via_kernel_intent(kernel: Any, symbol: str, price: float, label: str) -> None:
"""Flatten slot 0 by submitting an EXIT intent at the given price.
No-op if already flat."""
if kernel.slot(0).is_free():
return
tid = f"flat-{label}-{int(time.time() * 1000)}"
side = TradeSide.SHORT
intent = KernelIntent(
timestamp=__import__("datetime").datetime.now(__import__("datetime").timezone.utc),
intent_id=tid,
trade_id=tid,
slot_id=0,
asset=symbol,
side=side,
action=KernelCommandType.EXIT,
reference_price=price,
target_size=0.001,
leverage=1.0,
exit_leg_ratios=(1.0,),
reason=f"flatten_{label}",
)
kernel.process_intent(intent)
async def _flatten_live_position(client: BingxHttpClient, symbol: str) -> None:
"""Emergency raw flatten via REST if kernel can't."""
pass
async def _run_pink_live_roundtrip(
bundle: _RuntimeBundle, client: BingxHttpClient,
) -> tuple[KernelOutcome, Optional[KernelOutcome], Optional[KernelOutcome]]:
"""Original roundtrip test entry → partial/monitor → flatten."""
kernel = bundle.runtime.kernel
symbol, snap, vsym = await _pick_live_symbol(kernel, client)
price = float(snap.price)
await bundle.runtime.connect(initial_capital=25000.0)
try:
_flatten_via_kernel_intent(kernel, symbol, price, "roundtrip-pre")
await asyncio.sleep(0.3)
tid = f"rt-{int(time.time() * 1000)}"
entry = _submit_intent_direct(kernel, KernelCommandType.ENTER, tid, symbol, "SHORT", price, 0.001)
await asyncio.sleep(1.0)
monitor = None
if not kernel.slot(0).is_free():
_submit_intent_direct(kernel, KernelCommandType.CANCEL, tid, symbol, "SHORT", price, 0.001)
await asyncio.sleep(0.3)
flatt = None
if not kernel.slot(0).is_free():
flatt = _submit_intent_direct(kernel, KernelCommandType.EXIT, tid, symbol, "SHORT", price * 0.995, 0.001)
await asyncio.sleep(1.0)
if not kernel.slot(0).is_free():
_flatten_via_kernel_intent(kernel, symbol, price * 0.99, "roundtrip-post")
await asyncio.sleep(1.0)
return entry, monitor, flatt
finally:
await bundle.runtime.disconnect()
async def _run_pink_live_recovery(
bundle: _RuntimeBundle, client: BingxHttpClient,
) -> dict:
"""Recovery test: enter, disconnect, reconnect, verify capital preserved."""
kernel = bundle.runtime.kernel
symbol, snap, vsym = await _pick_live_symbol(kernel, client)
price = float(snap.price)
await bundle.runtime.connect(initial_capital=25000.0)
try:
_flatten_via_kernel_intent(kernel, symbol, price, "recovery-pre")
await asyncio.sleep(0.3)
_submit_intent_direct(kernel, KernelCommandType.ENTER, tid := f"r-{int(time.time() * 1000)}", symbol, "SHORT", price, 0.001)
await asyncio.sleep(1.0)
await bundle.runtime.disconnect()
await bundle.runtime.connect(initial_capital=25000.0)
await asyncio.sleep(1.0)
if not kernel.slot(0).is_free():
_flatten_via_kernel_intent(kernel, symbol, price * 0.99, "recovery-post")
await asyncio.sleep(1.0)
return {"capital": kernel.account.snapshot.capital, "peak": kernel.account.snapshot.peak_capital}
finally:
await bundle.runtime.disconnect()
''' # end PROLOGUE
# =====================================================================
# Scenario runner + shortcut
# =====================================================================
RUNNER = '''
# =====================================================================
# Generic runner & shortcut
# =====================================================================
async def _run_scenario(bundle, client, body_fn, label, initial_capital):
k = bundle.runtime.kernel
symbol, snap, vsym = await _pick_live_symbol(k, client)
await bundle.runtime.connect(initial_capital=initial_capital)
try:
_flatten_via_kernel_intent(k, symbol, float(snap.price), f"{label}-pre")
await asyncio.sleep(0.3)
_cap_before = k.account.snapshot.capital
await body_fn(bundle, client, symbol, snap)
_cap_after = k.account.snapshot.capital
assert _cap_after > 0, f"Capital went to zero: {_cap_after}"
assert _cap_after < _cap_before * 10, f"Capital growth beyond bounds: {_cap_before} -> {_cap_after}"
if not k.slot(0).is_free():
_flatten_via_kernel_intent(k, symbol, float(snap.price) * 0.99, f"{label}-post")
await asyncio.sleep(1.0)
return await _verify_exchange_state(client, vsym, expect_open=False)
finally:
await bundle.runtime.disconnect()
def _si(kernel, action, trade_id, asset, side_str, price, size, **kw):
ds = TradeSide.SHORT if side_str.upper() == "SHORT" else TradeSide.LONG
return kernel.process_intent(KernelIntent(
timestamp=__import__("datetime").datetime.now(__import__("datetime").timezone.utc),
intent_id=trade_id, trade_id=trade_id, slot_id=0, asset=asset,
side=ds, action=action, reference_price=price, target_size=size,
leverage=kw.pop("leverage", 1.0),
exit_leg_ratios=kw.pop("exit_leg_ratios", (1.0,)),
reason=kw.pop("reason", f"auto_{action.value.lower()}"),
metadata=kw,
))
'''
# =====================================================================
# Build scenario bodies + tests
# =====================================================================
scenarios = [] # (name, code_lines)
def S(name, code_lines):
scenarios.append((name, list(code_lines)))
# --- Original 9 ---
S("simple_entry_exit", [
'tid = f"s-{int(time.time()*1000)}"; p = float(snap.price)',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.995, 0.001); await asyncio.sleep(1)',
])
S("multi_leg_exit", [
'tid = f"ml-{int(time.time()*1000)}"; p = float(snap.price)',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, 0.002, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.995, 0.001, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.993, 0.001, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(1)',
])
S("cancel_entry_order", [
'tid = f"ce-{int(time.time()*1000)}"; p = float(snap.price)',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(0.3)',
'_si(k, KernelCommandType.CANCEL, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(1)',
])
S("entry_hold_exit", [
'tid = f"h-{int(time.time()*1000)}"; p = float(snap.price)',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(3)',
'_si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.995, 0.001); await asyncio.sleep(1)',
])
S("entry_exit_at_loss", [
'tid = f"l-{int(time.time()*1000)}"; p = float(snap.price)',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*1.005, 0.001); await asyncio.sleep(1)',
])
S("two_sequential_cycles", [
'p = float(snap.price)',
't1 = f"2c1-{int(time.time()*1000)}"; t2 = f"2c2-{int(time.time()*1000)}"',
'_si(k, KernelCommandType.ENTER, t1, symbol, "SHORT", p, 0.001); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, t1, symbol, "SHORT", p*0.995, 0.001); await asyncio.sleep(1)',
'_si(k, KernelCommandType.ENTER, t2, symbol, "SHORT", p*0.995, 0.001); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, t2, symbol, "SHORT", p*0.99, 0.001); await asyncio.sleep(1)',
])
S("entry_then_recover", [
'tid = f"r-{int(time.time()*1000)}"; p = float(snap.price)',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(1)',
'await bundle.runtime.disconnect()',
'await bundle.runtime.connect(initial_capital=k.account.snapshot.capital)',
'await asyncio.sleep(1)',
])
S("long_entry_exit", [
'tid = f"ln-{int(time.time()*1000)}"; p = float(snap.price)',
'_si(k, KernelCommandType.ENTER, tid, symbol, "LONG", p, 0.001); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, tid, symbol, "LONG", p*1.005, 0.001); await asyncio.sleep(1)',
])
# --- Cancel combos ---
S("cancel_idempotent", [
'tid = f"ci-{int(time.time()*1000)}"; p = float(snap.price)',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(0.5)',
'_si(k, KernelCommandType.CANCEL, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(0.3)',
'_si(k, KernelCommandType.CANCEL, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(1)',
])
S("double_cancel", [
'tid = f"dc-{int(time.time()*1000)}"; p = float(snap.price)',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(0.3)',
'_si(k, KernelCommandType.CANCEL, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(0.3)',
'_si(k, KernelCommandType.CANCEL, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(1)',
])
S("cancel_then_exit", [
'tid = f"ctx-{int(time.time()*1000)}"; p = float(snap.price)',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(0.5)',
'_si(k, KernelCommandType.CANCEL, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(0.3)',
'if not k.slot(0).is_free():',
' _si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.995, 0.001); await asyncio.sleep(1)',
])
S("exit_then_cancel_exit", [
'tid = f"exc-{int(time.time()*1000)}"; p = float(snap.price)',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.995, 0.001); await asyncio.sleep(0.3)',
'_si(k, KernelCommandType.CANCEL, tid, symbol, "SHORT", p*0.995, 0.001); await asyncio.sleep(1)',
])
S("exit_then_reentry", [
'p = float(snap.price)',
't1 = f"er1-{int(time.time()*1000)}"; t2 = f"er2-{int(time.time()*1000)}"',
'_si(k, KernelCommandType.ENTER, t1, symbol, "SHORT", p, 0.001); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, t1, symbol, "SHORT", p*0.995, 0.001); await asyncio.sleep(0.3)',
'_si(k, KernelCommandType.ENTER, t2, symbol, "SHORT", p*0.995, 0.001); await asyncio.sleep(1)',
])
S("limit_cancel", [
'tid = f"lc-{int(time.time()*1000)}"; p = float(snap.price)',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p*0.9, 0.001); await asyncio.sleep(0.5)',
'_si(k, KernelCommandType.CANCEL, tid, symbol, "SHORT", p*0.9, 0.001); await asyncio.sleep(1)',
])
# --- X4 expanded ---
S("x4_partial_hold_exit", [
'tid = f"ph-{int(time.time()*1000)}"; p = float(snap.price); sz = 0.003',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, sz, exit_leg_ratios=(0.3,1.0)); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.995, sz*0.3, exit_leg_ratios=(0.3,1.0)); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.993, sz*0.7, exit_leg_ratios=(0.3,1.0)); await asyncio.sleep(1)',
])
S("x4_three_leg", [
'tid = f"3l-{int(time.time()*1000)}"; p = float(snap.price); sz = 0.004',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, sz, exit_leg_ratios=(0.25,0.25,1.0)); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.995, sz*0.25, exit_leg_ratios=(0.25,0.25,1.0)); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.993, sz*0.25, exit_leg_ratios=(0.25,0.25,1.0)); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.99, sz*0.5, exit_leg_ratios=(0.25,0.25,1.0)); await asyncio.sleep(1)',
])
S("x4_cancel_fill_partial", [
'tid = f"cfp-{int(time.time()*1000)}"; p = float(snap.price)',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, 0.002); await asyncio.sleep(0.5)',
'_si(k, KernelCommandType.CANCEL, tid, symbol, "SHORT", p, 0.002); await asyncio.sleep(0.3)',
'if not k.slot(0).is_free():',
' _si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.995, 0.001); await asyncio.sleep(0.5)',
'if not k.slot(0).is_free():',
' _si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.993, 0.001); await asyncio.sleep(1)',
])
S("x4_rapid_three", [
'p = float(snap.price)',
'for i in range(3):',
' tid = f"r3-{i}-{int(time.time()*1000)}"',
' _si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p*(1-i*0.005), 0.001); await asyncio.sleep(0.8)',
' _si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.995*(1-i*0.005), 0.001); await asyncio.sleep(0.8)',
])
S("x4_diff_symbol", [
'tid = f"ds-{int(time.time()*1000)}"; p = float(snap.price)',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(1)',
'sym2 = "BTCUSDT" if symbol != "BTCUSDT" else "ETHUSDT"',
'_si(k, KernelCommandType.EXIT, tid, sym2, "SHORT", p, 0.001); await asyncio.sleep(0.5)',
])
S("x4_alternating", [
'p = float(snap.price)',
't1 = f"as1-{int(time.time()*1000)}"; t2 = f"as2-{int(time.time()*1000)}"',
'_si(k, KernelCommandType.ENTER, t1, symbol, "SHORT", p, 0.001); await asyncio.sleep(1)',
'sym2 = "BTCUSDT" if symbol != "BTCUSDT" else "ETHUSDT"',
'try:',
' url = "https://open-api-vst.bingx.com/openApi/swap/v2/quote/price?symbol=" + sym2.replace("USDT","-USDT")',
' p2 = float(json.loads(urllib.request.urlopen(url, timeout=5).read())["data"]["price"])',
'except: p2 = p',
'_si(k, KernelCommandType.ENTER, t2, sym2, "LONG", p2, 0.001); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, t1, symbol, "SHORT", p*0.995, 0.001); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, t2, sym2, "LONG", p2*1.005, 0.001); await asyncio.sleep(1)',
])
S("x4_multi_flatten", [
'tid = f"mf-{int(time.time()*1000)}"; p = float(snap.price)',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(1)',
'for i in range(3):',
' if k.slot(0).is_free(): break',
' _flatten_via_kernel_intent(k, symbol, p*0.99, f"mf{i}"); await asyncio.sleep(0.5)',
])
S("x4_three_leg_25_50_25", [
'tid = f"x4a-{int(time.time()*1000)}"; p = float(snap.price); sz = 0.004',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, sz, exit_leg_ratios=(0.25,0.5,1.0)); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.995, sz*0.25, exit_leg_ratios=(0.25,0.5,1.0)); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.993, sz*0.5, exit_leg_ratios=(0.25,0.5,1.0)); await asyncio.sleep(1)',
'_si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.99, sz*0.25, exit_leg_ratios=(0.25,0.5,1.0)); await asyncio.sleep(1)',
])
S("x4_enter_exit_hold_twice", [
'p = float(snap.price)',
't1 = f"x4b1-{int(time.time()*1000)}"',
'_si(k, KernelCommandType.ENTER, t1, symbol, "SHORT", p, 0.001); await asyncio.sleep(0.5)',
'_si(k, KernelCommandType.EXIT, t1, symbol, "SHORT", p*0.995, 0.001); await asyncio.sleep(0.5)',
't2 = f"x4b2-{int(time.time()*1000)}"',
'_si(k, KernelCommandType.ENTER, t2, symbol, "SHORT", p*0.995, 0.001); await asyncio.sleep(0.5)',
'_si(k, KernelCommandType.EXIT, t2, symbol, "SHORT", p*0.99, 0.001); await asyncio.sleep(0.5)',
't3 = f"x4b3-{int(time.time()*1000)}"',
'_si(k, KernelCommandType.ENTER, t3, symbol, "SHORT", p*0.99, 0.001); await asyncio.sleep(0.5)',
'_si(k, KernelCommandType.EXIT, t3, symbol, "SHORT", p*0.985, 0.001); await asyncio.sleep(0.5)',
])
S("x4_cancel_then_double_exit", [
'tid = f"x4c-{int(time.time()*1000)}"; p = float(snap.price); sz = 0.002',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, sz, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(0.5)',
'_si(k, KernelCommandType.CANCEL, tid, symbol, "SHORT", p, sz); await asyncio.sleep(0.3)',
'if not k.slot(0).is_free():',
' _si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.995, sz*0.5, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(0.5)',
'if not k.slot(0).is_free():',
' _si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.993, sz*0.5, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(0.5)',
])
# --- 2 sides × 2 profit × 4 patterns = 16 ---
for side, side_str, ep in [("short","SHORT",0.995), ("long","LONG",1.005)]:
for prof, pname, xp_mult in [(True,"profit",ep), (False,"loss",1/ep)]:
for pat, pat_suffix, lines in [
("basic", "", [
f'_si(k, KernelCommandType.ENTER, tid, symbol, "{side_str}", p, 0.001); await asyncio.sleep(0.8)',
f'_si(k, KernelCommandType.EXIT, tid, symbol, "{side_str}", p*{xp_mult}, 0.001); await asyncio.sleep(0.8)',
]),
("partial", "_partial", [
'sz = 0.002',
f'_si(k, KernelCommandType.ENTER, tid, symbol, "{side_str}", p, sz, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(0.8)',
f'_si(k, KernelCommandType.EXIT, tid, symbol, "{side_str}", p*{ep}, sz*0.5, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(0.8)',
f'_si(k, KernelCommandType.EXIT, tid, symbol, "{side_str}", p*{xp_mult}, sz*0.5, exit_leg_ratios=(0.5,1.0)); await asyncio.sleep(0.8)',
]),
("cancel", "_cancel", [
f'_si(k, KernelCommandType.ENTER, tid, symbol, "{side_str}", p, 0.001); await asyncio.sleep(0.3)',
f'_si(k, KernelCommandType.CANCEL, tid, symbol, "{side_str}", p, 0.001); await asyncio.sleep(0.3)',
'if not k.slot(0).is_free():',
f' _si(k, KernelCommandType.EXIT, tid, symbol, "{side_str}", p*{xp_mult}, 0.001); await asyncio.sleep(0.8)',
]),
("double_exit", "_double_exit", [
f'_si(k, KernelCommandType.ENTER, tid, symbol, "{side_str}", p, 0.001); await asyncio.sleep(0.8)',
f'_si(k, KernelCommandType.EXIT, tid, symbol, "{side_str}", p*{xp_mult}, 0.001); await asyncio.sleep(0.3)',
'if not k.slot(0).is_free():',
f' _si(k, KernelCommandType.EXIT, tid, symbol, "{side_str}", p*{xp_mult}*0.995, 0.001); await asyncio.sleep(0.5)',
]),
]:
name = f"{pat}_{side}_{pname}"
S(name, [
f'tid = f"{pat[0]}{side[0]}{"p" if prof else "l"}-{{int(time.time()*1000)}}"; p = float(snap.price)',
*lines,
])
# --- Triple sequential × 4 ---
for i in range(4):
side = "SHORT"; ep = 0.995
S(f"triple_seq_{i}", [
'p = float(snap.price)',
'for j in range(3):',
f' tid = f"ts{i}-j-{{int(time.time()*1000)}}"',
f' _si(k, KernelCommandType.ENTER, tid, symbol, "{side}", p*(1-j*0.003), 0.001); await asyncio.sleep(0.7)',
f' _si(k, KernelCommandType.EXIT, tid, symbol, "{side}", p*{ep}*(1-j*0.003), 0.001); await asyncio.sleep(0.7)',
])
for i in range(4):
side = "LONG"; ep = 1.005
S(f"triple_seq_long_{i}", [
'p = float(snap.price)',
'for j in range(3):',
f' tid = f"tsl{i}-j-{{int(time.time()*1000)}}"',
f' _si(k, KernelCommandType.ENTER, tid, symbol, "{side}", p*(1+j*0.003), 0.001); await asyncio.sleep(0.7)',
f' _si(k, KernelCommandType.EXIT, tid, symbol, "{side}", p*{ep}*(1+j*0.003), 0.001); await asyncio.sleep(0.7)',
])
# --- Cancel+reenter × 4 ---
for i in range(4):
side = "SHORT"
S(f"cancel_reenter_{i}", [
'p = float(snap.price)',
f't1 = f"cr{i}a-{{int(time.time()*1000)}}"; t2 = f"cr{i}b-{{int(time.time()*1000)}}"',
f'_si(k, KernelCommandType.ENTER, t1, symbol, "{side}", p, 0.001); await asyncio.sleep(0.3)',
f'_si(k, KernelCommandType.CANCEL, t1, symbol, "{side}", p, 0.001); await asyncio.sleep(0.3)',
f'_si(k, KernelCommandType.ENTER, t2, symbol, "{side}", p*0.995, 0.001); await asyncio.sleep(0.8)',
'if not k.slot(0).is_free():',
f' _si(k, KernelCommandType.EXIT, t2, symbol, "{side}", p*0.99, 0.001); await asyncio.sleep(0.5)',
])
for i in range(4):
side = "LONG"
S(f"cancel_reenter_long_{i}", [
'p = float(snap.price)',
f't1 = f"crl{i}a-{{int(time.time()*1000)}}"; t2 = f"crl{i}b-{{int(time.time()*1000)}}"',
f'_si(k, KernelCommandType.ENTER, t1, symbol, "{side}", p, 0.001); await asyncio.sleep(0.3)',
f'_si(k, KernelCommandType.CANCEL, t1, symbol, "{side}", p, 0.001); await asyncio.sleep(0.3)',
f'_si(k, KernelCommandType.ENTER, t2, symbol, "{side}", p*1.005, 0.001); await asyncio.sleep(0.8)',
'if not k.slot(0).is_free():',
f' _si(k, KernelCommandType.EXIT, t2, symbol, "{side}", p*1.01, 0.001); await asyncio.sleep(0.5)',
])
# --- Leg ratios × 8 ---
for i, ratios in enumerate([
(0.1,1.0), (0.33,0.33,1.0), (0.5,0.5,1.0), (0.75,1.0),
(0.2,0.3,0.5,1.0), (0.4,0.6,1.0), (0.15,0.85,1.0), (0.25,0.25,0.5,1.0),
]):
rat_str = ",".join(str(r) for r in ratios)
nlegs = len(ratios)
code = [
f'tid = f"lr{i}-{{int(time.time()*1000)}}"; p = float(snap.price); sz = 0.004',
f'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, sz, exit_leg_ratios=({rat_str})); await asyncio.sleep(1)',
]
for leg in range(nlegs - 1):
r = ratios[leg]
code.append(f'_si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.995*(1-{leg}*0.002), sz*{r}, exit_leg_ratios=({rat_str})); await asyncio.sleep(0.8)')
r_last = ratios[-1]
code.append(f'_si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p*0.99, sz*{r_last}, exit_leg_ratios=({rat_str})); await asyncio.sleep(0.8)')
S(f"leg_ratio_{i}", code)
# --- Breakeven × 4 ---
for i in range(4):
S(f"breakeven_{i}", [
f'tid = f"be{i}-{{int(time.time()*1000)}}"; p = float(snap.price)',
'_si(k, KernelCommandType.ENTER, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(0.8)',
'_si(k, KernelCommandType.EXIT, tid, symbol, "SHORT", p, 0.001); await asyncio.sleep(0.8)',
])
# =====================================================================
# Assemble output
# =====================================================================
lines = [PROLOGUE, RUNNER]
lines.append('# =====================================================================')
lines.append('# Scenario body functions')
lines.append('# =====================================================================')
lines.append('')
lines.append('k = None # type: ignore # shorthand alias for bundle.runtime.kernel')
lines.append('')
for name, code_lines in scenarios:
lines.append(f'async def _body_{name}(bundle, client, symbol, snap):')
lines.append(' k = bundle.runtime.kernel')
for cl in code_lines:
lines.append(f' {cl}')
lines.append('')
lines.append('# =====================================================================')
lines.append('# Test functions')
lines.append('# =====================================================================')
lines.append('')
lines.append(
'@pytest.fixture(scope="session")\n'
'def _live_client():\n'
' cfg = _build_bingx_config(25000.0)\n'
' c = BingxHttpClient(cfg)\n'
' yield c\n'
)
for name, _ in scenarios:
lines.append(f'''
def test_pink_ditav2_{name}(_live_client) -> None:
bundle = _build_runtime_bundle(25000.0)
ic = bundle.runtime.kernel.account.snapshot.capital
result = asyncio.run(_run_scenario(bundle, _live_client, _body_{name}, "{name}", ic))
assert result.positions_flat, f"{name}: {{result.error}}"
''')
lines.append('''
def test_pink_ditav2_open_partial_close_and_flatten(_live_client) -> None:
bundle = _build_runtime_bundle(25000.0)
outcomes = asyncio.run(_run_pink_live_roundtrip(bundle, _live_client))
e, m, f = outcomes
assert e.accepted or e.diagnostic_code in {KernelDiagnosticCode.OK}, f"Entry not accepted: {e.diagnostic_code}"
slot = bundle.runtime.kernel.slot(0) if bundle.runtime.kernel.max_slots > 0 else None
if slot is not None and not slot.is_free():
pytest.skip(f"Slot not flat (fsm_state={slot.fsm_state})")
def test_pink_ditav2_reconciliation_only_on_explicit_recovery(_live_client) -> None:
bundle = _build_runtime_bundle(25000.0)
recovered = asyncio.run(_run_pink_live_recovery(bundle, _live_client))
assert isinstance(recovered, dict), f"Expected dict, got {type(recovered)}"
assert recovered.get("capital", 0) > 0, "Expected positive capital after recovery"
''')
full = '\n'.join(lines)
try:
ast.parse(full)
test_count = full.count("def test_pink_ditav2_")
print(f"Syntax OK — {test_count} tests, {len(full)} chars")
with open(OUT, 'w') as f:
f.write(full)
print(f"Written to {OUT}")
print(f"Breakdown: {len(scenarios)} scenarios + 2 legacy = {test_count} total tests")
except SyntaxError as e:
print(f"Syntax error line {e.lineno}: {e.msg}")
fl = full.split('\n')
for i in range(max(0,e.lineno-5), min(len(fl), e.lineno+3)):
print(f" {i+1}: {fl[i]}")

View File

@@ -0,0 +1,67 @@
from __future__ import annotations
import json
from typing import Any, Protocol
from .contracts import KernelTransition, TradeSlot
from .control import KernelControlSnapshot
from .journal import _transition_row
from .projection import build_position_state_row
from .utils import json_safe
class HazelcastClientLike(Protocol):
def get_map(self, name: str): ...
def get_topic(self, name: str): ...
class HazelcastProjector:
"""Durable BLUE/PINK-compatible projection mirror."""
def __init__(
self,
client: HazelcastClientLike | None = None,
*,
active_slots_map: str = "dita_active_slots",
events_topic: str = "dita_trade_events",
) -> None:
self.client = client
self.active_slots_map = active_slots_map
self.events_topic = events_topic
def publish_slot(self, slot: TradeSlot) -> None:
if self.client is None:
return
self.client.get_map(self.active_slots_map).put(slot.trade_id, build_position_state_row(slot))
def publish_event(self, event_type: str, payload: dict[str, Any]) -> None:
if self.client is None:
return
topic = self.client.get_topic(self.events_topic)
topic.publish(
json.dumps(
{"event_type": event_type, "payload": json_safe(payload)},
ensure_ascii=False,
sort_keys=True,
default=str,
)
)
class HazelcastRowWriter:
"""Callback bridge for ``HazelcastProjection`` writer hooks."""
def __init__(self, client: HazelcastClientLike) -> None:
self.client = client
def __call__(self, name: str, row: dict[str, Any]) -> None:
if name.endswith("trade_events"):
self.client.get_topic(name).publish(
json.dumps(row, ensure_ascii=False, sort_keys=True, default=str)
)
return
if name.endswith("control"):
key = "control"
else:
key = str(row.get("trade_id", row.get("slot_id", row.get("event_id", ""))))
self.client.get_map(name).put(key, json_safe(row))

View File

@@ -0,0 +1,102 @@
"""Debug journaling surfaces for DITAv2."""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any, Callable, Dict, List, Optional, Protocol
from .contracts import KernelTransition, TradeSlot, TradeStage, VenueEvent
from .control import KernelControlSnapshot
from .utils import json_safe, json_text
JournalSink = Callable[[str, Dict[str, Any]], None]
class KernelJournal(Protocol):
"""Append-only debug journal interface."""
def record(self, row: Dict[str, Any]) -> None:
...
def record_transition(
self,
*,
transition: KernelTransition,
slot: TradeSlot,
event: Optional[VenueEvent] = None,
control: Optional[KernelControlSnapshot] = None,
) -> None:
...
@dataclass
class MemoryKernelJournal:
"""In-memory journal used in tests."""
rows: List[Dict[str, Any]] = field(default_factory=list)
capture_limit: int = 10_000
def record(self, row: Dict[str, Any]) -> None:
if len(self.rows) < self.capture_limit:
self.rows.append(dict(row))
def record_transition(
self,
*,
transition: KernelTransition,
slot: TradeSlot,
event: Optional[VenueEvent] = None,
control: Optional[KernelControlSnapshot] = None,
) -> None:
row = _transition_row(transition=transition, slot=slot, event=event, control=control)
self.record(row)
class ClickHouseKernelJournal:
"""Fire-and-forget ClickHouse journal.
The sink is a small callable of the form ``sink(table_name, row_dict)``.
"""
def __init__(self, sink: Optional[JournalSink] = None):
self.sink = sink
def record(self, row: Dict[str, Any]) -> None:
if self.sink is not None:
self.sink("dita_kernel_debug", row)
def record_transition(
self,
*,
transition: KernelTransition,
slot: TradeSlot,
event: Optional[VenueEvent] = None,
control: Optional[KernelControlSnapshot] = None,
) -> None:
self.record(_transition_row(transition=transition, slot=slot, event=event, control=control))
def _transition_row(
*,
transition: KernelTransition,
slot: TradeSlot,
event: Optional[VenueEvent],
control: Optional[KernelControlSnapshot],
) -> Dict[str, Any]:
return {
"ts": transition.timestamp.isoformat() if hasattr(transition.timestamp, "isoformat") else str(transition.timestamp),
"trade_id": transition.trade_id,
"slot_id": transition.slot_id,
"prev_state": transition.prev_state.value,
"next_state": transition.next_state.value,
"trigger": transition.trigger,
"intent_id": transition.intent_id,
"event_id": transition.event_id,
"control_mode": transition.control_mode,
"control_verbosity": transition.control_verbosity,
"slot_state": slot.to_dict(),
"event_payload": json_safe(event) if event is not None else {},
"control_snapshot": control.as_dict() if control is not None else {},
"slot_state_json": json_text(slot.to_dict()),
}

View File

@@ -0,0 +1,8 @@
"""Compatibility shim for the Rust-backed DITAv2 execution kernel."""
from __future__ import annotations
from .rust_backend import ExecutionKernel
__all__ = ["ExecutionKernel"]

View File

@@ -0,0 +1,350 @@
"""Operator-facing bootstrap helpers for DITAv2.
This module keeps the wiring explicit:
- control plane selection
- Zinc plane selection
- projection sink selection
- venue adapter selection
The defaults stay safe and testable. Real shared-memory or live BingX wiring
is only enabled when the caller opts in via arguments or environment.
"""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
import asyncio
import inspect
import os
from pathlib import Path
from typing import Any, Optional
from dotenv import load_dotenv
from prod.bingx.config import BingxExecClientConfig
from prod.bingx.config import BingxInstrumentProviderConfig
from prod.bingx.enums import BingxEnvironment
from .bingx_venue import BingxVenueAdapter
from .control import BackendMode
from .control import ControlPlane
from .control import ControlUpdate
from .control import KernelControlSnapshot
from .control import KernelMode
from .control import KernelVerbosity
from .control import build_control_plane
from .mock_venue import MockVenueAdapter
from .mock_venue import MockVenueScenario
from .projection import HazelcastProjection
from .projection import build_projection
from .real_control_plane import RealZincControlPlane
from .real_control_plane import RealZincUnavailable
from .real_zinc_plane import RealZincPlane
from .real_zinc_plane import RealZincUnavailable as RealZincPlaneUnavailable
from .rust_backend import ExecutionKernel
from .venue import VenueAdapter
from .zinc_plane import InMemoryZincPlane
from .zinc_plane import ZincPlane
PROJECT_ROOT = Path(__file__).resolve().parents[3]
load_dotenv(PROJECT_ROOT / ".env")
class LauncherVenueMode(str, Enum):
MOCK = "MOCK"
BINGX = "BINGX"
class LauncherZincMode(str, Enum):
IN_MEMORY = "IN_MEMORY"
REAL = "REAL"
@dataclass
class DITAv2LauncherBundle:
"""Concrete runtime components assembled by the launcher."""
kernel: ExecutionKernel
control_plane: ControlPlane
projection: HazelcastProjection
zinc_plane: ZincPlane
venue: VenueAdapter
def close(self) -> None:
_maybe_close(self.venue)
_maybe_close(self.zinc_plane)
_maybe_close(self.control_plane)
def _env_upper(name: str, default: str = "") -> str:
return str(os.environ.get(name, default)).strip().upper()
def _env_bool(name: str, default: bool = False) -> bool:
raw = os.environ.get(name)
if raw is None:
return default
return str(raw).strip().lower() in {"1", "true", "yes", "on"}
def _resolve_control_mode() -> KernelMode | None:
raw = _env_upper("DITA_V2_MODE", "")
if raw == KernelMode.DEBUG.value:
return KernelMode.DEBUG
if raw == KernelMode.NORMAL.value:
return KernelMode.NORMAL
return None
def _resolve_control_verbosity() -> KernelVerbosity | None:
raw = _env_upper("DITA_V2_VERBOSITY", "")
if raw == KernelVerbosity.TRACE.value:
return KernelVerbosity.TRACE
if raw == KernelVerbosity.VERBOSE.value:
return KernelVerbosity.VERBOSE
if raw == KernelVerbosity.QUIET.value:
return KernelVerbosity.QUIET
return None
def _resolve_backend_mode() -> BackendMode | None:
raw = _env_upper("DITA_V2_BACKEND_MODE", "")
if raw == BackendMode.BINGX.value:
return BackendMode.BINGX
if raw == BackendMode.MOCK.value:
return BackendMode.MOCK
return None
def _control_update_from_env() -> ControlUpdate | None:
fields: dict[str, Any] = {}
mode = _resolve_control_mode()
if mode is not None:
fields["mode"] = mode
verbosity = _resolve_control_verbosity()
if verbosity is not None:
fields["verbosity"] = verbosity
backend_mode = _resolve_backend_mode()
if backend_mode is not None:
fields["backend_mode"] = backend_mode
raw = os.environ.get("DITA_V2_DEBUG_CLICKHOUSE")
if raw is not None:
fields["debug_clickhouse_enabled"] = _env_bool("DITA_V2_DEBUG_CLICKHOUSE", True)
raw = os.environ.get("DITA_V2_TRACE_TRANSITIONS")
if raw is not None:
fields["trace_transitions"] = _env_bool("DITA_V2_TRACE_TRANSITIONS", False)
raw = os.environ.get("DITA_V2_MIRROR_TO_HAZELCAST")
if raw is not None:
fields["mirror_to_hazelcast"] = _env_bool("DITA_V2_MIRROR_TO_HAZELCAST", True)
raw = os.environ.get("DITA_V2_ACTIVE_SLOT_LIMIT")
if raw is not None:
try:
fields["active_slot_limit"] = max(1, int(str(raw).strip()))
except Exception:
pass
raw = os.environ.get("DITA_V2_RECONCILE_ON_RESTART")
if raw is not None:
fields["reconcile_on_restart"] = _env_bool("DITA_V2_RECONCILE_ON_RESTART", True)
return ControlUpdate(**fields) if fields else None
def _resolve_venue_mode(venue_mode: Optional[str] = None) -> LauncherVenueMode:
raw = _env_upper("DITA_V2_VENUE", venue_mode or LauncherVenueMode.MOCK.value)
if raw == LauncherVenueMode.BINGX.value:
return LauncherVenueMode.BINGX
return LauncherVenueMode.MOCK
def _resolve_zinc_mode(zinc_mode: Optional[str] = None) -> LauncherZincMode:
raw = _env_upper("DITA_V2_ZINC", zinc_mode or LauncherZincMode.IN_MEMORY.value)
if raw == LauncherZincMode.REAL.value:
return LauncherZincMode.REAL
return LauncherZincMode.IN_MEMORY
def _resolve_hazelcast_real(prefer_real_hazelcast: Optional[bool] = None) -> bool:
if prefer_real_hazelcast is not None:
return bool(prefer_real_hazelcast)
raw = _env_upper("DITA_V2_HAZELCAST", "")
return raw in {"REAL", "REAL_HZ", "HAZELCAST"}
def build_bingx_exec_client_config(
*,
environment: Optional[BingxEnvironment] = None,
allow_mainnet: Optional[bool] = None,
recv_window_ms: Optional[int] = None,
default_leverage: Optional[int] = None,
exchange_leverage_cap: Optional[int] = None,
prefer_websocket: Optional[bool] = None,
sizing_mode: Optional[str] = None,
) -> BingxExecClientConfig:
"""Build the direct BingX config used by the DITAv2 launcher."""
resolved_environment = environment or (
BingxEnvironment.LIVE if _env_upper("DOLPHIN_BINGX_ENV", "VST") == "LIVE" else BingxEnvironment.VST
)
resolved_allow_mainnet = _env_bool("DOLPHIN_BINGX_ALLOW_MAINNET", False) if allow_mainnet is None else bool(allow_mainnet)
resolved_recv_window = int(os.environ.get("DOLPHIN_BINGX_RECV_WINDOW_MS", "5000")) if recv_window_ms is None else int(recv_window_ms)
resolved_default_leverage = int(os.environ.get("DOLPHIN_BINGX_DEFAULT_LEVERAGE", "1")) if default_leverage is None else int(default_leverage)
resolved_exchange_cap = int(os.environ.get("DOLPHIN_BINGX_EXCHANGE_LEVERAGE_CAP", "3")) if exchange_leverage_cap is None else int(exchange_leverage_cap)
resolved_prefer_ws = _env_bool("DOLPHIN_BINGX_PREFER_WEBSOCKET", False) if prefer_websocket is None else bool(prefer_websocket)
resolved_sizing_mode = sizing_mode or os.environ.get("DOLPHIN_BINGX_SIZING_MODE", "testnet")
return BingxExecClientConfig(
api_key=os.environ.get("BINGX_API_KEY"),
secret_key=os.environ.get("BINGX_SECRET_KEY"),
environment=resolved_environment,
allow_mainnet=resolved_allow_mainnet,
recv_window_ms=max(1, resolved_recv_window),
default_leverage=max(1, resolved_default_leverage),
exchange_leverage_cap=max(1, resolved_exchange_cap),
prefer_websocket=resolved_prefer_ws,
sizing_mode=resolved_sizing_mode,
journal_strategy=os.environ.get("DOLPHIN_BINGX_JOURNAL_STRATEGY", "dita_v2"),
journal_db=os.environ.get("DOLPHIN_BINGX_JOURNAL_DB", "dolphin_pink"),
instrument_provider=BingxInstrumentProviderConfig(load_all=True),
)
def _build_control_plane(
*,
prefix: str,
control_plane: Optional[ControlPlane] = None,
) -> ControlPlane:
plane = control_plane or build_control_plane(prefix=prefix)
update = _control_update_from_env()
if update is not None:
plane.update(update)
return plane
def _build_zinc_plane(
*,
prefix: str,
slot_count: int,
zinc_mode: Optional[LauncherZincMode] = None,
zinc_plane: Optional[ZincPlane] = None,
) -> ZincPlane:
if zinc_plane is not None:
return zinc_plane
resolved_mode = zinc_mode or _resolve_zinc_mode()
if resolved_mode is LauncherZincMode.REAL:
try:
return RealZincPlane(prefix=prefix, slot_count=slot_count, create=True)
except (RealZincPlaneUnavailable, RealZincUnavailable, Exception):
pass
return InMemoryZincPlane()
def _build_venue(
*,
venue_mode: Optional[LauncherVenueMode] = None,
mock_scenario: Optional[MockVenueScenario] = None,
bingx_config: Optional[BingxExecClientConfig] = None,
bingx_backend: Optional[Any] = None,
venue: Optional[VenueAdapter] = None,
) -> VenueAdapter:
if venue is not None:
return venue
resolved_mode = venue_mode or _resolve_venue_mode()
if resolved_mode is LauncherVenueMode.BINGX:
backend = bingx_backend
if backend is None:
from prod.clean_arch.adapters.bingx_direct import BingxDirectExecutionAdapter
backend = BingxDirectExecutionAdapter(bingx_config or build_bingx_exec_client_config())
return BingxVenueAdapter(backend=backend)
return MockVenueAdapter(mock_scenario)
def _maybe_close(obj: Any) -> None:
for method_name in ("close", "disconnect"):
method = getattr(obj, method_name, None)
if method is None:
continue
try:
result = method()
except TypeError:
continue
if inspect.isawaitable(result):
try:
asyncio.run(result)
except RuntimeError:
pass
break
def build_launcher_bundle(
*,
max_slots: int = 10,
prefix: Optional[str] = None,
control_plane: Optional[ControlPlane] = None,
projection: Optional[HazelcastProjection] = None,
projection_client: Optional[Any] = None,
zinc_plane: Optional[ZincPlane] = None,
venue: Optional[VenueAdapter] = None,
venue_mode: Optional[LauncherVenueMode | str] = None,
zinc_mode: Optional[LauncherZincMode | str] = None,
bingx_config: Optional[BingxExecClientConfig] = None,
bingx_backend: Optional[Any] = None,
mock_scenario: Optional[MockVenueScenario] = None,
) -> DITAv2LauncherBundle:
"""Build a fully wired DITAv2 runtime bundle.
Defaults stay non-destructive:
- in-memory Zinc plane
- in-process control plane
- mock venue
- callback projection unless a Hazelcast client is supplied
"""
resolved_prefix = (prefix or os.environ.get("DITA_V2_PREFIX", "dita_v2")).strip() or "dita_v2"
if isinstance(venue_mode, LauncherVenueMode):
resolved_venue_mode = venue_mode
elif isinstance(venue_mode, str):
resolved_venue_mode = LauncherVenueMode(venue_mode.strip().upper())
else:
resolved_venue_mode = None
if isinstance(zinc_mode, LauncherZincMode):
resolved_zinc_mode = zinc_mode
elif isinstance(zinc_mode, str):
resolved_zinc_mode = LauncherZincMode(zinc_mode.strip().upper())
else:
resolved_zinc_mode = None
active_control_plane = _build_control_plane(prefix=resolved_prefix, control_plane=control_plane)
control_snapshot = active_control_plane.read()
active_projection = projection or build_projection(
client=projection_client,
prefer_real_hazelcast=_resolve_hazelcast_real(),
control_snapshot=control_snapshot,
)
active_zinc_plane = _build_zinc_plane(
prefix=resolved_prefix,
slot_count=int(max_slots),
zinc_mode=resolved_zinc_mode,
zinc_plane=zinc_plane,
)
active_venue = _build_venue(
venue_mode=resolved_venue_mode,
mock_scenario=mock_scenario,
bingx_config=bingx_config,
bingx_backend=bingx_backend,
venue=venue,
)
kernel = ExecutionKernel(
max_slots=int(max_slots),
control_plane=active_control_plane,
venue=active_venue,
projection=active_projection,
projection_client=projection_client,
zinc_plane=active_zinc_plane,
)
return DITAv2LauncherBundle(
kernel=kernel,
control_plane=active_control_plane,
projection=active_projection,
zinc_plane=active_zinc_plane,
venue=active_venue,
)

View File

@@ -0,0 +1,203 @@
"""Deterministic mock venue for DITAv2 tests."""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional
import itertools
from .contracts import (
KernelCommandType,
KernelEventKind,
KernelIntent,
TradeSide,
VenueEvent,
VenueEventStatus,
VenueOrder,
VenueOrderStatus,
)
from .venue import VenueAdapter
@dataclass(frozen=True)
class MockVenueScenario:
"""Failure knobs for the mock venue."""
reject_entries: bool = False
reject_exits: bool = False
partial_fill_ratio: float = 1.0
cancel_reject: bool = False
emit_ack_before_fill: bool = True
emit_fill_on_submit: bool = False
class MockVenueAdapter(VenueAdapter):
"""Scriptable mock venue with BingX-shaped response semantics."""
def __init__(self, scenario: Optional[MockVenueScenario] = None):
self.scenario = scenario or MockVenueScenario()
self._order_seq = itertools.count(1)
self._event_seq = itertools.count(1)
self._open_orders: Dict[str, VenueOrder] = {}
self._open_positions: Dict[str, Dict[str, Any]] = {}
def submit(self, intent: KernelIntent) -> List[VenueEvent]:
is_entry = intent.action == KernelCommandType.ENTER
should_reject = self.scenario.reject_entries if is_entry else self.scenario.reject_exits
order_id = f"V-{next(self._order_seq):08d}"
client_id = f"{intent.trade_id}:{intent.intent_id}"
order = VenueOrder(
internal_trade_id=intent.trade_id,
venue_order_id=order_id,
venue_client_id=client_id,
side=intent.side,
intended_size=float(intent.target_size),
status=VenueOrderStatus.NEW,
metadata={"intent_id": intent.intent_id, "action": intent.action.value, "slot_id": intent.slot_id},
)
if should_reject:
order = VenueOrder(
internal_trade_id=order.internal_trade_id,
venue_order_id=order.venue_order_id,
venue_client_id=order.venue_client_id,
side=order.side,
intended_size=order.intended_size,
filled_size=0.0,
average_fill_price=0.0,
status=VenueOrderStatus.REJECTED,
metadata=dict(order.metadata),
)
return [self._event_from_order(intent, order, KernelEventKind.ORDER_REJECT, VenueEventStatus.REJECTED, reason="MOCK_REJECT")]
self._open_orders[order_id] = order
events: List[VenueEvent] = []
if self.scenario.emit_ack_before_fill or not self.scenario.emit_fill_on_submit:
events.append(self._event_from_order(intent, order, KernelEventKind.ORDER_ACK, VenueEventStatus.ACKED))
if self.scenario.emit_fill_on_submit or self.scenario.partial_fill_ratio > 0:
fill_ratio = max(0.0, min(1.0, float(self.scenario.partial_fill_ratio)))
fill_size = float(intent.target_size) * fill_ratio
event_kind = KernelEventKind.FULL_FILL if fill_ratio >= 1.0 else KernelEventKind.PARTIAL_FILL
event_status = VenueEventStatus.FILLED if fill_ratio >= 1.0 else VenueEventStatus.PARTIALLY_FILLED
fill_event = self._event_from_order(
intent,
order,
event_kind,
event_status,
price=float(intent.reference_price or 0.0),
fill_size=fill_size,
remaining_size=max(0.0, float(intent.target_size) - fill_size),
)
events.append(fill_event)
order = VenueOrder(
internal_trade_id=order.internal_trade_id,
venue_order_id=order.venue_order_id,
venue_client_id=order.venue_client_id,
side=order.side,
intended_size=order.intended_size,
filled_size=fill_size,
average_fill_price=float(intent.reference_price or 0.0),
status=VenueOrderStatus.FILLED if fill_ratio >= 1.0 else VenueOrderStatus.PARTIALLY_FILLED,
metadata=dict(order.metadata),
)
self._open_orders[order_id] = order
return events
def cancel(self, order: VenueOrder, *, reason: str = "") -> List[VenueEvent]:
if self.scenario.cancel_reject:
return [
self._event_from_order(
self._dummy_intent(order),
order,
KernelEventKind.CANCEL_REJECT,
VenueEventStatus.CANCELED_REJECTED,
reason=reason or "MOCK_CANCEL_REJECT",
)
]
existing = self._open_orders.get(order.venue_order_id, order)
canceled = VenueOrder(
internal_trade_id=existing.internal_trade_id,
venue_order_id=existing.venue_order_id,
venue_client_id=existing.venue_client_id,
side=existing.side,
intended_size=existing.intended_size,
filled_size=existing.filled_size,
average_fill_price=existing.average_fill_price,
status=VenueOrderStatus.CANCELED,
metadata=dict(existing.metadata),
)
self._open_orders.pop(order.venue_order_id, None)
return [
self._event_from_order(
self._dummy_intent(order),
canceled,
KernelEventKind.CANCEL_ACK,
VenueEventStatus.CANCELED,
reason=reason or "MOCK_CANCEL_ACK",
)
]
def open_orders(self) -> List[VenueOrder]:
return list(self._open_orders.values())
def open_positions(self) -> List[Dict[str, Any]]:
return list(self._open_positions.values())
def reconcile(self) -> List[VenueEvent]:
return []
def _dummy_intent(self, order: VenueOrder) -> KernelIntent:
return KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=order.venue_client_id,
trade_id=order.internal_trade_id,
slot_id=int(order.metadata.get("slot_id", 0)),
asset=str(order.metadata.get("asset", "")),
side=order.side,
action=KernelCommandType.EXIT if order.metadata.get("action") == "EXIT" else KernelCommandType.ENTER,
reference_price=float(order.metadata.get("reference_price", 0.0)),
target_size=float(order.intended_size),
leverage=float(order.metadata.get("leverage", 1.0)),
reason=str(order.metadata.get("reason", "")),
metadata=dict(order.metadata),
)
def _event_from_order(
self,
intent: KernelIntent,
order: VenueOrder,
kind: KernelEventKind,
status: VenueEventStatus,
*,
price: Optional[float] = None,
fill_size: float = 0.0,
remaining_size: float = 0.0,
reason: str = "",
) -> VenueEvent:
event = VenueEvent(
timestamp=datetime.now(timezone.utc),
event_id=f"EV-{next(self._event_seq):08d}",
trade_id=intent.trade_id,
slot_id=intent.slot_id,
kind=kind,
status=status,
venue_order_id=order.venue_order_id,
venue_client_id=order.venue_client_id,
side=order.side,
asset=intent.asset,
price=float(price if price is not None else intent.reference_price or 0.0),
size=float(intent.target_size),
filled_size=float(fill_size),
remaining_size=float(remaining_size),
reason=reason,
raw_payload={
"status": status.value,
"orderId": order.venue_order_id,
"clientOrderId": order.venue_client_id,
"symbol": intent.asset,
"side": order.side.value,
"action": intent.action.value,
},
metadata={"intent_id": intent.intent_id, "action": intent.action.value},
)
return event

View File

@@ -0,0 +1,97 @@
"""Hazelcast-compatible projection helpers for DITAv2."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
import os
from typing import Any, Callable, Dict, Iterable, List, Optional
from .account import AccountProjection
from .contracts import KernelTransition, TradeSlot, TradeStage, VenueEvent
from .control import KernelControlSnapshot
from .journal import _transition_row
from .utils import json_safe
Writer = Callable[[str, Dict[str, Any]], None]
@dataclass
class HazelcastProjection:
"""Projection helper for BLUE/PINK-compatible durable writes."""
active_slots_map: str = "hz:dita_active_slots"
trade_events_topic: str = "hz:dita_trade_events"
control_map: str = "hz:dita_control"
writer: Optional[Writer] = None
control_snapshot: Optional[KernelControlSnapshot] = None
def write_slot(self, slot: TradeSlot) -> Dict[str, Any]:
row = build_position_state_row(slot, self.control_snapshot)
if self.writer is not None:
self.writer(self.active_slots_map, row)
return row
def write_transition(
self,
*,
transition: KernelTransition,
slot: TradeSlot,
event: Optional[VenueEvent] = None,
control: Optional[KernelControlSnapshot] = None,
) -> Dict[str, Any]:
row = _transition_row(transition=transition, slot=slot, event=event, control=control)
if self.writer is not None:
self.writer(self.trade_events_topic, row)
return row
def write_control(self, control: KernelControlSnapshot) -> Dict[str, Any]:
self.control_snapshot = control
row = control.as_dict()
if self.writer is not None:
self.writer(self.control_map, row)
return row
def build_projection(
*,
writer: Optional[Writer] = None,
client: Optional[Any] = None,
prefer_real_hazelcast: Optional[bool] = None,
control_snapshot: Optional[KernelControlSnapshot] = None,
) -> HazelcastProjection:
"""Build the active projection helper with an operator-visible switch.
The default remains the callback-based projection helper. If a Hazelcast
client is supplied and the caller opts in via ``prefer_real_hazelcast`` or
``DITA_V2_HAZELCAST=REAL``, the helper routes directly through the
client-backed map/topic writer path.
"""
env_choice = os.environ.get("DITA_V2_HAZELCAST", "").strip().upper()
real_requested = prefer_real_hazelcast if prefer_real_hazelcast is not None else env_choice in {"REAL", "REAL_HZ", "HAZELCAST"}
if real_requested and client is not None:
try:
from .hazelcast_projection import HazelcastRowWriter
writer = HazelcastRowWriter(client)
except Exception:
pass
return HazelcastProjection(writer=writer, control_snapshot=control_snapshot)
def build_position_state_row(slot: TradeSlot, control: Optional[KernelControlSnapshot] = None) -> Dict[str, Any]:
"""Build a state row shaped for durable compatibility."""
row = slot.to_dict()
row.update(
{
"runtime_namespace": control.runtime_namespace if control else "dita_v2",
"strategy_namespace": control.strategy_namespace if control else "dita_v2",
"event_namespace": control.event_namespace if control else "dita_v2",
"actor_name": control.actor_name if control else "ExecutionKernel",
"exec_venue": control.exec_venue if control else "bingx",
"data_venue": control.data_venue if control else "binance",
"ledger_authority": control.ledger_authority if control else "exchange",
}
)
return row

View File

@@ -0,0 +1,129 @@
"""Real Zinc-backed control plane for DITAv2."""
from __future__ import annotations
import json
import struct
import sys
from pathlib import Path
from typing import Any, Dict, Optional
from .control import BackendMode, ControlPlane, ControlUpdate, KernelControlSnapshot, KernelMode, KernelVerbosity
_ZINC_ADAPTER_PATH = Path(__file__).resolve().parents[3] / "zinc" / "adapters" / "python"
if _ZINC_ADAPTER_PATH.exists() and str(_ZINC_ADAPTER_PATH) not in sys.path:
sys.path.insert(0, str(_ZINC_ADAPTER_PATH))
try: # pragma: no cover - exercised in integration tests
from zinc import SharedRegion
except Exception as exc: # pragma: no cover
SharedRegion = None # type: ignore[assignment]
_ZINC_IMPORT_ERROR = exc
else:
_ZINC_IMPORT_ERROR = None
class RealZincUnavailable(RuntimeError):
"""Raised when the Zinc Python adapter cannot be loaded."""
def require_real_zinc() -> None:
if SharedRegion is None:
raise RealZincUnavailable(str(_ZINC_IMPORT_ERROR))
def _json_default(value: Any) -> Any:
if hasattr(value, "value"):
return value.value
if hasattr(value, "isoformat"):
try:
return value.isoformat()
except Exception:
pass
if hasattr(value, "__dict__"):
return dict(vars(value))
raise TypeError(f"Unsupported value: {type(value)!r}")
def _encode_packet(seq: int, payload: Dict[str, Any]) -> bytes:
text = json.dumps(payload, sort_keys=True, ensure_ascii=False, default=_json_default, separators=(",", ":")).encode("utf-8")
return struct.pack("!QQ", int(seq), len(text)) + text
def _decode_packet(buf: memoryview) -> Dict[str, Any]:
if len(buf) < 16:
return {}
seq, size = struct.unpack_from("!QQ", buf, 0)
if size <= 0 or size > len(buf) - 16:
return {}
payload = bytes(buf[16 : 16 + size]).decode("utf-8")
out = json.loads(payload)
if isinstance(out, dict):
out["_seq"] = seq
return out
class RealZincControlPlane(ControlPlane):
"""Shared-memory Zinc-backed control plane."""
def __init__(self, *, prefix: str, create: bool = True) -> None:
require_real_zinc()
base = prefix.strip("/").replace("/", "_")
self.region_name = f"{base}_control"
self._seq = 0
self._snapshot = KernelControlSnapshot()
if create:
self.region = SharedRegion.create(self.region_name, 1 << 20)
self._write_region(self._seq, self._snapshot.as_dict())
else:
self.region = SharedRegion.open(self.region_name)
payload = _decode_packet(self.region.as_buffer())
control = payload.get("control") if isinstance(payload, dict) else None
if isinstance(control, dict):
self._snapshot = KernelControlSnapshot(**control)
def close(self) -> None:
self.region.close()
def read(self) -> KernelControlSnapshot:
payload = _decode_packet(self.region.as_buffer())
control = payload.get("control") if isinstance(payload, dict) else None
if not isinstance(control, dict):
return self._snapshot
self._snapshot = KernelControlSnapshot(**control)
return self._snapshot
def update(self, update: ControlUpdate) -> KernelControlSnapshot:
self._snapshot = update.apply(self.read())
self._seq += 1
self._write_region(self._seq, self._snapshot.as_dict())
return self._snapshot
def mirror(self) -> Dict[str, Any]:
return self._snapshot.as_dict()
def wait(self, timeout_ms: int = 1000) -> bool:
try:
return bool(self.region.wait(timeout_ms))
except Exception:
return False
def notify(self) -> None:
try:
self.region.notify()
except Exception:
pass
def _write_region(self, seq: int, control: Dict[str, Any]) -> None:
packet = _encode_packet(seq, {"control": control})
buf = self.region.as_buffer()
if len(packet) > len(buf):
raise ValueError(f"payload too large for Zinc control region: {len(packet)} > {len(buf)}")
view = memoryview(buf)
view[: len(packet)] = packet
if len(view) > len(packet):
view[len(packet) :] = b"\x00" * (len(view) - len(packet))
try:
self.region.notify()
except Exception:
pass

View File

@@ -0,0 +1,263 @@
"""Real Zinc-backed hot-path plane for DITAv2.
This wrapper uses the Zinc Python adapter directly. The kernel still talks to
the narrow ``ZincPlane`` interface; this module just makes that interface real.
"""
from __future__ import annotations
from dataclasses import asdict
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional
import json
import os
import struct
import sys
import threading
from .contracts import KernelIntent, TradeSide, TradeSlot, TradeStage, VenueOrder, VenueOrderStatus
from .control import KernelControlSnapshot
_ZINC_ADAPTER_PATH = Path(__file__).resolve().parents[3] / "zinc" / "adapters" / "python"
if _ZINC_ADAPTER_PATH.exists() and str(_ZINC_ADAPTER_PATH) not in sys.path:
sys.path.insert(0, str(_ZINC_ADAPTER_PATH))
try: # pragma: no cover - exercised in integration tests
from zinc import SharedRegion
except Exception as exc: # pragma: no cover
SharedRegion = None # type: ignore[assignment]
_ZINC_IMPORT_ERROR = exc
else:
_ZINC_IMPORT_ERROR = None
class RealZincUnavailable(RuntimeError):
"""Raised when the Zinc Python adapter cannot be loaded."""
def require_real_zinc() -> None:
if SharedRegion is None:
raise RealZincUnavailable(str(_ZINC_IMPORT_ERROR))
def _json_default(value: Any) -> Any:
if hasattr(value, "value"):
return value.value
if hasattr(value, "isoformat"):
try:
return value.isoformat()
except Exception:
pass
if hasattr(value, "__dict__"):
return dict(vars(value))
raise TypeError(f"Unsupported value: {type(value)!r}")
def _slot_to_payload(slot: TradeSlot) -> Dict[str, Any]:
data = slot.to_dict()
return data
def _slot_from_payload(payload: Dict[str, Any]) -> TradeSlot:
active_entry_order = None
active_exit_order = None
if isinstance(payload.get("active_entry_order"), dict):
active_entry_order = VenueOrder(
internal_trade_id=str(payload.get("trade_id", "")),
venue_order_id=str(payload["active_entry_order"].get("venue_order_id", "")),
venue_client_id=str(payload["active_entry_order"].get("venue_client_id", "")),
side=TradeSide(str(payload["active_entry_order"].get("side", TradeSide.FLAT.value))),
intended_size=float(payload["active_entry_order"].get("intended_size", payload.get("size", 0.0))),
filled_size=float(payload["active_entry_order"].get("filled_size", 0.0)),
average_fill_price=float(payload["active_entry_order"].get("average_fill_price", 0.0)),
status=VenueOrderStatus(str(payload["active_entry_order"].get("status", VenueOrderStatus.NEW.value))),
metadata=dict(payload["active_entry_order"].get("metadata", {})),
)
if isinstance(payload.get("active_exit_order"), dict):
active_exit_order = VenueOrder(
internal_trade_id=str(payload.get("trade_id", "")),
venue_order_id=str(payload["active_exit_order"].get("venue_order_id", "")),
venue_client_id=str(payload["active_exit_order"].get("venue_client_id", "")),
side=TradeSide(str(payload["active_exit_order"].get("side", TradeSide.FLAT.value))),
intended_size=float(payload["active_exit_order"].get("intended_size", payload.get("size", 0.0))),
filled_size=float(payload["active_exit_order"].get("filled_size", 0.0)),
average_fill_price=float(payload["active_exit_order"].get("average_fill_price", 0.0)),
status=VenueOrderStatus(str(payload["active_exit_order"].get("status", VenueOrderStatus.NEW.value))),
metadata=dict(payload["active_exit_order"].get("metadata", {})),
)
slot = TradeSlot(
slot_id=int(payload.get("slot_id", 0)),
trade_id=str(payload.get("trade_id", "")),
asset=str(payload.get("asset", "")),
side=TradeSide(str(payload.get("side", TradeSide.FLAT.value))),
entry_price=float(payload.get("entry_price", 0.0)),
size=float(payload.get("size", 0.0)),
initial_size=float(payload.get("initial_size", 0.0)),
leverage=float(payload.get("leverage", 0.0)),
entry_time=datetime.fromisoformat(payload["entry_time"]) if payload.get("entry_time") else None,
unrealized_pnl=float(payload.get("unrealized_pnl", 0.0)),
realized_pnl=float(payload.get("realized_pnl", 0.0)),
closed=bool(payload.get("closed", False)),
exit_leg_ratios=tuple(float(r) for r in payload.get("exit_leg_ratios", (1.0,))),
active_leg_index=int(payload.get("active_leg_index", 0)),
active_exit_order=active_exit_order,
active_entry_order=active_entry_order,
fsm_state=TradeStage(str(payload.get("fsm_state", TradeStage.IDLE.value))),
close_reason=str(payload.get("close_reason", "")),
last_event_time=datetime.fromisoformat(payload["last_event_time"]) if payload.get("last_event_time") else None,
seen_event_ids=tuple(str(event_id) for event_id in payload.get("seen_event_ids", ())),
metadata=dict(payload.get("metadata", {})),
)
return slot
def _encode_packet(seq: int, payload: Dict[str, Any]) -> bytes:
text = json.dumps(payload, sort_keys=True, ensure_ascii=False, default=_json_default, separators=(",", ":")).encode("utf-8")
return struct.pack("!QQ", int(seq), len(text)) + text
def _decode_packet(buf: memoryview) -> Dict[str, Any]:
if len(buf) < 16:
return {}
seq, size = struct.unpack_from("!QQ", buf, 0)
if size <= 0 or size > len(buf) - 16:
return {}
payload = bytes(buf[16 : 16 + size]).decode("utf-8")
out = json.loads(payload)
if isinstance(out, dict):
out["_seq"] = seq
return out
class RealZincPlane:
"""Shared-memory Zinc plane used by the Python prototype."""
def __init__(
self,
*,
prefix: str,
slot_count: int = 10,
intent_capacity: int = 1 << 20,
state_capacity: int = 1 << 20,
control_capacity: int = 1 << 20,
create: bool = True,
) -> None:
require_real_zinc()
base = prefix.strip("/").replace("/", "_")
self.intent_name = f"{base}_intent"
self.state_name = f"{base}_state"
self.control_name = f"{base}_control"
self._intent_seq = 0
self._state_seq = 0
self._control_seq = 0
self._lock = threading.Lock()
self._slot_cache: Dict[int, TradeSlot] = {i: TradeSlot(slot_id=i) for i in range(int(slot_count))}
self._slot_count = int(slot_count)
self._intent_cache: List[Dict[str, Any]] = []
self._control_cache = KernelControlSnapshot()
if create:
self.intent_region = SharedRegion.create(self.intent_name, intent_capacity)
self.state_region = SharedRegion.create(self.state_name, state_capacity)
self.control_region = SharedRegion.create(self.control_name, control_capacity)
self._write_region(self.control_region, self._control_seq, {"control": self._control_cache.as_dict()})
self._write_region(
self.state_region,
self._state_seq,
{"slots": [self._slot_cache[key].to_dict() for key in range(self._slot_count)]},
)
self._write_region(self.intent_region, self._intent_seq, {"items": []})
else:
self.intent_region = SharedRegion.open(self.intent_name)
self.state_region = SharedRegion.open(self.state_name)
self.control_region = SharedRegion.open(self.control_name)
control_payload = _decode_packet(self.control_region.as_buffer())
state_payload = _decode_packet(self.state_region.as_buffer())
intent_payload = _decode_packet(self.intent_region.as_buffer())
if isinstance(control_payload.get("control"), dict):
self._control_cache = KernelControlSnapshot(**control_payload["control"])
if isinstance(state_payload.get("slots"), list):
for slot_payload in state_payload["slots"]:
if isinstance(slot_payload, dict):
slot = _slot_from_payload(slot_payload)
self._slot_cache[int(slot.slot_id)] = slot
if isinstance(intent_payload.get("items"), list):
self._intent_cache = list(intent_payload["items"])
def close(self) -> None:
self.intent_region.close()
self.state_region.close()
self.control_region.close()
def publish_intent(self, intent: KernelIntent) -> None:
with self._lock:
self._intent_seq += 1
row = intent.__dict__.copy()
row["timestamp"] = intent.timestamp.isoformat()
row["side"] = intent.side.value
row["action"] = intent.action.value
row["stage"] = intent.stage.value
row["exit_leg_ratios"] = list(intent.exit_leg_ratios)
row["metadata"] = json.loads(json.dumps(intent.metadata, default=_json_default))
self._intent_cache.append(row)
self._write_region(self.intent_region, self._intent_seq, {"items": self._intent_cache[-512:]})
def write_slot(self, slot: TradeSlot) -> None:
with self._lock:
self._state_seq += 1
self._slot_cache[int(slot.slot_id)] = slot
payload = {
"slots": [self._slot_cache[key].to_dict() for key in range(self._slot_count)],
}
self._write_region(self.state_region, self._state_seq, payload)
def read_slots(self) -> List[TradeSlot]:
payload = _decode_packet(self.state_region.as_buffer())
slots = payload.get("slots", []) if isinstance(payload, dict) else []
return [_slot_from_payload(slot) for slot in sorted(slots, key=lambda row: int(row.get("slot_id", 0)))]
def read_intents(self) -> List[Dict[str, Any]]:
payload = _decode_packet(self.intent_region.as_buffer())
items = payload.get("items", []) if isinstance(payload, dict) else []
return list(items)
def update_control(self, control: KernelControlSnapshot) -> None:
with self._lock:
self._control_seq += 1
self._control_cache = control
self._write_region(self.control_region, self._control_seq, {"control": control.as_dict()})
def read_control(self) -> KernelControlSnapshot:
payload = _decode_packet(self.control_region.as_buffer())
control = payload.get("control") if isinstance(payload, dict) else None
if not isinstance(control, dict):
return self._control_cache
return KernelControlSnapshot(**control)
def wait_on_state(self, timeout_ms: int = 1000) -> bool:
return bool(self.state_region.wait(timeout_ms))
def notify_state(self) -> None:
self.state_region.notify()
def wait_on_control(self, timeout_ms: int = 1000) -> bool:
return bool(self.control_region.wait(timeout_ms))
def notify_control(self) -> None:
self.control_region.notify()
def wait_on_intent(self, timeout_ms: int = 1000) -> bool:
return bool(self.intent_region.wait(timeout_ms))
def notify_intent(self) -> None:
self.intent_region.notify()
def _write_region(self, region: Any, seq: int, payload: Dict[str, Any]) -> None:
packet = _encode_packet(seq, payload)
buf = region.as_buffer()
if len(packet) > len(buf):
raise ValueError(f"payload too large for Zinc region: {len(packet)} > {len(buf)}")
view = memoryview(buf)
view[:] = b"\x00" * len(view)
view[: len(packet)] = packet
region.notify()

View File

@@ -0,0 +1,683 @@
"""Rust-backed DITAv2 execution kernel.
This module keeps the Python API shape stable while moving the kernel state
machine into a Rust shared library. Slot views write through to the backend on
assignment, then the Python side mirrors the resulting state into Zinc and the
existing projections/journals.
"""
from __future__ import annotations
from dataclasses import asdict
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional, Sequence
import ctypes
import json
import os
import subprocess
import sys
from .account import AccountProjection
from .control import ControlPlane, ControlUpdate, KernelControlSnapshot, KernelVerbosity, build_control_plane
from .contracts import (
KernelCommandType,
KernelDiagnosticCode,
KernelEventKind,
KernelIntent,
KernelOutcome,
KernelSeverity,
KernelTransition,
TradeSide,
TradeSlot,
TradeStage,
VenueEvent,
VenueOrder,
VenueOrderStatus,
VenueEventStatus,
)
from .journal import KernelJournal, MemoryKernelJournal
from .mock_venue import MockVenueAdapter
from .projection import HazelcastProjection
from .projection import build_projection
from .utils import json_safe
from .venue import VenueAdapter
from .zinc_plane import InMemoryZincPlane, ZincPlane
def _repo_root() -> Path:
return Path(__file__).resolve().parents[3]
def _crate_dir() -> Path:
return Path(__file__).resolve().with_name("_rust_kernel")
def _library_path() -> Path:
if sys.platform == "darwin":
name = "libdita_v2_kernel.dylib"
elif os.name == "nt":
name = "dita_v2_kernel.dll"
else:
name = "libdita_v2_kernel.so"
return _crate_dir() / "target" / "release" / name
def _build_library() -> None:
crate_dir = _crate_dir()
if not crate_dir.exists():
raise FileNotFoundError(f"Missing Rust kernel crate: {crate_dir}")
subprocess.run(
["cargo", "build", "--release", "--manifest-path", str(crate_dir / "Cargo.toml")],
cwd=_repo_root(),
check=True,
)
def _ensure_library() -> Path:
path = _library_path()
if not path.exists():
_build_library()
return path
class _RustKernelLib:
def __init__(self) -> None:
path = _ensure_library()
self.lib = ctypes.CDLL(str(path))
self.lib.dita_kernel_create.argtypes = [ctypes.c_size_t]
self.lib.dita_kernel_create.restype = ctypes.c_void_p
self.lib.dita_kernel_destroy.argtypes = [ctypes.c_void_p]
self.lib.dita_kernel_destroy.restype = None
self.lib.dita_kernel_free_string.argtypes = [ctypes.c_void_p]
self.lib.dita_kernel_free_string.restype = None
self.lib.dita_kernel_get_slot_json.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
self.lib.dita_kernel_get_slot_json.restype = ctypes.c_void_p
self.lib.dita_kernel_set_slot_json.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_char_p]
self.lib.dita_kernel_set_slot_json.restype = ctypes.c_int
self.lib.dita_kernel_process_intent_json.argtypes = [
ctypes.c_void_p,
ctypes.c_char_p,
ctypes.c_char_p,
ctypes.c_char_p,
]
self.lib.dita_kernel_process_intent_json.restype = ctypes.c_void_p
self.lib.dita_kernel_on_venue_event_json.argtypes = [
ctypes.c_void_p,
ctypes.c_char_p,
ctypes.c_char_p,
ctypes.c_char_p,
]
self.lib.dita_kernel_on_venue_event_json.restype = ctypes.c_void_p
self.lib.dita_kernel_reconcile_slots_json.argtypes = [
ctypes.c_void_p,
ctypes.c_char_p,
ctypes.c_char_p,
ctypes.c_char_p,
]
self.lib.dita_kernel_reconcile_slots_json.restype = ctypes.c_void_p
self.lib.dita_kernel_snapshot_json.argtypes = [ctypes.c_void_p]
self.lib.dita_kernel_snapshot_json.restype = ctypes.c_void_p
def create(self, max_slots: int) -> ctypes.c_void_p:
handle = self.lib.dita_kernel_create(ctypes.c_size_t(max_slots))
if not handle:
raise RuntimeError("dita_kernel_create failed")
return ctypes.c_void_p(handle)
def destroy(self, handle: ctypes.c_void_p) -> None:
if handle and handle.value:
self.lib.dita_kernel_destroy(handle)
def _take_string(self, raw: ctypes.c_void_p) -> str:
if not raw:
raise RuntimeError("Rust kernel returned null string")
text = ctypes.cast(raw, ctypes.c_char_p).value
if text is None:
self.lib.dita_kernel_free_string(raw)
raise RuntimeError("Rust kernel returned empty string")
try:
return text.decode("utf-8")
finally:
self.lib.dita_kernel_free_string(raw)
def get_slot_json(self, handle: ctypes.c_void_p, slot_id: int) -> Dict[str, Any]:
raw = self.lib.dita_kernel_get_slot_json(handle, ctypes.c_size_t(slot_id))
if not raw:
raise IndexError(f"Invalid slot id: {slot_id}")
return json.loads(self._take_string(raw))
def set_slot_json(self, handle: ctypes.c_void_p, slot_id: int, payload: Dict[str, Any]) -> None:
encoded = json.dumps(json_safe(payload), separators=(",", ":"), ensure_ascii=False).encode("utf-8")
rc = self.lib.dita_kernel_set_slot_json(handle, ctypes.c_size_t(slot_id), ctypes.c_char_p(encoded))
if rc != 0:
raise RuntimeError(f"dita_kernel_set_slot_json failed rc={rc}")
def process_intent(
self,
handle: ctypes.c_void_p,
payload: Dict[str, Any],
*,
mode: str,
verbosity: str,
) -> Dict[str, Any]:
encoded = json.dumps(json_safe(payload), separators=(",", ":"), ensure_ascii=False).encode("utf-8")
raw = self.lib.dita_kernel_process_intent_json(
handle,
ctypes.c_char_p(encoded),
ctypes.c_char_p(mode.encode("utf-8")),
ctypes.c_char_p(verbosity.encode("utf-8")),
)
return json.loads(self._take_string(raw))
def on_venue_event(
self,
handle: ctypes.c_void_p,
payload: Dict[str, Any],
*,
mode: str,
verbosity: str,
) -> Dict[str, Any]:
encoded = json.dumps(json_safe(payload), separators=(",", ":"), ensure_ascii=False).encode("utf-8")
raw = self.lib.dita_kernel_on_venue_event_json(
handle,
ctypes.c_char_p(encoded),
ctypes.c_char_p(mode.encode("utf-8")),
ctypes.c_char_p(verbosity.encode("utf-8")),
)
return json.loads(self._take_string(raw))
def reconcile_slots(
self,
handle: ctypes.c_void_p,
payload: Sequence[Dict[str, Any]],
*,
mode: str,
verbosity: str,
) -> Dict[str, Any]:
encoded = json.dumps(json_safe(list(payload)), separators=(",", ":"), ensure_ascii=False).encode("utf-8")
raw = self.lib.dita_kernel_reconcile_slots_json(
handle,
ctypes.c_char_p(encoded),
ctypes.c_char_p(mode.encode("utf-8")),
ctypes.c_char_p(verbosity.encode("utf-8")),
)
return json.loads(self._take_string(raw))
def snapshot(self, handle: ctypes.c_void_p) -> Dict[str, Any]:
raw = self.lib.dita_kernel_snapshot_json(handle)
return json.loads(self._take_string(raw))
_RUST: _RustKernelLib | None = None # lazy init — avoids Rust build on import
def _get_rust() -> _RustKernelLib:
global _RUST
if _RUST is None:
_RUST = _RustKernelLib()
return _RUST
def _slot_to_payload(slot: TradeSlot) -> Dict[str, Any]:
return slot.to_dict()
def _order_to_payload(order: Optional[VenueOrder]) -> Optional[Dict[str, Any]]:
if order is None:
return None
return {
"internal_trade_id": order.internal_trade_id,
"venue_order_id": order.venue_order_id,
"venue_client_id": order.venue_client_id,
"side": order.side.value,
"intended_size": float(order.intended_size or 0.0),
"filled_size": float(order.filled_size or 0.0),
"average_fill_price": float(order.average_fill_price or 0.0),
"status": order.status.value,
"metadata": dict(order.metadata),
}
def _order_from_payload(payload: Optional[Dict[str, Any]], *, trade_id: str) -> Optional[VenueOrder]:
if not isinstance(payload, dict):
return None
return VenueOrder(
internal_trade_id=trade_id,
venue_order_id=str(payload.get("venue_order_id", "")),
venue_client_id=str(payload.get("venue_client_id", "")),
side=TradeSide(str(payload.get("side", TradeSide.FLAT.value))),
intended_size=float(payload.get("intended_size", 0.0)),
filled_size=float(payload.get("filled_size", 0.0)),
average_fill_price=float(payload.get("average_fill_price", 0.0)),
status=VenueOrderStatus(str(payload.get("status", VenueOrderStatus.NEW.value))),
metadata=dict(payload.get("metadata", {})),
)
def _slot_from_payload(payload: Dict[str, Any]) -> TradeSlot:
return TradeSlot(
slot_id=int(payload.get("slot_id", 0)),
trade_id=str(payload.get("trade_id", "")),
asset=str(payload.get("asset", "")),
side=TradeSide(str(payload.get("side", TradeSide.FLAT.value))),
entry_price=float(payload.get("entry_price", 0.0)),
size=float(payload.get("size", 0.0)),
initial_size=float(payload.get("initial_size", 0.0)),
leverage=float(payload.get("leverage", 0.0)),
entry_time=datetime.fromisoformat(payload["entry_time"]) if payload.get("entry_time") else None,
unrealized_pnl=float(payload.get("unrealized_pnl", 0.0)),
realized_pnl=float(payload.get("realized_pnl", 0.0)),
closed=bool(payload.get("closed", False)),
exit_leg_ratios=tuple(float(r) for r in payload.get("exit_leg_ratios", (1.0,))),
active_leg_index=int(payload.get("active_leg_index", 0)),
active_exit_order=_order_from_payload(payload.get("active_exit_order"), trade_id=str(payload.get("trade_id", ""))),
active_entry_order=_order_from_payload(payload.get("active_entry_order"), trade_id=str(payload.get("trade_id", ""))),
fsm_state=TradeStage(str(payload.get("fsm_state", TradeStage.IDLE.value))),
close_reason=str(payload.get("close_reason", "")),
last_event_time=datetime.fromisoformat(payload["last_event_time"]) if payload.get("last_event_time") else None,
seen_event_ids=tuple(str(event_id) for event_id in payload.get("seen_event_ids", ())),
metadata=dict(payload.get("metadata", {})),
)
def _intent_to_payload(intent: KernelIntent) -> Dict[str, Any]:
return {
"timestamp": intent.timestamp.isoformat() if hasattr(intent.timestamp, "isoformat") else str(intent.timestamp),
"intent_id": intent.intent_id,
"trade_id": intent.trade_id,
"slot_id": intent.slot_id,
"asset": intent.asset,
"side": intent.side.value,
"action": intent.action.value,
"reference_price": float(intent.reference_price or 0.0),
"target_size": float(intent.target_size or 0.0),
"leverage": float(intent.leverage or 0.0),
"exit_leg_ratios": list(intent.exit_leg_ratios),
"reason": intent.reason,
"metadata": dict(intent.metadata),
"stage": intent.stage.value,
}
def _event_to_payload(event: VenueEvent) -> Dict[str, Any]:
return {
"timestamp": event.timestamp.isoformat() if hasattr(event.timestamp, "isoformat") else str(event.timestamp),
"event_id": event.event_id,
"trade_id": event.trade_id,
"slot_id": event.slot_id,
"kind": event.kind.value,
"status": event.status.value,
"venue_order_id": event.venue_order_id,
"venue_client_id": event.venue_client_id,
"side": event.side.value,
"asset": event.asset,
"price": float(event.price or 0.0),
"size": float(event.size or 0.0),
"filled_size": float(event.filled_size or 0.0),
"remaining_size": float(event.remaining_size or 0.0),
"reason": event.reason,
"raw_payload": dict(event.raw_payload),
"metadata": dict(event.metadata),
}
def _transition_from_payload(payload: Dict[str, Any]) -> KernelTransition:
return KernelTransition(
timestamp=datetime.fromisoformat(payload["timestamp"]),
trade_id=str(payload.get("trade_id", "")),
slot_id=int(payload.get("slot_id", 0)),
prev_state=TradeStage(str(payload.get("prev_state", TradeStage.IDLE.value))),
next_state=TradeStage(str(payload.get("next_state", TradeStage.IDLE.value))),
trigger=str(payload.get("trigger", "")),
intent_id=str(payload.get("intent_id", "")),
event_id=str(payload.get("event_id", "")),
control_mode=str(payload.get("control_mode", "")),
control_verbosity=str(payload.get("control_verbosity", "")),
details=dict(payload.get("details", {})),
)
def _outcome_from_payload(payload: Dict[str, Any]) -> KernelOutcome:
return KernelOutcome(
accepted=bool(payload.get("accepted", False)),
slot_id=int(payload.get("slot_id", 0)),
trade_id=str(payload.get("trade_id", "")),
state=TradeStage(str(payload.get("state", TradeStage.IDLE.value))),
diagnostic_code=KernelDiagnosticCode(str(payload.get("diagnostic_code", KernelDiagnosticCode.OK.value))),
severity=KernelSeverity(str(payload.get("severity", KernelSeverity.INFO.value))),
transitions=tuple(_transition_from_payload(row) for row in payload.get("transitions", [])),
emitted_events=tuple(
VenueEvent(
timestamp=datetime.fromisoformat(row["timestamp"]),
event_id=str(row.get("event_id", "")),
trade_id=str(row.get("trade_id", "")),
slot_id=int(row.get("slot_id", 0)),
kind=KernelEventKind(str(row.get("kind", KernelEventKind.ORDER_ACK.value))),
status=VenueEventStatus(str(row.get("status", VenueEventStatus.ACKED.value))),
venue_order_id=str(row.get("venue_order_id", "")),
venue_client_id=str(row.get("venue_client_id", "")),
side=TradeSide(str(row.get("side", TradeSide.FLAT.value))),
asset=str(row.get("asset", "")),
price=float(row.get("price", 0.0)),
size=float(row.get("size", 0.0)),
filled_size=float(row.get("filled_size", 0.0)),
remaining_size=float(row.get("remaining_size", 0.0)),
reason=str(row.get("reason", "")),
raw_payload=dict(row.get("raw_payload", {})),
metadata=dict(row.get("metadata", {})),
)
for row in payload.get("emitted_events", [])
),
details=dict(payload.get("details", {})),
)
def _enum_text(value: Any) -> str:
if hasattr(value, "value"):
return str(getattr(value, "value"))
return str(value)
class KernelSlotView:
"""Write-through view over a Rust-backed slot."""
def __init__(self, kernel: "ExecutionKernel", slot_id: int) -> None:
object.__setattr__(self, "_kernel", kernel)
object.__setattr__(self, "_slot_id", int(slot_id))
@property
def slot_id(self) -> int:
return object.__getattribute__(self, "_slot_id")
def _snapshot(self) -> TradeSlot:
return self._kernel._get_slot(self.slot_id)
def __getattr__(self, name: str) -> Any:
slot = self._snapshot()
if hasattr(slot, name):
return getattr(slot, name)
raise AttributeError(name)
def __setattr__(self, name: str, value: Any) -> None:
if name in {"_kernel", "_slot_id"}:
object.__setattr__(self, name, value)
return
slot = self._snapshot()
if not hasattr(slot, name):
raise AttributeError(name)
setattr(slot, name, value)
self._kernel._set_slot(slot)
def to_dict(self) -> Dict[str, Any]:
return self._snapshot().to_dict()
def is_free(self) -> bool:
return self._snapshot().is_free()
def is_open(self) -> bool:
return self._snapshot().is_open()
def mark_price(self, price: float) -> None:
slot = self._snapshot()
slot.mark_price(price)
self._kernel._set_slot(slot)
def next_exit_ratio(self) -> float:
return self._snapshot().next_exit_ratio()
def consume_exit_leg(self) -> float:
slot = self._snapshot()
ratio = slot.consume_exit_leg()
self._kernel._set_slot(slot)
return ratio
def attach_entry_order(self, order: VenueOrder) -> None:
slot = self._snapshot()
slot.active_entry_order = order
self._kernel._set_slot(slot)
def attach_exit_order(self, order: VenueOrder) -> None:
slot = self._snapshot()
slot.active_exit_order = order
self._kernel._set_slot(slot)
def __repr__(self) -> str: # pragma: no cover - debugging helper
return f"KernelSlotView(slot_id={self.slot_id}, state={self._snapshot().fsm_state.value})"
class KernelStateView:
def __init__(self, kernel: "ExecutionKernel") -> None:
self._kernel = kernel
self.slots = [KernelSlotView(kernel, slot_id) for slot_id in range(kernel.max_slots)]
self.active_trade_index: Dict[str, int] = {}
self.venue_order_index: Dict[str, int] = {}
self.client_order_index: Dict[str, int] = {}
self.refresh()
def refresh(self) -> None:
snapshot = self._kernel._snapshot_backend()
self.active_trade_index = dict(snapshot.get("active_trade_index", {}))
self.venue_order_index = dict(snapshot.get("venue_order_index", {}))
self.client_order_index = dict(snapshot.get("client_order_index", {}))
class ExecutionKernel:
"""Rust-backed multi-slot execution kernel."""
def __init__(
self,
*,
max_slots: int = 10,
control_plane: Optional[ControlPlane] = None,
venue: Optional[VenueAdapter] = None,
journal: Optional[KernelJournal] = None,
account: Optional[AccountProjection] = None,
projection: Optional[HazelcastProjection] = None,
projection_client: Optional[Any] = None,
zinc_plane: Optional[ZincPlane] = None,
) -> None:
self.max_slots = int(max_slots)
self.control_plane = control_plane or build_control_plane()
self.venue = venue or MockVenueAdapter()
self.journal = journal or MemoryKernelJournal()
self.account = account or AccountProjection()
self.projection = projection or build_projection(client=projection_client)
self.zinc_plane = zinc_plane or InMemoryZincPlane()
self._backend = _get_rust().create(self.max_slots)
self._control_snapshot = self.control_plane.read()
self.projection.write_control(self._control_snapshot)
self.zinc_plane.update_control(self._control_snapshot)
self.state = KernelStateView(self)
self.account.observe_slots([self._get_slot(slot_id) for slot_id in range(self.max_slots)])
def __del__(self) -> None: # pragma: no cover - cleanup best effort
backend = getattr(self, "_backend", None)
if backend is not None:
try:
_get_rust().destroy(backend)
except Exception:
pass
@property
def control(self) -> KernelControlSnapshot:
return self.control_plane.read()
def update_control(self, update: ControlUpdate) -> KernelControlSnapshot:
snapshot = self.control_plane.update(update)
self._control_snapshot = snapshot
self.projection.write_control(snapshot)
self.zinc_plane.update_control(snapshot)
return snapshot
def _snapshot_backend(self) -> Dict[str, Any]:
return _get_rust().snapshot(self._backend)
def _get_slot(self, slot_id: int) -> TradeSlot:
return _slot_from_payload(_get_rust().get_slot_json(self._backend, slot_id))
def _set_slot(self, slot: TradeSlot, *, journal: bool = False) -> None:
payload = _slot_to_payload(slot)
_get_rust().set_slot_json(self._backend, slot.slot_id, payload)
self.state.refresh()
slots = [self._get_slot(slot_id) for slot_id in range(self.max_slots)]
self.account.observe_slots(slots)
current = self._get_slot(slot.slot_id)
self.projection.write_slot(current)
self.zinc_plane.write_slot(current)
def slot(self, slot_id: int) -> KernelSlotView:
if not (0 <= int(slot_id) < self.max_slots):
raise IndexError(slot_id)
return self.state.slots[int(slot_id)]
def free_slot(self) -> Optional[KernelSlotView]:
for slot in self.state.slots:
if slot.is_free():
return slot
return None
def _record_transitions(self, transitions: Iterable[KernelTransition], slot: TradeSlot, event: Optional[VenueEvent]) -> None:
if self.control.debug_clickhouse_enabled:
for transition in transitions:
self.journal.record_transition(
transition=transition,
slot=slot,
event=event,
control=self.control,
)
def process_intent(self, intent: KernelIntent) -> KernelOutcome:
self.zinc_plane.publish_intent(intent)
if not (0 <= int(intent.slot_id) < self.max_slots):
return KernelOutcome(
accepted=False,
slot_id=int(intent.slot_id),
trade_id=intent.trade_id,
state=TradeStage.IDLE,
diagnostic_code=KernelDiagnosticCode.INVALID_SLOT_ID,
details={"reason": "INVALID_SLOT_ID", "slot_id": int(intent.slot_id), "intent_id": intent.intent_id},
)
payload = _intent_to_payload(intent)
result = _get_rust().process_intent(
self._backend,
payload,
mode=_enum_text(self.control.mode),
verbosity=_enum_text(self.control.verbosity),
)
outcome = _outcome_from_payload(result["outcome"])
self.state.refresh()
emitted_events = []
if intent.action in {KernelCommandType.ENTER, KernelCommandType.EXIT}:
emitted_events = self.venue.submit(intent)
for event in emitted_events:
self.on_venue_event(event)
elif intent.action == KernelCommandType.CANCEL:
emitted_events = self.venue.cancel(self.slot(intent.slot_id).active_exit_order, reason=intent.reason) if self.slot(intent.slot_id).active_exit_order else []
for event in emitted_events:
self.on_venue_event(event)
final_slot = self._get_slot(outcome.slot_id)
rate_limit_event = next((event for event in emitted_events if event.kind == KernelEventKind.RATE_LIMITED), None)
if rate_limit_event is not None:
rate_limit_details = dict(outcome.details)
rate_limit_details.update(
{
"reason": rate_limit_event.reason or "RATE_LIMITED",
"retry_after_ms": int(rate_limit_event.metadata.get("retry_after_ms", 0) or 0),
"venue_event_kind": rate_limit_event.kind.value,
"severity": KernelSeverity.WARNING.value,
"release_eta": "few minutes",
"retryable": True,
}
)
outcome = KernelOutcome(
accepted=False,
slot_id=outcome.slot_id,
trade_id=outcome.trade_id,
state=final_slot.fsm_state,
diagnostic_code=KernelDiagnosticCode.RATE_LIMITED,
severity=KernelSeverity.WARNING,
transitions=outcome.transitions,
emitted_events=outcome.emitted_events,
details=rate_limit_details,
)
final_outcome = KernelOutcome(
accepted=outcome.accepted,
slot_id=outcome.slot_id,
trade_id=final_slot.trade_id,
state=final_slot.fsm_state,
diagnostic_code=outcome.diagnostic_code,
transitions=outcome.transitions,
emitted_events=tuple(emitted_events),
details=dict(outcome.details),
)
slots = [self._get_slot(i) for i in range(self.max_slots)]
self.account.observe_slots(slots)
current = self._get_slot(final_slot.slot_id)
self.projection.write_slot(current)
self.zinc_plane.write_slot(current)
self._record_transitions(outcome.transitions, final_slot, None)
return final_outcome
def on_venue_event(self, event: VenueEvent) -> KernelOutcome:
result = _get_rust().on_venue_event(
self._backend,
_event_to_payload(event),
mode=_enum_text(self.control.mode),
verbosity=_enum_text(self.control.verbosity),
)
outcome = _outcome_from_payload(result["outcome"])
slot = _slot_from_payload(result["slot"])
self.state.refresh()
# Single capital mutation point: settle realiized PnL when a fill
# transitions the slot to a terminal closed state. This is the *only*
# place post-startup where capital is changed — no external balance
# polls overwrite it.
if slot.fsm_state in {TradeStage.CLOSED, TradeStage.TRADE_TERMINAL_WRITTEN} and slot.realized_pnl != 0.0:
self.account.settle(slot.realized_pnl)
slots = [self._get_slot(i) for i in range(self.max_slots)]
self.account.observe_slots(slots)
current = self._get_slot(slot.slot_id)
self.projection.write_slot(current)
self.zinc_plane.write_slot(current)
self._record_transitions(outcome.transitions, slot, event)
return outcome
def mark_price(self, asset: str, price: float) -> None:
for slot in self.state.slots:
if slot.asset == asset and slot.is_open():
slot.mark_price(price)
self.account.observe_slots([self._get_slot(i) for i in range(self.max_slots)])
def reconcile_from_slots(self, slots: Sequence[TradeSlot]) -> KernelOutcome:
payload = [_slot_to_payload(slot) for slot in slots]
result = _get_rust().reconcile_slots(
self._backend,
payload,
mode=_enum_text(self.control.mode),
verbosity=_enum_text(self.control.verbosity),
)
outcome = _outcome_from_payload(result["outcome"])
self.state.refresh()
slots = [self._get_slot(i) for i in range(self.max_slots)]
self.account.observe_slots(slots)
for current in slots:
self.projection.write_slot(current)
self.zinc_plane.write_slot(current)
return outcome
def snapshot(self) -> Dict[str, Any]:
return {
"control": self.control.as_dict(),
"slots": [self._get_slot(slot.slot_id).to_dict() for slot in self.state.slots],
"account": {
"capital": self.account.snapshot.capital,
"equity": self.account.snapshot.equity,
"realized_pnl": self.account.snapshot.realized_pnl,
"unrealized_pnl": self.account.snapshot.unrealized_pnl,
"open_positions": self.account.snapshot.open_positions,
"open_notional": self.account.snapshot.open_notional,
"leverage": self.account.snapshot.leverage,
},
}

View File

@@ -0,0 +1,14 @@
[package]
name = "dita-v2-kernel"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
chrono = { version = "0.4", features = ["serde"] }
libc = "0.2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,43 @@
"""Utility helpers for the DITAv2 kernel."""
from __future__ import annotations
from dataclasses import asdict, is_dataclass
from datetime import datetime
from enum import Enum
from typing import Any
import json
import math
def safe_float(value: Any, default: float = 0.0) -> float:
"""Return a finite float or ``default``."""
try:
out = float(value)
except Exception:
return default
if not math.isfinite(out):
return default
return out
def json_safe(value: Any) -> Any:
"""Convert enums, dataclasses and datetimes to JSON-safe objects."""
if isinstance(value, Enum):
return value.value
if isinstance(value, datetime):
return value.isoformat()
if is_dataclass(value):
return json_safe(asdict(value))
if isinstance(value, dict):
return {str(key): json_safe(val) for key, val in value.items()}
if isinstance(value, list):
return [json_safe(item) for item in value]
if isinstance(value, tuple):
return [json_safe(item) for item in value]
return value
def json_text(value: Any) -> str:
"""Serialize a value using stable JSON settings."""
return json.dumps(json_safe(value), separators=(",", ":"), ensure_ascii=False, default=str)

View File

@@ -0,0 +1,37 @@
"""Venue adapter contracts for DITAv2."""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any, Dict, List, Optional, Protocol
from .contracts import (
KernelCommandType,
KernelIntent,
KernelEventKind,
TradeSide,
VenueEvent,
VenueEventStatus,
VenueOrder,
VenueOrderStatus,
)
class VenueAdapter(Protocol):
"""Abstract venue adapter used by the kernel."""
def submit(self, intent: KernelIntent) -> List[VenueEvent]:
...
def cancel(self, order: VenueOrder, *, reason: str = "") -> List[VenueEvent]:
...
def open_orders(self) -> List[VenueOrder]:
...
def open_positions(self) -> List[Dict[str, Any]]:
...
def reconcile(self) -> List[VenueEvent]:
...

View File

@@ -0,0 +1,135 @@
"""Python prototype of the Zinc hot-path plane.
This is an in-memory stand-in for the eventual Zinc-backed shared memory
regions. The interface is explicit so the implementation can be swapped later
without touching the kernel logic.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Dict, Iterable, List, Mapping, Optional, Protocol
import threading
import time
from .contracts import KernelIntent, TradeSlot
from .control import KernelControlSnapshot
class ZincPlane(Protocol):
"""Hot-path plane for intents, state and control."""
def publish_intent(self, intent: KernelIntent) -> None:
...
def write_slot(self, slot: TradeSlot) -> None:
...
def read_slots(self) -> List[TradeSlot]:
...
def update_control(self, control: KernelControlSnapshot) -> None:
...
def read_control(self) -> KernelControlSnapshot:
...
def wait_on_intent(self, timeout_ms: int = 1000) -> bool:
...
def notify_intent(self) -> None:
...
def wait_on_state(self, timeout_ms: int = 1000) -> bool:
...
def notify_state(self) -> None:
...
def wait_on_control(self, timeout_ms: int = 1000) -> bool:
...
def notify_control(self) -> None:
...
@dataclass
class InMemoryZincPlane:
"""Simple in-memory Zinc lookalike for Python prototype tests."""
intent_region: List[KernelIntent] = field(default_factory=list)
state_region: Dict[int, TradeSlot] = field(default_factory=dict)
control_region: Optional[KernelControlSnapshot] = None
_intent_seq: int = field(default=0, init=False, repr=False)
_state_seq: int = field(default=0, init=False, repr=False)
_control_seq: int = field(default=0, init=False, repr=False)
_intent_observed_seq: int = field(default=0, init=False, repr=False)
_state_observed_seq: int = field(default=0, init=False, repr=False)
_control_observed_seq: int = field(default=0, init=False, repr=False)
_signal: threading.Condition = field(default_factory=threading.Condition, init=False, repr=False)
def publish_intent(self, intent: KernelIntent) -> None:
with self._signal:
self.intent_region.append(intent)
self._intent_seq += 1
self._signal.notify_all()
def write_slot(self, slot: TradeSlot) -> None:
with self._signal:
self.state_region[int(slot.slot_id)] = slot
self._state_seq += 1
self._signal.notify_all()
def read_slots(self) -> List[TradeSlot]:
return [self.state_region[key] for key in sorted(self.state_region)]
def update_control(self, control: KernelControlSnapshot) -> None:
with self._signal:
self.control_region = control
self._control_seq += 1
self._signal.notify_all()
def read_control(self) -> KernelControlSnapshot:
if self.control_region is None:
return KernelControlSnapshot()
return self.control_region
def wait_on_intent(self, timeout_ms: int = 1000) -> bool:
return self._wait_for_change("_intent_seq", "_intent_observed_seq", timeout_ms)
def notify_intent(self) -> None:
with self._signal:
self._intent_seq += 1
self._signal.notify_all()
def wait_on_state(self, timeout_ms: int = 1000) -> bool:
return self._wait_for_change("_state_seq", "_state_observed_seq", timeout_ms)
def notify_state(self) -> None:
with self._signal:
self._state_seq += 1
self._signal.notify_all()
def wait_on_control(self, timeout_ms: int = 1000) -> bool:
return self._wait_for_change("_control_seq", "_control_observed_seq", timeout_ms)
def notify_control(self) -> None:
with self._signal:
self._control_seq += 1
self._signal.notify_all()
def _wait_for_change(self, seq_attr: str, observed_attr: str, timeout_ms: int) -> bool:
timeout_s = None if timeout_ms is None or timeout_ms < 0 else max(0.0, timeout_ms / 1000.0)
deadline = None if timeout_s is None else time.monotonic() + timeout_s
with self._signal:
observed = getattr(self, observed_attr)
while getattr(self, seq_attr) == observed:
if deadline is None:
self._signal.wait()
continue
remaining = deadline - time.monotonic()
if remaining <= 0:
return False
self._signal.wait(timeout=remaining)
setattr(self, observed_attr, getattr(self, seq_attr))
return True