repo hygiene: track the PINK launcher import closure

67 production .py modules that the running PINK service imports but which
were never committed: prod/bingx/ (HTTP client, market/user streams,
journal, config), prod/clean_arch/ adapters/persistence/runtime/dita/dita_v2
production modules and their co-located tests. Rule going forward: every
module imported by launch_dolphin_pink.py / pink_direct.py must appear in
git ls-files. Excludes _backup dirs, __pycache__, and non-code files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Codex
2026-06-12 15:09:32 +02:00
parent c3a18f693a
commit 84e4a50e3f
67 changed files with 15090 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,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,563 @@
"""DITAv2 Execution Router — the execution-style layer (SOR seed).
Decides HOW an intent reaches the venue (taker MARKET vs post-only maker
LIMIT, quote price, TTL, miss policy) — never WHETHER (that is the alpha
layer's job and is not touched here). This module is the abstraction the
S3 "Smart Order Router" TODO in ``adapters/bingx_direct.py`` calls for:
``submit`` paths stay thin; policy lives here; future improvements (OBF
depth gating, price-impact models, TWAP/iceberg) plug in via hooks.
Design rules (DO NOT WEAKEN):
1. Exits are NEVER skipped. A maker exit that misses its TTL is always
escalated to MARKET. Only entries may be skipped.
2. One working order per slot. While an entry (or exit) quote is
working, duplicate ENTER (or same-urgency EXIT) intents are
suppressed — this is the double-entry guard. An *urgent* exit
always preempts a working maker exit (cancel + MARKET).
3. Bounded retries. ``entry_retries`` re-quotes maximum, then the
configured exhaust action (skip|market). No unbounded loops.
4. Pure policy. This module does no I/O, no asyncio, no venue calls —
the runtime drives cancels/submits. That is what makes it testable
"to heavens and high back".
5. Default config == legacy behavior (pure taker). With
``DOLPHIN_PINK_EXEC_STYLE`` unset every plan is MARKET and the
registry stays empty.
Hook points (each receives ``(plan_or_event, ctx)`` and may return a
replacement ``ExecutionPlan`` from ``pre_submit``; exceptions are isolated
and logged, never propagated to the trading path):
- ``pre_plan`` — observe/adjust planning inputs
- ``pre_submit`` — last-look mutation of the plan (e.g. depth gate)
- ``on_working`` — a maker quote was registered as working
- ``on_fill`` — a working quote filled (or immediate fill)
- ``on_miss`` — a working entry expired and a miss action was taken
- ``on_escalate`` — a working exit expired / was preempted to MARKET
- ``on_cancel`` — a working quote was cancelled
Configuration (env, parsed once by ``ExecConfig.from_env()``):
DOLPHIN_PINK_EXEC_STYLE taker|maker_entry|maker_exit|maker_both [taker]
DOLPHIN_PINK_MAKER_ENTRY_TTL_S float seconds quote lifetime [8.0]
DOLPHIN_PINK_MAKER_EXIT_TTL_S float seconds quote lifetime [5.0]
DOLPHIN_PINK_MAKER_ENTRY_MISS skip|retry|market [skip]
DOLPHIN_PINK_MAKER_ENTRY_RETRIES int max re-quotes when MISS=retry [1]
DOLPHIN_PINK_MAKER_RETRY_EXHAUST skip|market after retries spent [skip]
DOLPHIN_PINK_MAKER_OFFSET_TICKS int quote distance from reference [1]
DOLPHIN_PINK_MAKER_MAX_SPREAD_BPS float; spread wider than this → taker [5.0]
DOLPHIN_PINK_POST_ONLY 0|1 send PostOnly TIF on maker quotes [1]
DOLPHIN_PINK_TICK_SIZE_<SYMBOL> per-symbol tick override (e.g. _BTCUSDT)
Maker-eligible exit reasons: TAKE_PROFIT only. CATASTROPHIC_LOSS,
MAX_HOLD, MEAN_REVERSION and anything unrecognised are urgent → MARKET.
"""
from __future__ import annotations
import logging
import os
import time
from dataclasses import dataclass, field, replace
from typing import Any, Callable, Dict, List, Optional, Tuple
LOGGER = logging.getLogger("dita_v2.exec_router")
# Exit reasons that tolerate a resting reduce-only quote. Everything else
# (stops, max-hold, mean-reversion flips, reconcile-driven closes) demands
# immediacy and is executed as taker MARKET regardless of style.
MAKER_EXIT_REASONS = frozenset({"TAKE_PROFIT"})
VALID_STYLES = ("taker", "maker_entry", "maker_exit", "maker_both")
VALID_MISS = ("skip", "retry", "market")
VALID_EXHAUST = ("skip", "market")
HOOK_STAGES = (
"pre_plan", "pre_submit", "on_working", "on_fill",
"on_miss", "on_escalate", "on_cancel",
)
# Tick sizes from the BingX characterization sweep
# (prod/docs/BingX_FILL_CHARACTERIZATION_AND_ADVANTAGES.md §Precision).
DEFAULT_TICKS: Dict[str, float] = {
"BTCUSDT": 0.1,
"ETHUSDT": 0.01,
"AAVEUSDT": 0.01,
"SOLUSDT": 0.001,
"XRPUSDT": 0.0001,
"DOGEUSDT": 0.00001,
"SHIBUSDT": 1e-9,
"YFIUSDT": 0.01,
"XAUTUSDT": 0.1,
"ADAUSDT": 0.0001,
"TRXUSDT": 0.00001,
"ALGOUSDT": 0.0001,
}
_FALLBACK_TICK_FRACTION = 1e-5 # unknown symbol: ~0.1 bp of price
def _env_float(name: str, default: float, lo: float, hi: float) -> float:
raw = os.environ.get(name)
if raw is None or not str(raw).strip():
return default
try:
val = float(str(raw).strip())
except Exception:
LOGGER.warning("exec_router: bad %s=%r — using default %s", name, raw, default)
return default
if not (lo <= val <= hi):
clamped = min(max(val, lo), hi)
LOGGER.warning("exec_router: %s=%s outside [%s, %s] — clamped to %s",
name, val, lo, hi, clamped)
return clamped
return val
def _env_int(name: str, default: int, lo: int, hi: int) -> int:
return int(_env_float(name, float(default), float(lo), float(hi)))
def _env_choice(name: str, default: str, choices: Tuple[str, ...]) -> str:
raw = str(os.environ.get(name, default) or default).strip().lower()
if raw not in choices:
LOGGER.warning("exec_router: %s=%r not in %s — using %r", name, raw, choices, default)
return default
return raw
def _env_bool(name: str, default: bool) -> bool:
raw = os.environ.get(name)
if raw is None or not str(raw).strip():
return default
return str(raw).strip().lower() in ("1", "true", "yes", "on")
@dataclass(frozen=True)
class ExecConfig:
"""Validated execution-policy configuration. Frozen: build once at boot."""
style: str = "taker"
entry_ttl_s: float = 8.0
exit_ttl_s: float = 5.0
entry_miss: str = "skip"
entry_retries: int = 1
retry_exhaust: str = "skip"
offset_ticks: int = 1
max_spread_bps: float = 5.0
post_only: bool = True
tick_overrides: Dict[str, float] = field(default_factory=dict)
@property
def maker_entry(self) -> bool:
return self.style in ("maker_entry", "maker_both")
@property
def maker_exit(self) -> bool:
return self.style in ("maker_exit", "maker_both")
@classmethod
def from_env(cls) -> "ExecConfig":
ticks: Dict[str, float] = {}
for key, raw in os.environ.items():
if key.startswith("DOLPHIN_PINK_TICK_SIZE_"):
sym = key[len("DOLPHIN_PINK_TICK_SIZE_"):].upper()
try:
val = float(raw)
if val > 0:
ticks[sym] = val
except Exception:
LOGGER.warning("exec_router: bad tick override %s=%r", key, raw)
return cls(
style=_env_choice("DOLPHIN_PINK_EXEC_STYLE", "taker", VALID_STYLES),
entry_ttl_s=_env_float("DOLPHIN_PINK_MAKER_ENTRY_TTL_S", 8.0, 0.5, 300.0),
exit_ttl_s=_env_float("DOLPHIN_PINK_MAKER_EXIT_TTL_S", 5.0, 0.5, 300.0),
entry_miss=_env_choice("DOLPHIN_PINK_MAKER_ENTRY_MISS", "skip", VALID_MISS),
entry_retries=_env_int("DOLPHIN_PINK_MAKER_ENTRY_RETRIES", 1, 0, 10),
retry_exhaust=_env_choice("DOLPHIN_PINK_MAKER_RETRY_EXHAUST", "skip", VALID_EXHAUST),
offset_ticks=_env_int("DOLPHIN_PINK_MAKER_OFFSET_TICKS", 1, 0, 100),
max_spread_bps=_env_float("DOLPHIN_PINK_MAKER_MAX_SPREAD_BPS", 5.0, 0.0, 1000.0),
post_only=_env_bool("DOLPHIN_PINK_POST_ONLY", True),
tick_overrides=ticks,
)
@dataclass(frozen=True)
class ExecutionPlan:
"""How one intent should be executed. Produced by the router, consumed
by the runtime, forwarded to the venue via KernelIntent fields/metadata."""
order_type: str = "MARKET" # "MARKET" | "LIMIT"
limit_price: float = 0.0
post_only: bool = False
ttl_s: float = 0.0 # 0 = no TTL management (taker)
is_maker: bool = False
action: str = "ENTER" # "ENTER" | "EXIT"
reason: str = "taker_default" # provenance for logs/persistence
suppress: bool = False # True → do not submit (dup guard)
metadata: Dict[str, Any] = field(default_factory=dict)
def sane(self) -> bool:
if self.order_type not in ("MARKET", "LIMIT"):
return False
if self.order_type == "LIMIT" and not (self.limit_price > 0.0):
return False
return True
@dataclass
class WorkingOrder:
"""Runtime-registered maker quote awaiting fill or TTL."""
trade_id: str
asset: str
side: str # "SHORT" | "LONG" (position side)
action: str # "ENTER" | "EXIT"
plan: ExecutionPlan
submitted_at: float # monotonic clock
deadline: float
retries_left: int
base_trade_id: str # original id before retry suffixes
retry_n: int = 0
class MissAction:
SKIP = "skip"
RETRY = "retry"
MARKET = "market"
class ExecutionRouter:
"""Pure-policy execution router with a working-order registry.
The runtime asks ``plan_entry``/``plan_exit`` before each kernel
submission, registers maker quotes via ``register_working``, polls
``expired`` from its TTL loop, and reports outcomes back via
``note_fill``/``note_cancel``. All venue I/O stays in the runtime.
"""
def __init__(self, config: Optional[ExecConfig] = None, *,
logger: Any = LOGGER, clock: Callable[[], float] = time.monotonic):
self.config = config or ExecConfig()
self.logger = logger
self.clock = clock
self._working: Dict[str, WorkingOrder] = {} # trade_id → WorkingOrder
self._hooks: Dict[str, List[Callable]] = {s: [] for s in HOOK_STAGES}
self.counters: Dict[str, int] = {
"plans_entry": 0, "plans_exit": 0,
"maker_entries": 0, "maker_exits": 0,
"taker_entries": 0, "taker_exits": 0,
"suppressed_dup_enter": 0, "suppressed_dup_exit": 0,
"spread_gate_taker": 0,
"entry_miss_skip": 0, "entry_miss_retry": 0, "entry_miss_market": 0,
"exit_escalations": 0, "fills_working": 0, "cancels": 0,
"hook_errors": 0,
}
# ── hooks ────────────────────────────────────────────────────────────────
def register_hook(self, stage: str, fn: Callable) -> Callable[[], None]:
"""Register ``fn`` at ``stage``; returns an unregister callable."""
if stage not in self._hooks:
raise ValueError(f"unknown hook stage {stage!r}; valid: {HOOK_STAGES}")
self._hooks[stage].append(fn)
def _unregister() -> None:
try:
self._hooks[stage].remove(fn)
except ValueError:
pass
return _unregister
def _run_hooks(self, stage: str, payload: Any, ctx: Dict[str, Any]) -> Any:
"""Run hooks; a ``pre_submit`` hook may return a replacement plan.
Hook exceptions are isolated — the trading path must never die in
a plugin."""
out = payload
for fn in list(self._hooks.get(stage, ())):
try:
ret = fn(out, dict(ctx))
if stage == "pre_submit" and isinstance(ret, ExecutionPlan):
if ret.sane():
out = ret
else:
self.logger.warning(
"exec_router: hook %r returned insane plan — ignored", fn)
except Exception as exc:
self.counters["hook_errors"] += 1
self.logger.warning("exec_router: hook %r failed at %s: %s", fn, stage, exc)
return out
# ── pricing ──────────────────────────────────────────────────────────────
def tick_size(self, asset: str) -> float:
sym = str(asset or "").upper()
if sym in self.config.tick_overrides:
return self.config.tick_overrides[sym]
return DEFAULT_TICKS.get(sym, 0.0)
def maker_price(self, *, asset: str, order_side: str, reference_price: float) -> float:
"""Quote price that rests on the book on our side of the touch.
``order_side`` is the ORDER side ("SELL"/"BUY"), not the position
side. SELL rests at/above reference; BUY rests at/below. Post-only
rejects any residual cross, so quoting at the touch is safe.
"""
ref = float(reference_price)
if not (ref > 0.0):
return 0.0
tick = self.tick_size(asset)
if tick <= 0.0:
tick = ref * _FALLBACK_TICK_FRACTION
off = self.config.offset_ticks * tick
if str(order_side).upper() == "SELL":
return ref + off
return max(tick, ref - off)
@staticmethod
def order_side(action: str, position_side: str) -> str:
"""Map (action, position side) → order side, mirroring the adapter."""
pos = str(position_side).upper()
if str(action).upper() == "EXIT":
return "SELL" if pos == "LONG" else "BUY"
return "BUY" if pos == "LONG" else "SELL"
# ── planning ─────────────────────────────────────────────────────────────
def _spread_allows_maker(self, spread_bps: Optional[float]) -> bool:
if spread_bps is None:
return True # no OBF data — quote anyway; post-only caps the risk
return float(spread_bps) <= self.config.max_spread_bps
def plan_entry(self, *, trade_id: str, asset: str, position_side: str,
reference_price: float,
spread_bps: Optional[float] = None) -> ExecutionPlan:
"""Plan an ENTER execution. Never raises; falls back to MARKET."""
self.counters["plans_entry"] += 1
ctx = {"trade_id": trade_id, "asset": asset, "side": position_side,
"reference_price": reference_price, "spread_bps": spread_bps,
"action": "ENTER"}
self._run_hooks("pre_plan", None, ctx)
# Double-entry guard: a working entry means the slot is spoken for.
for wo in self._working.values():
if wo.action == "ENTER":
self.counters["suppressed_dup_enter"] += 1
return ExecutionPlan(action="ENTER", suppress=True,
reason=f"working_entry_exists:{wo.trade_id}")
plan = ExecutionPlan(action="ENTER", reason="taker_default")
if self.config.maker_entry and reference_price > 0.0:
if not self._spread_allows_maker(spread_bps):
self.counters["spread_gate_taker"] += 1
plan = ExecutionPlan(action="ENTER",
reason=f"spread_gate:{spread_bps}bps")
else:
side = self.order_side("ENTER", position_side)
px = self.maker_price(asset=asset, order_side=side,
reference_price=reference_price)
if px > 0.0:
plan = ExecutionPlan(
order_type="LIMIT", limit_price=px,
post_only=self.config.post_only,
ttl_s=self.config.entry_ttl_s, is_maker=True,
action="ENTER", reason="maker_entry",
)
plan = self._run_hooks("pre_submit", plan, ctx)
if plan.is_maker:
self.counters["maker_entries"] += 1
elif not plan.suppress:
self.counters["taker_entries"] += 1
return plan
def plan_exit(self, *, trade_id: str, asset: str, position_side: str,
reference_price: float, reason: str,
spread_bps: Optional[float] = None) -> ExecutionPlan:
"""Plan an EXIT execution.
RULE 1: exits are never skipped. A non-maker-eligible reason, a bad
reference price, or a wide spread all degrade to MARKET — never to
suppression, except the duplicate-guard case where a maker exit for
the SAME trade is already working and the new reason is equally
non-urgent (the resting quote IS the exit in flight).
"""
self.counters["plans_exit"] += 1
urgent = str(reason or "").upper() not in MAKER_EXIT_REASONS
ctx = {"trade_id": trade_id, "asset": asset, "side": position_side,
"reference_price": reference_price, "spread_bps": spread_bps,
"action": "EXIT", "reason": reason, "urgent": urgent}
self._run_hooks("pre_plan", None, ctx)
wo = self._working.get(trade_id)
if wo is not None and wo.action == "EXIT":
if not urgent:
# Same-trade maker exit already resting → nothing to add.
self.counters["suppressed_dup_exit"] += 1
return ExecutionPlan(action="EXIT", suppress=True,
reason="working_exit_exists")
# Urgent reason preempts the resting quote: runtime must cancel
# the working order, then submit this MARKET plan.
self.counters["exit_escalations"] += 1
plan = ExecutionPlan(action="EXIT", reason=f"escalate:{reason}",
metadata={"preempt_working": True})
return self._run_hooks("pre_submit", plan, ctx)
plan = ExecutionPlan(action="EXIT", reason=f"taker_exit:{reason}")
if (self.config.maker_exit and not urgent and reference_price > 0.0
and self._spread_allows_maker(spread_bps)):
side = self.order_side("EXIT", position_side)
px = self.maker_price(asset=asset, order_side=side,
reference_price=reference_price)
if px > 0.0:
plan = ExecutionPlan(
order_type="LIMIT", limit_price=px,
post_only=self.config.post_only,
ttl_s=self.config.exit_ttl_s, is_maker=True,
action="EXIT", reason="maker_exit:TAKE_PROFIT",
)
plan = self._run_hooks("pre_submit", plan, ctx)
if plan.suppress:
# RULE 1 enforcement against plugins: only the dup-guard branches
# above may suppress an exit; a hook returning suppress is
# overridden to MARKET so a position can never be stranded.
plan = ExecutionPlan(action="EXIT", reason="hook_suppress_overridden_market")
if not plan.sane():
# Hard floor: an exit must reach the venue. Insane plan → MARKET.
plan = ExecutionPlan(action="EXIT", reason="sanity_fallback_market")
if plan.is_maker:
self.counters["maker_exits"] += 1
elif not plan.suppress:
self.counters["taker_exits"] += 1
return plan
# ── working-order registry ───────────────────────────────────────────────
def register_working(self, *, trade_id: str, asset: str, position_side: str,
plan: ExecutionPlan,
base_trade_id: Optional[str] = None,
retry_n: int = 0) -> WorkingOrder:
now = self.clock()
wo = WorkingOrder(
trade_id=trade_id, asset=asset, side=str(position_side).upper(),
action=plan.action, plan=plan, submitted_at=now,
deadline=now + max(0.5, plan.ttl_s),
retries_left=self.config.entry_retries if plan.action == "ENTER" else 0,
base_trade_id=base_trade_id or trade_id, retry_n=retry_n,
)
if retry_n > 0:
wo.retries_left = max(0, self.config.entry_retries - retry_n)
self._working[trade_id] = wo
self._run_hooks("on_working", wo, {"trade_id": trade_id})
return wo
def working(self, trade_id: str) -> Optional[WorkingOrder]:
return self._working.get(trade_id)
def working_orders(self) -> List[WorkingOrder]:
return list(self._working.values())
def has_working_entry(self) -> bool:
return any(wo.action == "ENTER" for wo in self._working.values())
def expired(self, now: Optional[float] = None) -> List[WorkingOrder]:
t = self.clock() if now is None else now
return [wo for wo in self._working.values() if t >= wo.deadline]
def note_fill(self, trade_id: str) -> None:
wo = self._working.pop(trade_id, None)
if wo is not None:
self.counters["fills_working"] += 1
self._run_hooks("on_fill", wo, {"trade_id": trade_id})
def note_cancel(self, trade_id: str) -> None:
wo = self._working.pop(trade_id, None)
if wo is not None:
self.counters["cancels"] += 1
self._run_hooks("on_cancel", wo, {"trade_id": trade_id})
def clear_working(self, trade_id: str) -> None:
self._working.pop(trade_id, None)
# ── miss / escalation policy ─────────────────────────────────────────────
def entry_miss_action(self, wo: WorkingOrder) -> str:
"""Decide what to do with an expired working ENTRY (after the runtime
has cancelled the quote). Returns a ``MissAction``.
retry policy: up to ``entry_retries`` fresh quotes, then
``retry_exhaust`` (skip|market). ``entry_miss`` skip|market apply
immediately with no re-quote.
"""
mode = self.config.entry_miss
if mode == "skip":
self.counters["entry_miss_skip"] += 1
action = MissAction.SKIP
elif mode == "market":
self.counters["entry_miss_market"] += 1
action = MissAction.MARKET
else: # retry
if wo.retries_left > 0:
self.counters["entry_miss_retry"] += 1
action = MissAction.RETRY
elif self.config.retry_exhaust == "market":
self.counters["entry_miss_market"] += 1
action = MissAction.MARKET
else:
self.counters["entry_miss_skip"] += 1
action = MissAction.SKIP
self._run_hooks("on_miss", wo, {"action": action})
return action
def retry_plan(self, wo: WorkingOrder, *, reference_price: float) -> Tuple[str, ExecutionPlan]:
"""Fresh quote for a retried entry. Returns (new_trade_id, plan).
New trade_id guarantees clientOrderId uniqueness on the venue and a
clean kernel FSM lifecycle for the re-quote."""
n = wo.retry_n + 1
new_tid = f"{wo.base_trade_id}-r{n}"
side = self.order_side("ENTER", wo.side)
px = self.maker_price(asset=wo.asset, order_side=side,
reference_price=reference_price)
plan = ExecutionPlan(
order_type="LIMIT", limit_price=px,
post_only=self.config.post_only,
ttl_s=self.config.entry_ttl_s, is_maker=True,
action="ENTER", reason=f"maker_entry_retry_{n}",
metadata={"retry_n": n, "base_trade_id": wo.base_trade_id},
)
if not plan.sane():
plan = ExecutionPlan(action="ENTER", reason="retry_price_insane_market",
metadata={"retry_n": n, "base_trade_id": wo.base_trade_id})
return new_tid, plan
def market_fallback_plan(self, wo: WorkingOrder) -> Tuple[str, ExecutionPlan]:
"""MARKET fallback after a missed/escalated quote.
ENTER: fresh trade_id (``-m`` suffix) — the cancelled quote's
lifecycle is closed; the fallback is a new order.
EXIT: SAME trade_id — the exit must stay attached to the open
position's lifecycle in the kernel FSM.
"""
if wo.action == "ENTER":
new_tid = f"{wo.base_trade_id}-m"
self._run_hooks("on_escalate", wo, {"to": "MARKET"})
return new_tid, ExecutionPlan(
action="ENTER", reason="entry_miss_market_fallback",
metadata={"base_trade_id": wo.base_trade_id})
self.counters["exit_escalations"] += 1
self._run_hooks("on_escalate", wo, {"to": "MARKET"})
return wo.trade_id, ExecutionPlan(
action="EXIT", reason="exit_ttl_market_fallback",
metadata={"base_trade_id": wo.base_trade_id})
# ── observability ────────────────────────────────────────────────────────
def snapshot(self) -> Dict[str, Any]:
return {
"style": self.config.style,
"working": [
{"trade_id": w.trade_id, "action": w.action, "asset": w.asset,
"age_s": round(self.clock() - w.submitted_at, 3),
"retry_n": w.retry_n}
for w in self._working.values()
],
"counters": dict(self.counters),
}

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,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,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.append(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.append(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,378 @@
"""BLUE-parity restoration tests (2026-06-10).
The DITAv2 rewrite (Sprint 0) dropped two things the original PINK
(full-engine launch_dolphin_bingx) had from the start:
R1 IRP asset selection over the scan universe — PINK traded only the
snapshot anchor (BTCUSDT) since the rewrite.
R2 Cubic-convex dynamic leverage — the stub formula's confidence
(|vdiv/threshold|) is ≥ 1.0 on every possible ENTER, so leverage was
pinned at max_leverage (3.0) flat, and exchange leverage at the cap.
These tests pin the restored behavior:
- blue_parity.PinkAssetPicker / PinkAlphaSizer (wrappers over BLUE's
exact kernels)
- DecisionEngine sizer injection (and legacy path preserved verbatim)
- IntentEngine honoring decision sizing
- dual-leverage conviction map at the venue boundary
- PinkDirectRuntime._effective_snapshot retargeting rules
"""
from __future__ import annotations
import logging
from collections import deque
from datetime import datetime, timezone
from types import SimpleNamespace
import pytest
from prod.clean_arch.dita.contracts import DecisionAction, DecisionConfig, DecisionContext, IntentContext
from prod.clean_arch.dita.decision import DecisionEngine
from prod.clean_arch.dita.intent import IntentEngine
from prod.clean_arch.dita_v2.blue_parity import PinkAlphaSizer, PinkAssetPicker
from prod.clean_arch.ports.data_feed import MarketSnapshot
from prod.clean_arch.runtime.pink_direct import PinkDirectRuntime
from prod.bingx.leverage import map_internal_conviction_to_exchange_leverage
LOGGER = logging.getLogger("test_blue_parity")
def make_snapshot(symbol="BTCUSDT", price=50000.0, vdiv=-0.03, irp=0.60,
scan_number=100, payload=None):
return MarketSnapshot(
timestamp=datetime.now(timezone.utc),
symbol=symbol,
price=price,
eigenvalues=[1.0],
velocity_divergence=vdiv,
irp_alignment=irp,
scan_number=scan_number,
scan_payload=payload if payload is not None else {"vol_ok": True},
)
def make_config(max_leverage=8.0):
return DecisionConfig(
vel_div_threshold=-0.02,
vel_div_extreme=-0.05,
fixed_tp_pct=0.0020,
max_hold_bars=250,
capital_fraction=0.20,
max_leverage=max_leverage,
allow_short=True,
allow_long=False,
)
def feed_picker(picker, series: dict, start_scan=1):
"""series: asset → list of prices; all lists same length."""
n = len(next(iter(series.values())))
for i in range(n):
payload = {
"assets": list(series),
"asset_prices": [series[a][i] for a in series],
}
picker.observe(payload, scan_number=start_scan + i)
def trending_series(lookback, down=0.997, up=1.003):
n = lookback + 2
out = {"DOWNUSDT": [], "UPUSDT": []}
d, u = 100.0, 100.0
for _ in range(n):
d *= down
u *= up
out["DOWNUSDT"].append(d)
out["UPUSDT"].append(u)
return out
# ── R2: sizer ─────────────────────────────────────────────────────────────────
class TestPinkAlphaSizer:
def _sizer(self, **kw):
defaults = dict(min_leverage=0.5, max_leverage=8.0, leverage_convexity=3.0,
vel_div_threshold=-0.02, vel_div_extreme=-0.05,
use_dynamic_leverage=True, use_alpha_layers=False)
defaults.update(kw)
return PinkAlphaSizer(**defaults)
def test_cubic_curve_checkpoints(self):
s = self._sizer()
# At threshold: strength 0 → min leverage
assert s.calculate_size(capital=1e5, vel_div=-0.02)["leverage"] == pytest.approx(0.5)
# Midpoint: strength 0.5 → 0.5 + 0.125 × 7.5 = 1.4375
assert s.calculate_size(capital=1e5, vel_div=-0.035)["leverage"] == pytest.approx(1.4375)
# At/beyond extreme: strength 1 → max leverage
assert s.calculate_size(capital=1e5, vel_div=-0.05)["leverage"] == pytest.approx(8.0)
assert s.calculate_size(capital=1e5, vel_div=-0.30)["leverage"] == pytest.approx(8.0)
def test_leverage_is_not_flat(self):
"""The regression: every entry used to come out at max_leverage."""
s = self._sizer()
levs = {round(s.calculate_size(capital=1e5, vel_div=vd)["leverage"], 4)
for vd in (-0.021, -0.03, -0.04, -0.05)}
assert len(levs) > 1
def test_vd_trend_needs_ten_scans_and_dedupes(self):
s = self._sizer()
for i in range(9):
s.observe(-0.02 - i * 0.001, scan_number=i + 1)
assert s.vd_trend == 0.0
s.observe(-0.05, scan_number=9) # stale scan number → ignored
assert s.vd_trend == 0.0
s.observe(-0.031, scan_number=10)
assert s.vd_trend == pytest.approx(-0.031 - (-0.02))
def test_trade_feedback_roundtrip(self):
s = self._sizer(use_alpha_layers=True)
s.calculate_size(capital=1e5, vel_div=-0.06) # extreme bucket
s.note_entry()
s.record_close(150.0) # win
stats = s._sizer.get_stats()
assert sum(stats.get("bucket_wins", [0])) >= 1 or stats # recorded without raising
def test_record_close_without_entry_is_noop(self):
s = self._sizer()
s.record_close(100.0) # must not raise
# ── R1: picker ────────────────────────────────────────────────────────────────
class TestPinkAssetPicker:
def test_warm_after_lookback_scans(self):
p = PinkAssetPicker()
series = trending_series(p.lookback)
feed_picker(p, {k: v[: p.lookback] for k, v in series.items()})
assert not p.warm
feed_picker(p, {k: v[p.lookback:] for k, v in series.items()},
start_scan=p.lookback + 1)
assert p.warm
def test_observe_dedupes_scan_number(self):
p = PinkAssetPicker()
payload = {"assets": ["AUSDT"], "asset_prices": [10.0]}
assert p.observe(payload, scan_number=5)
assert not p.observe(payload, scan_number=5)
assert not p.observe(payload, scan_number=4)
assert p.scans_observed == 1
def test_picks_downtrend_for_short_regime(self):
p = PinkAssetPicker()
feed_picker(p, trending_series(p.lookback))
choice = p.pick(direction=-1)
assert choice is not None
asset, px, ars = choice
assert asset == "DOWNUSDT"
assert px == pytest.approx(p.price_of("DOWNUSDT"))
def test_no_candidate_returns_none(self):
"""All-uptrend universe in a SHORT regime → inverse rankings only →
direction gate leaves nothing (BLUE: no fallback asset)."""
p = PinkAssetPicker()
n = p.lookback + 2
up1, up2 = [], []
a, b = 100.0, 50.0
for _ in range(n):
a *= 1.004
b *= 1.003
up1.append(a)
up2.append(b)
feed_picker(p, {"AUSDT": up1, "BUSDT": up2})
assert p.pick(direction=-1) is None
def test_price_of_unknown_asset(self):
p = PinkAssetPicker()
assert p.price_of("NOPEUSDT") is None
# ── R2: decision/intent integration ──────────────────────────────────────────
class TestDecisionSizerInjection:
def test_sizer_drives_decision_leverage(self):
sizer = PinkAlphaSizer(min_leverage=0.5, max_leverage=8.0,
leverage_convexity=3.0, vel_div_threshold=-0.02,
vel_div_extreme=-0.05, use_alpha_layers=False)
eng = DecisionEngine(make_config(), sizer=sizer)
ctx = DecisionContext(capital=100_000.0, open_positions=0, trade_seq=0)
d = eng.decide(make_snapshot(vdiv=-0.035), ctx, None)
assert d.action == DecisionAction.ENTER
assert d.leverage == pytest.approx(1.4375)
assert d.metadata.get("sizing") == "alpha_bet_sizer_cubic_v1"
# target_size = capital × fraction × leverage / price
assert d.target_size == pytest.approx(100_000 * 0.20 * 1.4375 / 50000.0)
def test_legacy_path_unchanged_without_sizer(self):
"""Pin the legacy stub exactly: leverage saturates at max_leverage."""
eng = DecisionEngine(make_config(max_leverage=3.0))
ctx = DecisionContext(capital=100_000.0, open_positions=0, trade_seq=0)
for vdiv in (-0.021, -0.035, -0.10):
d = eng.decide(make_snapshot(vdiv=vdiv), ctx, None)
assert d.action == DecisionAction.ENTER
assert d.leverage == pytest.approx(3.0)
def test_intent_honors_decision_sizing(self):
sizer = PinkAlphaSizer(min_leverage=0.5, max_leverage=8.0,
leverage_convexity=3.0, vel_div_threshold=-0.02,
vel_div_extreme=-0.05, use_alpha_layers=False)
cfg = make_config()
eng = DecisionEngine(cfg, sizer=sizer)
ieng = IntentEngine(cfg)
ctx = DecisionContext(capital=100_000.0, open_positions=0, trade_seq=0)
d = eng.decide(make_snapshot(vdiv=-0.035), ctx, None)
plan = ieng.plan(d, IntentContext(capital=100_000.0, open_positions=0, trade_seq=0))
assert plan.intent.leverage == pytest.approx(d.leverage)
assert plan.intent.target_size == pytest.approx(d.target_size)
def test_intent_legacy_recompute_identical(self):
"""Honoring decision sizing must be a no-op for legacy decisions."""
cfg = make_config(max_leverage=3.0)
eng = DecisionEngine(cfg)
ieng = IntentEngine(cfg)
ctx = DecisionContext(capital=100_000.0, open_positions=0, trade_seq=0)
d = eng.decide(make_snapshot(vdiv=-0.03), ctx, None)
plan = ieng.plan(d, IntentContext(capital=100_000.0, open_positions=0, trade_seq=0))
conf = max(0.05, min(1.0, d.confidence))
legacy_lev = min(cfg.max_leverage, max(1.0, 1.0 + conf * (cfg.max_leverage - 1.0)))
assert plan.intent.leverage == pytest.approx(legacy_lev)
# ── dual-leverage venue boundary ─────────────────────────────────────────────
class TestConvictionToExchangeLeverage:
def test_endpoints_and_midrange(self):
m = lambda c: map_internal_conviction_to_exchange_leverage(c, exchange_max=3)
assert m(0.5) == 1
assert m(9.0) == 3
assert m(4.75) == 2 # exact midpoint of [0.5, 9.0] → 2.0
assert m(0.1) == 1 # clamped below conviction floor
assert m(50.0) == 3 # clamped above conviction ceiling
def test_monotonic(self):
vals = [map_internal_conviction_to_exchange_leverage(c, exchange_max=3)
for c in (0.5, 2.0, 4.0, 6.0, 8.0, 9.0)]
assert vals == sorted(vals)
assert set(vals) == {1, 2, 3}
# ── runtime retargeting ──────────────────────────────────────────────────────
class FakeSlot:
def __init__(self, asset="", size=0.0, free=True):
self.asset = asset
self.size = size
self._free = free
def is_free(self):
return self._free
class FakeKernel:
max_slots = 1
def __init__(self, slot=None):
self.slot0 = slot or FakeSlot()
def slot(self, _i):
return self.slot0
def make_runtime(picker=None, sizer=None, slot=None):
return PinkDirectRuntime(
data_feed=SimpleNamespace(),
kernel=FakeKernel(slot),
decision_engine=SimpleNamespace(config=make_config()),
intent_engine=SimpleNamespace(),
persistence=None,
logger=LOGGER,
asset_picker=picker,
alpha_sizer=sizer,
)
def universe_payload(prices: dict, scan_number: int):
return {
"assets": list(prices),
"asset_prices": list(prices.values()),
"scan_number": scan_number,
"vel_div": -0.03,
}
class TestEffectiveSnapshot:
def test_no_picker_passthrough(self):
rt = make_runtime()
snap = make_snapshot()
out, block = rt._effective_snapshot(snap)
assert out is snap and block == ""
def test_cold_picker_blocks_entries_only(self):
rt = make_runtime(picker=PinkAssetPicker())
snap = make_snapshot(payload=universe_payload({"BTCUSDT": 50000.0}, 1))
out, block = rt._effective_snapshot(snap)
assert out.symbol == "BTCUSDT"
assert "warming" in block and not block.startswith("all:")
def test_flat_warm_picker_retargets_entry(self):
p = PinkAssetPicker()
feed_picker(p, trending_series(p.lookback))
rt = make_runtime(picker=p)
snap = make_snapshot(payload=universe_payload(
{"DOWNUSDT": p.price_of("DOWNUSDT"), "UPUSDT": p.price_of("UPUSDT")},
p.scans_observed + 1))
out, block = rt._effective_snapshot(snap)
assert block == ""
assert out.symbol == "DOWNUSDT"
assert out.price == pytest.approx(p.price_of("DOWNUSDT"))
# Regime signal untouched
assert out.velocity_divergence == snap.velocity_divergence
def test_open_slot_follows_slot_asset(self):
p = PinkAssetPicker()
feed_picker(p, trending_series(p.lookback))
slot = FakeSlot(asset="UPUSDT", size=2.0, free=False)
rt = make_runtime(picker=p, slot=slot)
snap = make_snapshot(payload=universe_payload(
{"DOWNUSDT": 90.0, "UPUSDT": 110.0}, p.scans_observed + 1))
out, block = rt._effective_snapshot(snap)
assert block == ""
assert out.symbol == "UPUSDT"
assert out.price == pytest.approx(110.0)
def test_open_slot_unpriced_asset_blocks_all(self):
p = PinkAssetPicker()
slot = FakeSlot(asset="STRAYUSDT", size=1.0, free=False)
rt = make_runtime(picker=p, slot=slot)
snap = make_snapshot(payload=universe_payload({"BTCUSDT": 50000.0}, 1))
out, block = rt._effective_snapshot(snap)
assert block.startswith("all:")
assert out.symbol == "BTCUSDT" # unchanged; step() must HOLD
def test_no_candidate_blocks_entry(self):
p = PinkAssetPicker()
n = p.lookback + 2
up = [100.0 * (1.004 ** i) for i in range(1, n + 1)]
feed_picker(p, {"AUSDT": up})
rt = make_runtime(picker=p)
snap = make_snapshot(payload=universe_payload({"AUSDT": up[-1]}, n + 1))
out, block = rt._effective_snapshot(snap)
assert "no IRP candidate" in block
def test_sizer_observe_fed_per_scan(self):
sizer = PinkAlphaSizer(vel_div_threshold=-0.02, vel_div_extreme=-0.05,
use_alpha_layers=False)
rt = make_runtime(sizer=sizer)
for i in range(12):
snap = make_snapshot(
scan_number=i + 1,
payload={"scan_number": i + 1, "vel_div": -0.02 - i * 0.001},
)
rt._effective_snapshot(snap)
assert len(sizer._vd_history) == 10
assert sizer.vd_trend != 0.0
if __name__ == "__main__":
import sys
sys.exit(pytest.main([__file__, "-v"]))

View File

@@ -0,0 +1,267 @@
"""Live BingX VST E2E for the execution-router order path (PostOnly/LIMIT).
GATED: runs only with DOLPHIN_EXEC_LIVE_E2E=1 and BingX VST credentials in
the environment. Places REAL orders on BingX VST (testnet) — never mainnet
(BingxEnvironment.VST is hardcoded; allow_mainnet=False).
Symbol policy: TRXUSDT, deliberately NOT BTCUSDT, so the concurrently
running PINK daemon (whose current build filters its account stream to
BTCUSDT) cannot misattribute our test fills.
Scenarios:
1. postonly_far_rests_then_cancel — PostOnly SELL far above market must
rest (ack, no fill), then CANCEL must remove it. Verifies the resting
leg of the maker path end-to-end through kernel → venue → BingX.
2. postonly_crossing_rejected — PostOnly SELL far below market is
marketable; the venue must NOT fill it as taker. Verifies the fee
guarantee that makes maker mode safe.
3. maker_exit_reduceonly — open a small MARKET short, then close it with
a PostOnly reduce-only BUY near the touch; on TTL miss fall back to
MARKET (mirrors the runtime escalation). Verifies flat at the end.
Every scenario flattens TRXUSDT and cancels stray orders in setup and
teardown — the account must end exactly as it started: flat, no orders.
Run:
DOLPHIN_EXEC_LIVE_E2E=1 BINGX_API_KEY=… BINGX_SECRET_KEY=… \
python -m pytest prod/clean_arch/dita_v2/test_exec_live_e2e.py -v -s
"""
from __future__ import annotations
import asyncio
import os
import time
import unittest
from datetime import datetime, timezone
import pytest
LIVE = os.environ.get("DOLPHIN_EXEC_LIVE_E2E", "") == "1" and \
bool(os.environ.get("BINGX_API_KEY")) and bool(os.environ.get("BINGX_SECRET_KEY"))
pytestmark = pytest.mark.skipif(
not LIVE, reason="live VST E2E gated: set DOLPHIN_EXEC_LIVE_E2E=1 + BingX keys")
ASSET = "TRXUSDT"
VENUE_SYMBOL = "TRX-USDT"
QTY = 30.0 # ~10 USDT notional at TRX ≈ 0.33
FAR_UP = 1.06 # +6% — rests, will not fill
FAR_DOWN = 0.94 # 6% — marketable, PostOnly must refuse to take
def _build_bundle():
from prod.bingx.config import BingxExecClientConfig
from prod.bingx.enums import BingxEnvironment
from prod.clean_arch.dita_v2.launcher import build_launcher_bundle
cfg = BingxExecClientConfig(
api_key=os.environ["BINGX_API_KEY"],
secret_key=os.environ["BINGX_SECRET_KEY"],
environment=BingxEnvironment.VST, # testnet, always
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",
)
bundle = build_launcher_bundle(venue_mode="BINGX", max_slots=1, bingx_config=cfg)
k = bundle.kernel
k.account.snapshot.capital = 1000.0
k.account.snapshot.peak_capital = 1000.0
k.account.snapshot.equity = 1000.0
return bundle
async def _client(bundle):
return bundle.kernel.venue.backend._client
async def _price(bundle) -> float:
c = await _client(bundle)
resp = await c.signed_get("/openApi/swap/v2/quote/price", {"symbol": VENUE_SYMBOL})
if isinstance(resp, dict):
return float(resp.get("price") or 0.0)
return float(resp or 0.0)
async def _open_orders(bundle) -> list:
c = await _client(bundle)
resp = await c.signed_get("/openApi/swap/v2/trade/openOrders",
{"symbol": VENUE_SYMBOL})
if isinstance(resp, dict):
return list(resp.get("orders") or [])
return list(resp or [])
async def _positions(bundle) -> list:
c = await _client(bundle)
resp = await c.signed_get("/openApi/swap/v2/user/positions",
{"symbol": VENUE_SYMBOL})
rows = resp if isinstance(resp, list) else []
return [r for r in rows
if abs(float(r.get("positionAmt") or r.get("positionQty") or 0)) > 1e-9]
async def _flatten(bundle) -> None:
"""Cancel all TRX orders + close any TRX position with reduce-only MARKET."""
c = await _client(bundle)
try:
await c.signed_delete("/openApi/swap/v2/trade/allOpenOrders",
{"symbol": VENUE_SYMBOL})
except Exception:
pass
for p in await _positions(bundle):
qty = abs(float(p.get("positionAmt") or p.get("positionQty") or 0))
side = "BUY" if float(p.get("positionAmt") or 0) < 0 else "SELL"
try:
await c.signed_post("/openApi/swap/v2/trade/order", {
"symbol": VENUE_SYMBOL, "side": side, "positionSide": "BOTH",
"type": "MARKET", "quantity": f"{qty:.0f}", "reduceOnly": "true",
})
except Exception:
pass
await asyncio.sleep(1.0)
def _intent(action, tid, *, order_type="MARKET", limit_price=0.0,
post_only=False, size=QTY, ref=0.0):
from prod.clean_arch.dita_v2.contracts import (
KernelCommandType, KernelIntent, TradeSide)
meta = {}
if order_type == "LIMIT":
meta["_time_in_force"] = "PostOnly" if post_only else "GTC"
return KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=tid, trade_id=tid, slot_id=0, asset=ASSET,
side=TradeSide.SHORT, action=getattr(KernelCommandType, action),
reference_price=ref, target_size=size, leverage=1.0,
order_type=order_type, limit_price=limit_price, metadata=meta,
reason="exec_live_e2e",
)
async def _connect(bundle):
res = bundle.kernel.venue.connect()
if asyncio.iscoroutine(res):
await res
class TestExecLiveE2E(unittest.TestCase):
def setUp(self):
self.bundle = _build_bundle()
asyncio.run(self._setup_async())
def tearDown(self):
asyncio.run(self._teardown_async())
async def _setup_async(self):
await _connect(self.bundle)
await _flatten(self.bundle)
async def _teardown_async(self):
try:
await _flatten(self.bundle)
assert await _positions(self.bundle) == [], "teardown left a position!"
assert await _open_orders(self.bundle) == [], "teardown left open orders!"
finally:
try:
disc = self.bundle.kernel.venue.backend.disconnect()
if asyncio.iscoroutine(disc):
await disc
except Exception:
pass
# ── scenario 1 ───────────────────────────────────────────────────────────
def test_postonly_far_rests_then_cancel(self):
asyncio.run(self._s1())
async def _s1(self):
k = self.bundle.kernel
px = await _price(self.bundle)
assert px > 0, "no TRX price"
tid = f"e2e-rest-{int(time.time()*1000)}"
quote = round(px * FAR_UP, 5)
await k.process_intent_async(_intent(
"ENTER", tid, order_type="LIMIT", limit_price=quote,
post_only=True, ref=px))
await asyncio.sleep(2.0)
orders = await _open_orders(self.bundle)
assert len(orders) == 1, f"expected 1 resting order, got {orders}"
assert (await _positions(self.bundle)) == [], "far quote must not fill"
# cancel through the kernel (the TTL loop's path)
await k.process_intent_async(_intent(
"CANCEL", tid, order_type="LIMIT", limit_price=quote, ref=px))
await asyncio.sleep(2.0)
assert (await _open_orders(self.bundle)) == [], "cancel left the order"
assert (await _positions(self.bundle)) == [], "flat expected after cancel"
print(f"\nS1 OK: PostOnly rested @ {quote} (px={px}) then cancelled clean")
# ── scenario 2 ───────────────────────────────────────────────────────────
def test_postonly_crossing_rejected(self):
asyncio.run(self._s2())
async def _s2(self):
k = self.bundle.kernel
px = await _price(self.bundle)
assert px > 0
tid = f"e2e-cross-{int(time.time()*1000)}"
quote = round(px * FAR_DOWN, 5) # SELL below market = marketable
await k.process_intent_async(_intent(
"ENTER", tid, order_type="LIMIT", limit_price=quote,
post_only=True, ref=px))
await asyncio.sleep(2.5)
pos = await _positions(self.bundle)
orders = await _open_orders(self.bundle)
# The whole point of PostOnly: a crossing quote must NOT execute as
# taker. Reject (nothing) is correct.
assert pos == [], f"PostOnly crossing quote FILLED — taker leak! {pos}"
for o in orders: # defensive: if venue let it rest, clean it
print(f"S2 note: venue rested crossing PostOnly: {o}")
await _flatten(self.bundle)
print(f"\nS2 OK: PostOnly crossing quote @ {quote} (px={px}) did not take")
# ── scenario 3 ───────────────────────────────────────────────────────────
def test_maker_exit_reduceonly_with_market_fallback(self):
asyncio.run(self._s3())
async def _s3(self):
k = self.bundle.kernel
px = await _price(self.bundle)
assert px > 0
tid = f"e2e-mx-{int(time.time()*1000)}"
# open small short (taker)
await k.process_intent_async(_intent("ENTER", tid, ref=px))
await asyncio.sleep(2.0)
pos = await _positions(self.bundle)
assert pos, "entry MARKET did not open a position"
# maker exit: reduce-only PostOnly BUY just below the touch
quote = round(px * 0.9985, 5)
await k.process_intent_async(_intent(
"EXIT", tid, order_type="LIMIT", limit_price=quote,
post_only=True, ref=px))
# TTL window: give it up to 10 s to fill as maker
deadline = time.time() + 10.0
filled = False
while time.time() < deadline:
await asyncio.sleep(2.0)
if not await _positions(self.bundle):
filled = True
break
if not filled:
# runtime escalation path: cancel quote, MARKET close
await k.process_intent_async(_intent(
"CANCEL", tid, order_type="LIMIT", limit_price=quote, ref=px))
await asyncio.sleep(1.0)
await _flatten(self.bundle)
assert (await _positions(self.bundle)) == [], "position not closed"
assert (await _open_orders(self.bundle)) == [], "stray order left"
print(f"\nS3 OK: maker exit {'FILLED as maker' if filled else 'missed → MARKET fallback'} — flat verified")
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,605 @@
"""ExecutionRouter — unit, adversarial and fuzz tests (pure policy layer).
Invariants under test (the non-negotiables from exec_router's docstring):
R1 exits are never skipped / suppressed except the working-dup guard
R2 one working ENTER maximum; duplicate ENTER plans are suppressed
R3 retries are bounded by entry_retries, then retry_exhaust applies
R4 default config (no env) == pure taker == legacy behavior
R5 hooks can never crash the policy path nor strand an exit
"""
from __future__ import annotations
import itertools
import os
import random
import unittest
from unittest import mock
from hypothesis import given, settings, strategies as st
from prod.clean_arch.dita_v2.exec_router import (
DEFAULT_TICKS,
ExecConfig,
ExecutionPlan,
ExecutionRouter,
MAKER_EXIT_REASONS,
MissAction,
)
class FakeClock:
def __init__(self, t: float = 1000.0):
self.t = t
def __call__(self) -> float:
return self.t
def tick(self, dt: float) -> None:
self.t += dt
def make_router(clock=None, **cfg) -> ExecutionRouter:
return ExecutionRouter(ExecConfig(**cfg), clock=clock or FakeClock())
# ─────────────────────────────────────────────────────────────────────────────
# Config parsing
# ─────────────────────────────────────────────────────────────────────────────
class TestExecConfig(unittest.TestCase):
def test_defaults_are_taker(self):
cfg = ExecConfig()
self.assertEqual(cfg.style, "taker")
self.assertFalse(cfg.maker_entry)
self.assertFalse(cfg.maker_exit)
def test_from_env_defaults(self):
with mock.patch.dict(os.environ, {}, clear=True):
cfg = ExecConfig.from_env()
self.assertEqual(cfg.style, "taker")
self.assertEqual(cfg.entry_miss, "skip")
self.assertEqual(cfg.entry_retries, 1)
self.assertTrue(cfg.post_only)
def test_from_env_full(self):
env = {
"DOLPHIN_PINK_EXEC_STYLE": "maker_both",
"DOLPHIN_PINK_MAKER_ENTRY_TTL_S": "12.5",
"DOLPHIN_PINK_MAKER_EXIT_TTL_S": "3",
"DOLPHIN_PINK_MAKER_ENTRY_MISS": "retry",
"DOLPHIN_PINK_MAKER_ENTRY_RETRIES": "2",
"DOLPHIN_PINK_MAKER_RETRY_EXHAUST": "market",
"DOLPHIN_PINK_MAKER_OFFSET_TICKS": "3",
"DOLPHIN_PINK_MAKER_MAX_SPREAD_BPS": "7.5",
"DOLPHIN_PINK_POST_ONLY": "0",
"DOLPHIN_PINK_TICK_SIZE_FOOUSDT": "0.025",
}
with mock.patch.dict(os.environ, env, clear=True):
cfg = ExecConfig.from_env()
self.assertEqual(cfg.style, "maker_both")
self.assertEqual(cfg.entry_ttl_s, 12.5)
self.assertEqual(cfg.exit_ttl_s, 3.0)
self.assertEqual(cfg.entry_miss, "retry")
self.assertEqual(cfg.entry_retries, 2)
self.assertEqual(cfg.retry_exhaust, "market")
self.assertEqual(cfg.offset_ticks, 3)
self.assertEqual(cfg.max_spread_bps, 7.5)
self.assertFalse(cfg.post_only)
self.assertEqual(cfg.tick_overrides["FOOUSDT"], 0.025)
def test_from_env_garbage_falls_back(self):
env = {
"DOLPHIN_PINK_EXEC_STYLE": "yolo",
"DOLPHIN_PINK_MAKER_ENTRY_TTL_S": "not-a-number",
"DOLPHIN_PINK_MAKER_ENTRY_MISS": "explode",
"DOLPHIN_PINK_MAKER_ENTRY_RETRIES": "-5",
"DOLPHIN_PINK_MAKER_OFFSET_TICKS": "9999",
"DOLPHIN_PINK_TICK_SIZE_BADUSDT": "zero",
}
with mock.patch.dict(os.environ, env, clear=True):
cfg = ExecConfig.from_env()
self.assertEqual(cfg.style, "taker")
self.assertEqual(cfg.entry_ttl_s, 8.0)
self.assertEqual(cfg.entry_miss, "skip")
self.assertEqual(cfg.entry_retries, 0) # clamped up from -5
self.assertEqual(cfg.offset_ticks, 100) # clamped down
self.assertNotIn("BADUSDT", cfg.tick_overrides)
def test_from_env_empty_strings(self):
env = {"DOLPHIN_PINK_EXEC_STYLE": "", "DOLPHIN_PINK_MAKER_ENTRY_TTL_S": " "}
with mock.patch.dict(os.environ, env, clear=True):
cfg = ExecConfig.from_env()
self.assertEqual(cfg.style, "taker")
self.assertEqual(cfg.entry_ttl_s, 8.0)
# ─────────────────────────────────────────────────────────────────────────────
# Pricing
# ─────────────────────────────────────────────────────────────────────────────
class TestPricing(unittest.TestCase):
def test_sell_quotes_above_reference(self):
r = make_router(style="maker_both")
px = r.maker_price(asset="BTCUSDT", order_side="SELL", reference_price=61000.0)
self.assertAlmostEqual(px, 61000.1)
def test_buy_quotes_below_reference(self):
r = make_router(style="maker_both")
px = r.maker_price(asset="BTCUSDT", order_side="BUY", reference_price=61000.0)
self.assertAlmostEqual(px, 60999.9)
def test_offset_ticks_respected(self):
r = make_router(style="maker_both", offset_ticks=5)
px = r.maker_price(asset="BTCUSDT", order_side="SELL", reference_price=61000.0)
self.assertAlmostEqual(px, 61000.5)
def test_unknown_symbol_uses_fraction(self):
r = make_router(style="maker_both")
px = r.maker_price(asset="NEWUSDT", order_side="SELL", reference_price=100.0)
self.assertGreater(px, 100.0)
self.assertLess(px, 100.01)
def test_zero_reference_returns_zero(self):
r = make_router(style="maker_both")
self.assertEqual(r.maker_price(asset="BTCUSDT", order_side="SELL",
reference_price=0.0), 0.0)
self.assertEqual(r.maker_price(asset="BTCUSDT", order_side="BUY",
reference_price=-5.0), 0.0)
def test_buy_price_never_nonpositive(self):
r = make_router(style="maker_both", offset_ticks=100)
# tiny price, huge offset → clamped to >= one tick
px = r.maker_price(asset="SHIBUSDT", order_side="BUY", reference_price=2e-9)
self.assertGreater(px, 0.0)
def test_order_side_mapping(self):
self.assertEqual(ExecutionRouter.order_side("ENTER", "SHORT"), "SELL")
self.assertEqual(ExecutionRouter.order_side("ENTER", "LONG"), "BUY")
self.assertEqual(ExecutionRouter.order_side("EXIT", "SHORT"), "BUY")
self.assertEqual(ExecutionRouter.order_side("EXIT", "LONG"), "SELL")
# ─────────────────────────────────────────────────────────────────────────────
# Entry planning
# ─────────────────────────────────────────────────────────────────────────────
class TestPlanEntry(unittest.TestCase):
def test_taker_style_market(self):
r = make_router() # default taker
p = r.plan_entry(trade_id="t1", asset="BTCUSDT", position_side="SHORT",
reference_price=61000.0)
self.assertEqual(p.order_type, "MARKET")
self.assertFalse(p.is_maker)
self.assertFalse(p.suppress)
def test_maker_entry_limit_postonly(self):
r = make_router(style="maker_entry")
p = r.plan_entry(trade_id="t1", asset="BTCUSDT", position_side="SHORT",
reference_price=61000.0)
self.assertEqual(p.order_type, "LIMIT")
self.assertTrue(p.is_maker)
self.assertTrue(p.post_only)
self.assertAlmostEqual(p.limit_price, 61000.1)
self.assertEqual(p.ttl_s, 8.0)
def test_maker_exit_style_does_not_affect_entry(self):
r = make_router(style="maker_exit")
p = r.plan_entry(trade_id="t1", asset="BTCUSDT", position_side="SHORT",
reference_price=61000.0)
self.assertEqual(p.order_type, "MARKET")
def test_bad_reference_price_degrades_to_market(self):
r = make_router(style="maker_both")
for bad in (0.0, -1.0):
p = r.plan_entry(trade_id="t1", asset="BTCUSDT", position_side="SHORT",
reference_price=bad)
self.assertEqual(p.order_type, "MARKET")
self.assertTrue(p.sane())
def test_spread_gate(self):
r = make_router(style="maker_both", max_spread_bps=5.0)
wide = r.plan_entry(trade_id="t1", asset="BTCUSDT", position_side="SHORT",
reference_price=61000.0, spread_bps=6.0)
self.assertEqual(wide.order_type, "MARKET")
tight = r.plan_entry(trade_id="t2", asset="BTCUSDT", position_side="SHORT",
reference_price=61000.0, spread_bps=4.9)
self.assertEqual(tight.order_type, "LIMIT")
unknown = r.plan_entry(trade_id="t3", asset="BTCUSDT", position_side="SHORT",
reference_price=61000.0, spread_bps=None)
self.assertEqual(unknown.order_type, "LIMIT")
def test_duplicate_entry_suppressed_while_working(self):
r = make_router(style="maker_entry")
p1 = r.plan_entry(trade_id="t1", asset="BTCUSDT", position_side="SHORT",
reference_price=61000.0)
r.register_working(trade_id="t1", asset="BTCUSDT", position_side="SHORT", plan=p1)
p2 = r.plan_entry(trade_id="t2", asset="BTCUSDT", position_side="SHORT",
reference_price=61001.0)
self.assertTrue(p2.suppress)
self.assertIn("working_entry_exists", p2.reason)
# after fill the guard releases
r.note_fill("t1")
p3 = r.plan_entry(trade_id="t3", asset="BTCUSDT", position_side="SHORT",
reference_price=61002.0)
self.assertFalse(p3.suppress)
# ─────────────────────────────────────────────────────────────────────────────
# Exit planning — RULE 1
# ─────────────────────────────────────────────────────────────────────────────
class TestPlanExit(unittest.TestCase):
def test_take_profit_is_maker_eligible(self):
r = make_router(style="maker_exit")
p = r.plan_exit(trade_id="t1", asset="BTCUSDT", position_side="SHORT",
reference_price=60900.0, reason="TAKE_PROFIT")
self.assertEqual(p.order_type, "LIMIT")
self.assertTrue(p.post_only)
# SHORT exit = BUY → below reference
self.assertAlmostEqual(p.limit_price, 60899.9)
self.assertEqual(p.ttl_s, 5.0)
def test_urgent_reasons_always_market(self):
r = make_router(style="maker_both")
for reason in ("CATASTROPHIC_LOSS", "MAX_HOLD", "MEAN_REVERSION",
"anything_else", "", None):
p = r.plan_exit(trade_id="t1", asset="BTCUSDT", position_side="SHORT",
reference_price=60900.0, reason=reason)
self.assertEqual(p.order_type, "MARKET", f"reason={reason!r}")
self.assertFalse(p.suppress)
def test_exit_never_suppressed_fresh(self):
for style in ("taker", "maker_entry", "maker_exit", "maker_both"):
r = make_router(style=style)
p = r.plan_exit(trade_id="x", asset="BTCUSDT", position_side="SHORT",
reference_price=60900.0, reason="TAKE_PROFIT")
self.assertFalse(p.suppress, f"style={style}")
self.assertTrue(p.sane())
def test_duplicate_nonurgent_exit_suppressed_while_working(self):
r = make_router(style="maker_exit")
p1 = r.plan_exit(trade_id="t1", asset="BTCUSDT", position_side="SHORT",
reference_price=60900.0, reason="TAKE_PROFIT")
r.register_working(trade_id="t1", asset="BTCUSDT", position_side="SHORT", plan=p1)
p2 = r.plan_exit(trade_id="t1", asset="BTCUSDT", position_side="SHORT",
reference_price=60899.0, reason="TAKE_PROFIT")
self.assertTrue(p2.suppress)
def test_urgent_exit_preempts_working_quote(self):
r = make_router(style="maker_exit")
p1 = r.plan_exit(trade_id="t1", asset="BTCUSDT", position_side="SHORT",
reference_price=60900.0, reason="TAKE_PROFIT")
r.register_working(trade_id="t1", asset="BTCUSDT", position_side="SHORT", plan=p1)
p2 = r.plan_exit(trade_id="t1", asset="BTCUSDT", position_side="SHORT",
reference_price=61200.0, reason="CATASTROPHIC_LOSS")
self.assertFalse(p2.suppress)
self.assertEqual(p2.order_type, "MARKET")
self.assertTrue(p2.metadata.get("preempt_working"))
def test_bad_reference_price_exit_still_market(self):
r = make_router(style="maker_both")
p = r.plan_exit(trade_id="t1", asset="BTCUSDT", position_side="SHORT",
reference_price=0.0, reason="TAKE_PROFIT")
self.assertEqual(p.order_type, "MARKET")
self.assertTrue(p.sane())
def test_wide_spread_exit_degrades_to_market_not_skip(self):
r = make_router(style="maker_exit", max_spread_bps=2.0)
p = r.plan_exit(trade_id="t1", asset="BTCUSDT", position_side="SHORT",
reference_price=60900.0, reason="TAKE_PROFIT", spread_bps=50.0)
self.assertEqual(p.order_type, "MARKET")
self.assertFalse(p.suppress)
# ─────────────────────────────────────────────────────────────────────────────
# Registry + TTL + miss policy — RULE 2 / RULE 3
# ─────────────────────────────────────────────────────────────────────────────
class TestRegistryAndMiss(unittest.TestCase):
def _maker_plan(self, action="ENTER", ttl=8.0):
return ExecutionPlan(order_type="LIMIT", limit_price=61000.1, post_only=True,
ttl_s=ttl, is_maker=True, action=action, reason="t")
def test_expiry_with_fake_clock(self):
clk = FakeClock()
r = make_router(clock=clk, style="maker_entry")
r.register_working(trade_id="t1", asset="BTCUSDT", position_side="SHORT",
plan=self._maker_plan())
self.assertEqual(r.expired(), [])
clk.tick(7.9)
self.assertEqual(r.expired(), [])
clk.tick(0.2)
self.assertEqual([w.trade_id for w in r.expired()], ["t1"])
def test_note_fill_and_cancel_idempotent(self):
r = make_router(style="maker_entry")
r.register_working(trade_id="t1", asset="BTCUSDT", position_side="SHORT",
plan=self._maker_plan())
r.note_fill("t1")
self.assertIsNone(r.working("t1"))
r.note_fill("t1") # no-op
r.note_cancel("t1") # no-op
self.assertEqual(r.counters["fills_working"], 1)
self.assertEqual(r.counters["cancels"], 0)
def test_miss_skip(self):
r = make_router(style="maker_entry", entry_miss="skip")
wo = r.register_working(trade_id="t1", asset="BTCUSDT", position_side="SHORT",
plan=self._maker_plan())
self.assertEqual(r.entry_miss_action(wo), MissAction.SKIP)
def test_miss_market(self):
r = make_router(style="maker_entry", entry_miss="market")
wo = r.register_working(trade_id="t1", asset="BTCUSDT", position_side="SHORT",
plan=self._maker_plan())
self.assertEqual(r.entry_miss_action(wo), MissAction.MARKET)
def test_retry_bounded_then_exhaust_skip(self):
r = make_router(style="maker_entry", entry_miss="retry", entry_retries=2,
retry_exhaust="skip")
wo = r.register_working(trade_id="t1", asset="BTCUSDT", position_side="SHORT",
plan=self._maker_plan())
# miss 1 → retry
self.assertEqual(r.entry_miss_action(wo), MissAction.RETRY)
tid2, plan2 = r.retry_plan(wo, reference_price=61010.0)
self.assertEqual(tid2, "t1-r1")
self.assertEqual(plan2.order_type, "LIMIT")
r.note_cancel("t1")
wo2 = r.register_working(trade_id=tid2, asset="BTCUSDT", position_side="SHORT",
plan=plan2, base_trade_id="t1", retry_n=1)
# miss 2 → retry (retries=2)
self.assertEqual(r.entry_miss_action(wo2), MissAction.RETRY)
tid3, plan3 = r.retry_plan(wo2, reference_price=61020.0)
self.assertEqual(tid3, "t1-r2")
r.note_cancel(tid2)
wo3 = r.register_working(trade_id=tid3, asset="BTCUSDT", position_side="SHORT",
plan=plan3, base_trade_id="t1", retry_n=2)
# miss 3 → exhausted → skip
self.assertEqual(r.entry_miss_action(wo3), MissAction.SKIP)
def test_retry_exhaust_market(self):
r = make_router(style="maker_entry", entry_miss="retry", entry_retries=0,
retry_exhaust="market")
wo = r.register_working(trade_id="t1", asset="BTCUSDT", position_side="SHORT",
plan=self._maker_plan())
self.assertEqual(r.entry_miss_action(wo), MissAction.MARKET)
def test_retry_plan_insane_price_degrades_to_market(self):
r = make_router(style="maker_entry", entry_miss="retry")
wo = r.register_working(trade_id="t1", asset="BTCUSDT", position_side="SHORT",
plan=self._maker_plan())
_tid, plan = r.retry_plan(wo, reference_price=0.0)
self.assertEqual(plan.order_type, "MARKET")
self.assertTrue(plan.sane())
def test_market_fallback_ids(self):
r = make_router(style="maker_both")
woe = r.register_working(trade_id="e1", asset="BTCUSDT", position_side="SHORT",
plan=self._maker_plan("ENTER"))
tid, plan = r.market_fallback_plan(woe)
self.assertEqual(tid, "e1-m") # ENTER: fresh id
self.assertEqual(plan.order_type, "MARKET")
r.note_cancel("e1")
wox = r.register_working(trade_id="x1", asset="BTCUSDT", position_side="SHORT",
plan=self._maker_plan("EXIT", ttl=5.0))
tid2, plan2 = r.market_fallback_plan(wox)
self.assertEqual(tid2, "x1") # EXIT: same id — stays on position
self.assertEqual(plan2.order_type, "MARKET")
def test_snapshot_shape(self):
r = make_router(style="maker_both")
r.register_working(trade_id="t1", asset="BTCUSDT", position_side="SHORT",
plan=self._maker_plan())
snap = r.snapshot()
self.assertEqual(snap["style"], "maker_both")
self.assertEqual(len(snap["working"]), 1)
self.assertIn("counters", snap)
# ─────────────────────────────────────────────────────────────────────────────
# Hooks — RULE 5
# ─────────────────────────────────────────────────────────────────────────────
class TestHooks(unittest.TestCase):
def test_pre_submit_can_mutate_plan(self):
r = make_router(style="maker_entry")
def widen(plan, ctx):
if isinstance(plan, ExecutionPlan) and plan.is_maker:
from dataclasses import replace as _r
return _r(plan, limit_price=plan.limit_price + 1.0)
return plan
r.register_hook("pre_submit", widen)
p = r.plan_entry(trade_id="t1", asset="BTCUSDT", position_side="SHORT",
reference_price=61000.0)
self.assertAlmostEqual(p.limit_price, 61001.1)
def test_insane_hook_plan_ignored(self):
r = make_router(style="maker_entry")
r.register_hook("pre_submit",
lambda plan, ctx: ExecutionPlan(order_type="LIMIT", limit_price=0.0))
p = r.plan_entry(trade_id="t1", asset="BTCUSDT", position_side="SHORT",
reference_price=61000.0)
self.assertTrue(p.sane())
self.assertAlmostEqual(p.limit_price, 61000.1)
def test_hook_exception_isolated(self):
r = make_router(style="maker_entry")
def boom(plan, ctx):
raise RuntimeError("plugin gone wild")
r.register_hook("pre_submit", boom)
p = r.plan_entry(trade_id="t1", asset="BTCUSDT", position_side="SHORT",
reference_price=61000.0)
self.assertEqual(p.order_type, "LIMIT")
self.assertEqual(r.counters["hook_errors"], 1)
def test_hook_cannot_suppress_exit(self):
r = make_router(style="maker_exit")
r.register_hook("pre_submit",
lambda plan, ctx: ExecutionPlan(action="EXIT", suppress=True))
p = r.plan_exit(trade_id="t1", asset="BTCUSDT", position_side="SHORT",
reference_price=60900.0, reason="TAKE_PROFIT")
self.assertFalse(p.suppress)
self.assertEqual(p.order_type, "MARKET")
def test_unregister(self):
r = make_router(style="maker_entry")
calls = []
un = r.register_hook("on_fill", lambda wo, ctx: calls.append(1))
r.register_working(trade_id="t1", asset="BTCUSDT", position_side="SHORT",
plan=ExecutionPlan(order_type="LIMIT", limit_price=1.0,
ttl_s=5, is_maker=True, action="ENTER"))
r.note_fill("t1")
self.assertEqual(len(calls), 1)
un()
r.register_working(trade_id="t2", asset="BTCUSDT", position_side="SHORT",
plan=ExecutionPlan(order_type="LIMIT", limit_price=1.0,
ttl_s=5, is_maker=True, action="ENTER"))
r.note_fill("t2")
self.assertEqual(len(calls), 1)
def test_unknown_stage_raises(self):
r = make_router()
with self.assertRaises(ValueError):
r.register_hook("nonsense", lambda *a: None)
def test_lifecycle_hooks_fire(self):
r = make_router(style="maker_entry", entry_miss="skip")
seen = []
for stage in ("on_working", "on_miss", "on_cancel"):
r.register_hook(stage, lambda x, ctx, s=stage: seen.append(s))
wo = r.register_working(trade_id="t1", asset="BTCUSDT", position_side="SHORT",
plan=ExecutionPlan(order_type="LIMIT", limit_price=1.0,
ttl_s=5, is_maker=True, action="ENTER"))
r.entry_miss_action(wo)
r.note_cancel("t1")
self.assertEqual(seen, ["on_working", "on_miss", "on_cancel"])
# ─────────────────────────────────────────────────────────────────────────────
# Fuzz — property-based (hypothesis)
# ─────────────────────────────────────────────────────────────────────────────
class TestFuzz(unittest.TestCase):
@given(
style=st.sampled_from(["taker", "maker_entry", "maker_exit", "maker_both"]),
ref=st.floats(min_value=-1e9, max_value=1e9,
allow_nan=False, allow_infinity=False),
spread=st.one_of(st.none(), st.floats(min_value=-10, max_value=10_000,
allow_nan=False, allow_infinity=False)),
side=st.sampled_from(["SHORT", "LONG", "weird", ""]),
asset=st.sampled_from(list(DEFAULT_TICKS) + ["UNKNOWNUSDT", ""]),
)
@settings(max_examples=300, deadline=None)
def test_plan_entry_always_sane(self, style, ref, spread, side, asset):
r = make_router(style=style)
p = r.plan_entry(trade_id="f1", asset=asset, position_side=side,
reference_price=ref, spread_bps=spread)
assert p.sane(), p
if p.order_type == "LIMIT":
assert p.limit_price > 0.0
assert p.ttl_s > 0.0
@given(
style=st.sampled_from(["taker", "maker_entry", "maker_exit", "maker_both"]),
ref=st.floats(min_value=-1e9, max_value=1e9,
allow_nan=False, allow_infinity=False),
reason=st.one_of(st.none(), st.text(max_size=20),
st.sampled_from(sorted(MAKER_EXIT_REASONS) +
["CATASTROPHIC_LOSS", "MAX_HOLD"])),
side=st.sampled_from(["SHORT", "LONG"]),
)
@settings(max_examples=300, deadline=None)
def test_plan_exit_never_skips(self, style, ref, reason, side):
r = make_router(style=style)
p = r.plan_exit(trade_id="f1", asset="BTCUSDT", position_side=side,
reference_price=ref, reason=reason)
assert p.sane(), p
assert not p.suppress # no working order registered → never suppressed
@given(prices=st.lists(st.floats(min_value=1e-9, max_value=1e7,
allow_nan=False, allow_infinity=False),
min_size=1, max_size=20),
offset=st.integers(min_value=0, max_value=100),
asset=st.sampled_from(list(DEFAULT_TICKS) + ["XUSDT"]))
@settings(max_examples=200, deadline=None)
def test_maker_price_side_correct(self, prices, offset, asset):
r = make_router(style="maker_both", offset_ticks=offset)
for ref in prices:
sell = r.maker_price(asset=asset, order_side="SELL", reference_price=ref)
buy = r.maker_price(asset=asset, order_side="BUY", reference_price=ref)
assert sell >= ref
assert 0.0 < buy <= ref or buy > 0.0 # buy clamps to >= 1 tick
# ─────────────────────────────────────────────────────────────────────────────
# Chaos — randomized lifecycle sequences with invariants (seeded)
# ─────────────────────────────────────────────────────────────────────────────
class TestChaosLifecycle(unittest.TestCase):
def test_random_sequences_hold_invariants(self):
for seed in range(40):
rng = random.Random(seed)
clk = FakeClock()
r = make_router(
clock=clk,
style=rng.choice(["maker_entry", "maker_exit", "maker_both"]),
entry_miss=rng.choice(["skip", "retry", "market"]),
entry_retries=rng.randint(0, 3),
retry_exhaust=rng.choice(["skip", "market"]),
)
ids = itertools.count()
for _step in range(200):
op = rng.randrange(6)
clk.tick(rng.random() * 3)
if op == 0:
tid = f"e{next(ids)}"
p = r.plan_entry(trade_id=tid, asset="BTCUSDT",
position_side=rng.choice(["SHORT", "LONG"]),
reference_price=rng.uniform(0, 70000))
if p.is_maker and not p.suppress:
r.register_working(trade_id=tid, asset="BTCUSDT",
position_side="SHORT", plan=p)
elif op == 1:
tid = f"x{next(ids)}"
p = r.plan_exit(trade_id=tid, asset="BTCUSDT",
position_side=rng.choice(["SHORT", "LONG"]),
reference_price=rng.uniform(0, 70000),
reason=rng.choice(["TAKE_PROFIT", "MAX_HOLD",
"CATASTROPHIC_LOSS", "junk"]))
assert p.sane()
if p.is_maker and not p.suppress:
r.register_working(trade_id=tid, asset="BTCUSDT",
position_side="SHORT", plan=p)
elif op == 2 and r.working_orders():
r.note_fill(rng.choice(r.working_orders()).trade_id)
elif op == 3 and r.working_orders():
r.note_cancel(rng.choice(r.working_orders()).trade_id)
elif op == 4:
for wo in r.expired():
act = r.entry_miss_action(wo) if wo.action == "ENTER" else None
r.note_cancel(wo.trade_id)
if act == MissAction.RETRY:
tid2, plan2 = r.retry_plan(wo, reference_price=rng.uniform(1, 70000))
if plan2.is_maker:
r.register_working(trade_id=tid2, asset="BTCUSDT",
position_side="SHORT", plan=plan2,
base_trade_id=wo.base_trade_id,
retry_n=wo.retry_n + 1)
elif act == MissAction.MARKET or (wo.action == "EXIT"):
r.market_fallback_plan(wo)
else:
r.snapshot()
# INVARIANT R2: at most one working ENTER at any time
entries = [w for w in r.working_orders() if w.action == "ENTER"]
assert len(entries) <= 1, f"seed={seed}: {entries}"
# INVARIANT R3: retry numbering bounded
for w in r.working_orders():
assert w.retry_n <= r.config.entry_retries + 1
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,646 @@
"""ExecutionRouter ↔ PinkDirectRuntime glue tests (FakeKernel harness).
Covers the runtime-side drivers in prod/clean_arch/runtime/pink_direct.py:
_exec_plan_for / _exec_after_submit / _exec_cancel_working /
_handle_expired_working / pump_venue_events router notifications.
Invariants:
G1 taker style / router-None leave the kernel intent untouched
G2 a resting maker quote registers exactly once; immediate fills never do
G3 TTL expiry: fill races are detected before AND after the cancel —
a filled entry is never re-entered, a filled exit never re-closed
G4 expired EXIT always escalates to MARKET with the SAME trade_id
G5 expired ENTER honours miss policy; resubmit only into a free slot
G6 venue-side cancels accelerate the deadline (miss policy still runs)
"""
from __future__ import annotations
import asyncio
import logging
import unittest
from collections import deque
from datetime import datetime, timezone
from types import SimpleNamespace
from typing import Any, List
from prod.clean_arch.dita_v2.contracts import (
KernelCommandType,
KernelEventKind,
KernelIntent,
TradeSide,
TradeStage,
VenueEvent,
VenueEventStatus,
)
from prod.clean_arch.dita_v2.exec_router import (
ExecConfig,
ExecutionPlan,
ExecutionRouter,
)
from prod.clean_arch.runtime.pink_direct import PinkDirectRuntime
LOGGER = logging.getLogger("test_exec_router_runtime")
class FakeClock:
def __init__(self, t: float = 1000.0):
self.t = t
def __call__(self) -> float:
return self.t
def tick(self, dt: float) -> None:
self.t += dt
class FakeSlot:
def __init__(self):
self.trade_id = ""
self.asset = ""
self.fsm_state = TradeStage.IDLE
self.size = 0.0
def is_free(self) -> bool:
return self.fsm_state in (TradeStage.IDLE, TradeStage.CLOSED) and self.size <= 0.0
def to_dict(self):
return {"trade_id": self.trade_id, "asset": self.asset,
"fsm_state": self.fsm_state.value, "size": self.size}
class FakeVenue:
def __init__(self):
self.reconcile_queue: deque = deque()
async def reconcile(self) -> List[VenueEvent]:
if self.reconcile_queue:
return self.reconcile_queue.popleft()
return []
class FakeKernel:
"""Scripted kernel: on_intent callbacks mutate the slot to simulate venue
behavior (fill / rest / reject / cancel)."""
max_slots = 1
def __init__(self):
self.slot0 = FakeSlot()
self.venue = FakeVenue()
self.intents: List[KernelIntent] = []
self.on_intent = None # callable(intent, kernel) — scripted behavior
def slot(self, _i: int) -> FakeSlot:
return self.slot0
async def process_intent_async(self, intent: KernelIntent):
self.intents.append(intent)
if self.on_intent is not None:
self.on_intent(intent, self)
return SimpleNamespace(accepted=True, diagnostic_code=None, details={})
def on_venue_event(self, event: VenueEvent):
return SimpleNamespace(accepted=True, diagnostic_code=None)
def snapshot(self):
return {"account": {}}
def make_runtime(style="maker_both", clock=None, **cfg) -> PinkDirectRuntime:
kernel = FakeKernel()
rt = PinkDirectRuntime(
data_feed=SimpleNamespace(), # unused by the exec drivers
kernel=kernel,
decision_engine=SimpleNamespace(),
intent_engine=SimpleNamespace(),
persistence=None,
logger=LOGGER,
)
rt.exec_router = ExecutionRouter(ExecConfig(style=style, **cfg),
logger=LOGGER, clock=clock or FakeClock())
rt._working_intents = {}
rt._own_fill_symbols = set()
rt._price_history = deque([61000.0], maxlen=10)
return rt
def make_intent(action=KernelCommandType.ENTER, tid="t1", order_type="MARKET",
limit_price=0.0, asset="BTCUSDT", size=0.5) -> KernelIntent:
return KernelIntent(
timestamp=datetime.now(timezone.utc),
intent_id=tid, trade_id=tid, slot_id=0, asset=asset,
side=TradeSide.SHORT, action=action,
reference_price=61000.0, target_size=size, leverage=1.0,
order_type=order_type, limit_price=limit_price,
)
def fill_event(tid: str, kind=KernelEventKind.FULL_FILL,
status=VenueEventStatus.FILLED) -> VenueEvent:
return VenueEvent(
timestamp=datetime.now(timezone.utc), event_id=f"ev-{tid}",
trade_id=tid, slot_id=0, kind=kind, status=status,
asset="BTCUSDT", price=61000.0, size=0.5, filled_size=0.5,
)
def run(coro):
return asyncio.run(coro)
# ─────────────────────────────────────────────────────────────────────────────
# G2 — post-submit classification
# ─────────────────────────────────────────────────────────────────────────────
class TestAfterSubmit(unittest.TestCase):
def test_resting_entry_registers(self):
rt = make_runtime()
plan = ExecutionPlan(order_type="LIMIT", limit_price=61000.1, post_only=True,
ttl_s=8.0, is_maker=True, action="ENTER", reason="m")
intent = make_intent(order_type="LIMIT", limit_price=61000.1)
# slot stays IDLE → resting
rt._exec_after_submit(plan, intent, SimpleNamespace())
self.assertIsNotNone(rt.exec_router.working("t1"))
self.assertIn("t1", rt._working_intents)
def test_immediate_entry_fill_does_not_register(self):
rt = make_runtime()
rt.kernel.slot0.trade_id = "t1"
rt.kernel.slot0.size = 0.5
rt.kernel.slot0.fsm_state = TradeStage.POSITION_OPEN
plan = ExecutionPlan(order_type="LIMIT", limit_price=61000.1, post_only=True,
ttl_s=8.0, is_maker=True, action="ENTER", reason="m")
rt._exec_after_submit(plan, make_intent(), SimpleNamespace())
self.assertIsNone(rt.exec_router.working("t1"))
self.assertNotIn("t1", rt._working_intents)
def test_rejected_entry_registers_with_instant_deadline(self):
clk = FakeClock()
rt = make_runtime(clock=clk)
rt.kernel.slot0.fsm_state = TradeStage.ORDER_REJECTED
plan = ExecutionPlan(order_type="LIMIT", limit_price=61000.1, post_only=True,
ttl_s=8.0, is_maker=True, action="ENTER", reason="m")
rt._exec_after_submit(plan, make_intent(), SimpleNamespace())
wo = rt.exec_router.working("t1")
self.assertIsNotNone(wo)
self.assertLessEqual(wo.deadline, clk()) # resolvable immediately
def test_resting_exit_registers(self):
rt = make_runtime()
rt.kernel.slot0.trade_id = "t1"
rt.kernel.slot0.size = 0.5
rt.kernel.slot0.fsm_state = TradeStage.EXIT_WORKING
plan = ExecutionPlan(order_type="LIMIT", limit_price=60900.0, post_only=True,
ttl_s=5.0, is_maker=True, action="EXIT", reason="m")
rt._exec_after_submit(plan, make_intent(KernelCommandType.EXIT), SimpleNamespace())
self.assertIsNotNone(rt.exec_router.working("t1"))
def test_immediate_exit_fill_does_not_register(self):
rt = make_runtime()
rt.kernel.slot0.trade_id = "t1"
rt.kernel.slot0.size = 0.0
rt.kernel.slot0.fsm_state = TradeStage.CLOSED
plan = ExecutionPlan(order_type="LIMIT", limit_price=60900.0, post_only=True,
ttl_s=5.0, is_maker=True, action="EXIT", reason="m")
rt._exec_after_submit(plan, make_intent(KernelCommandType.EXIT), SimpleNamespace())
self.assertIsNone(rt.exec_router.working("t1"))
# ─────────────────────────────────────────────────────────────────────────────
# G3/G5 — entry TTL expiry
# ─────────────────────────────────────────────────────────────────────────────
def _register_working_entry(rt, tid="t1", ttl=8.0):
plan = ExecutionPlan(order_type="LIMIT", limit_price=61000.1, post_only=True,
ttl_s=ttl, is_maker=True, action="ENTER", reason="m")
intent = make_intent(tid=tid, order_type="LIMIT", limit_price=61000.1)
rt._exec_after_submit(plan, intent, SimpleNamespace())
return rt.exec_router.working(tid)
class TestEntryExpiry(unittest.TestCase):
def test_fill_detected_before_cancel(self):
clk = FakeClock()
rt = make_runtime(clock=clk, entry_miss="market")
wo = _register_working_entry(rt)
# quote fills via reconcile right before TTL handling
rt.kernel.venue.reconcile_queue.append([fill_event("t1")])
def on_intent(intent, k):
raise AssertionError("no intent should be submitted — fill won")
clk.tick(9)
# pump inside handler applies the fill → note_fill → return
rt.kernel.slot0.trade_id = "t1"
rt.kernel.slot0.size = 0.5
rt.kernel.slot0.fsm_state = TradeStage.POSITION_OPEN
rt.kernel.on_intent = on_intent
run(rt._handle_expired_working(wo))
self.assertIsNone(rt.exec_router.working("t1"))
self.assertEqual(rt.kernel.intents, [])
def test_fill_race_after_cancel(self):
clk = FakeClock()
rt = make_runtime(clock=clk, entry_miss="market")
wo = _register_working_entry(rt)
clk.tick(9)
def on_intent(intent, k):
# CANCEL arrives but the order had just filled: venue keeps position
if intent.action == KernelCommandType.CANCEL:
k.slot0.trade_id = "t1"
k.slot0.size = 0.5
k.slot0.fsm_state = TradeStage.POSITION_OPEN
else:
raise AssertionError(f"unexpected non-cancel intent {intent.action}")
rt.kernel.on_intent = on_intent
run(rt._handle_expired_working(wo))
self.assertIsNone(rt.exec_router.working("t1"))
# only the CANCEL was sent — no fallback after the raced fill
self.assertEqual([i.action for i in rt.kernel.intents],
[KernelCommandType.CANCEL])
def test_miss_skip_sends_only_cancel(self):
clk = FakeClock()
rt = make_runtime(clock=clk, entry_miss="skip")
wo = _register_working_entry(rt)
clk.tick(9)
run(rt._handle_expired_working(wo))
self.assertEqual([i.action for i in rt.kernel.intents],
[KernelCommandType.CANCEL])
self.assertIsNone(rt.exec_router.working("t1"))
self.assertEqual(rt.exec_router.counters["entry_miss_skip"], 1)
def test_miss_market_resubmits_market_with_new_id(self):
clk = FakeClock()
rt = make_runtime(clock=clk, entry_miss="market")
wo = _register_working_entry(rt)
clk.tick(9)
run(rt._handle_expired_working(wo))
actions = [(i.action, i.trade_id, i.order_type) for i in rt.kernel.intents]
self.assertEqual(actions[0][0], KernelCommandType.CANCEL)
self.assertEqual(actions[1], (KernelCommandType.ENTER, "t1-m", "MARKET"))
# market fallback is taker → not registered as working
self.assertIsNone(rt.exec_router.working("t1-m"))
def test_miss_retry_requotes_then_registers(self):
clk = FakeClock()
rt = make_runtime(clock=clk, entry_miss="retry", entry_retries=1)
wo = _register_working_entry(rt)
clk.tick(9)
run(rt._handle_expired_working(wo))
kinds = [(i.action, i.trade_id, i.order_type) for i in rt.kernel.intents]
self.assertEqual(kinds[0][0], KernelCommandType.CANCEL)
self.assertEqual(kinds[1][1], "t1-r1")
self.assertEqual(kinds[1][2], "LIMIT")
self.assertIsNotNone(rt.exec_router.working("t1-r1"))
self.assertIn("t1-r1", rt._working_intents)
# postonly TIF travels on the retried intent
self.assertEqual(rt.kernel.intents[1].metadata.get("_time_in_force"), "PostOnly")
def test_retry_exhaust_skip_after_budget(self):
clk = FakeClock()
rt = make_runtime(clock=clk, entry_miss="retry", entry_retries=1,
retry_exhaust="skip")
wo = _register_working_entry(rt)
clk.tick(9)
run(rt._handle_expired_working(wo)) # retry 1 → t1-r1 working
wo2 = rt.exec_router.working("t1-r1")
self.assertIsNotNone(wo2)
clk.tick(9)
run(rt._handle_expired_working(wo2)) # budget spent → skip
# intents: CANCEL, ENTER(r1), CANCEL — and nothing else
self.assertEqual([i.action for i in rt.kernel.intents],
[KernelCommandType.CANCEL, KernelCommandType.ENTER,
KernelCommandType.CANCEL])
self.assertEqual(rt.exec_router.working_orders(), [])
def test_slot_busy_blocks_resubmit(self):
clk = FakeClock()
rt = make_runtime(clock=clk, entry_miss="market")
wo = _register_working_entry(rt)
clk.tick(9)
def on_intent(intent, k):
if intent.action == KernelCommandType.CANCEL:
# a DIFFERENT trade occupies the slot after our cancel
k.slot0.trade_id = "other"
k.slot0.size = 0.7
k.slot0.fsm_state = TradeStage.POSITION_OPEN
rt.kernel.on_intent = on_intent
run(rt._handle_expired_working(wo))
# G5: no ENTER may follow into an occupied slot
self.assertEqual([i.action for i in rt.kernel.intents],
[KernelCommandType.CANCEL])
def test_stale_working_order_noop(self):
rt = make_runtime()
wo = _register_working_entry(rt)
rt.exec_router.note_fill("t1") # resolved before handler runs
run(rt._handle_expired_working(wo))
self.assertEqual(rt.kernel.intents, [])
class TestRequoteVenueTruthGate(unittest.TestCase):
"""Live double-entry regression (2026-06-10): a filled quote whose fill
the REST reconcile hadn't surfaced yet was treated as a miss and
re-quoted → 2× position. The gate must block requotes when (a) an own
fill landed in the hot window or (b) the venue shows any live position."""
def test_recent_own_fill_blocks_requote(self):
import time as _t
clk = FakeClock()
rt = make_runtime(clock=clk, entry_miss="market")
wo = _register_working_entry(rt)
rt._last_own_fill_mono = _t.monotonic() # fill just landed via WS
clk.tick(9)
run(rt._handle_expired_working(wo))
# cancel only — NO market fallback ENTER
self.assertEqual([i.action for i in rt.kernel.intents],
[KernelCommandType.CANCEL])
def test_live_exchange_position_blocks_requote(self):
clk = FakeClock()
rt = make_runtime(clock=clk, entry_miss="retry", entry_retries=2)
wo = _register_working_entry(rt)
rt.kernel.venue.open_positions = lambda: [{"positionAmt": "0.8932"}]
clk.tick(9)
run(rt._handle_expired_working(wo))
self.assertEqual([i.action for i in rt.kernel.intents],
[KernelCommandType.CANCEL])
def test_probe_error_fails_safe(self):
clk = FakeClock()
rt = make_runtime(clock=clk, entry_miss="market")
wo = _register_working_entry(rt)
def boom():
raise RuntimeError("venue probe down")
rt.kernel.venue.open_positions = boom
clk.tick(9)
run(rt._handle_expired_working(wo))
self.assertEqual([i.action for i in rt.kernel.intents],
[KernelCommandType.CANCEL])
def test_provably_flat_allows_requote(self):
clk = FakeClock()
rt = make_runtime(clock=clk, entry_miss="market")
wo = _register_working_entry(rt)
rt.kernel.venue.open_positions = lambda: []
rt._last_own_fill_mono = 0.0
clk.tick(9)
run(rt._handle_expired_working(wo))
self.assertEqual([i.action for i in rt.kernel.intents],
[KernelCommandType.CANCEL, KernelCommandType.ENTER])
class TestSingleSlotEntryInvariant(unittest.TestCase):
"""Second live double-entry (2026-06-10 17:24/17:25): the re-entry came
through the MAIN decision path (filled maker order vanished from
openOrders → reconcile misread it as cancel → slot freed → re-ENTER).
_unsafe_entry_reason must block ANY ENTER while the exchange shows an
open position, or within the own-fill hot window, or when the probe errs."""
def _ctx(self):
return SimpleNamespace(capital=10_000.0, open_positions=0, trade_seq=1)
def test_exchange_position_blocks_enter(self):
rt = make_runtime()
rt.kernel.venue.open_positions = lambda: [{"symbol": "BTC-USDT", "positionAmt": "0.89"}]
reason = rt._unsafe_entry_reason(make_intent(), self._ctx())
self.assertIsNotNone(reason)
self.assertIn("single-slot", reason)
def test_recent_own_fill_blocks_enter(self):
import time as _t
rt = make_runtime()
rt.kernel.venue.open_positions = lambda: []
rt._last_own_fill_mono = _t.monotonic()
reason = rt._unsafe_entry_reason(make_intent(), self._ctx())
self.assertIsNotNone(reason)
self.assertIn("hot window", reason)
def test_probe_error_fails_safe(self):
rt = make_runtime()
def boom():
raise RuntimeError("probe down")
rt.kernel.venue.open_positions = boom
reason = rt._unsafe_entry_reason(make_intent(), self._ctx())
self.assertIsNotNone(reason)
self.assertIn("fail safe", reason)
def test_flat_venue_allows_enter(self):
rt = make_runtime()
rt.kernel.venue.open_positions = lambda: []
rt._last_own_fill_mono = 0.0
intent = make_intent()
# reference_price/size/leverage are sane in make_intent → None expected
self.assertIsNone(rt._unsafe_entry_reason(intent, self._ctx()))
# ─────────────────────────────────────────────────────────────────────────────
# G4 — exit TTL expiry
# ─────────────────────────────────────────────────────────────────────────────
def _register_working_exit(rt, tid="t1", ttl=5.0):
rt.kernel.slot0.trade_id = tid
rt.kernel.slot0.asset = "BTCUSDT"
rt.kernel.slot0.size = 0.5
rt.kernel.slot0.fsm_state = TradeStage.EXIT_WORKING
plan = ExecutionPlan(order_type="LIMIT", limit_price=60900.0, post_only=True,
ttl_s=ttl, is_maker=True, action="EXIT", reason="m")
intent = make_intent(KernelCommandType.EXIT, tid=tid,
order_type="LIMIT", limit_price=60900.0)
rt._exec_after_submit(plan, intent, SimpleNamespace())
return rt.exec_router.working(tid)
class TestExitExpiry(unittest.TestCase):
def test_exit_ttl_escalates_to_market_same_trade_id(self):
clk = FakeClock()
rt = make_runtime(clock=clk)
wo = _register_working_exit(rt)
clk.tick(6)
run(rt._handle_expired_working(wo))
seq = [(i.action, i.trade_id, i.order_type) for i in rt.kernel.intents]
self.assertEqual(seq[0][0], KernelCommandType.CANCEL)
self.assertEqual(seq[1], (KernelCommandType.EXIT, "t1", "MARKET"))
self.assertEqual(rt.exec_router.counters["exit_escalations"], 1)
def test_exit_filled_during_race_no_fallback(self):
clk = FakeClock()
rt = make_runtime(clock=clk)
wo = _register_working_exit(rt)
clk.tick(6)
def on_intent(intent, k):
if intent.action == KernelCommandType.CANCEL:
# exit filled before cancel landed → flat
k.slot0.size = 0.0
k.slot0.fsm_state = TradeStage.CLOSED
rt.kernel.on_intent = on_intent
run(rt._handle_expired_working(wo))
self.assertEqual([i.action for i in rt.kernel.intents],
[KernelCommandType.CANCEL])
self.assertIsNone(rt.exec_router.working("t1"))
def test_exit_fill_detected_pre_cancel(self):
clk = FakeClock()
rt = make_runtime(clock=clk)
wo = _register_working_exit(rt)
rt.kernel.slot0.size = 0.0
rt.kernel.slot0.fsm_state = TradeStage.CLOSED
rt.kernel.venue.reconcile_queue.append([fill_event("t1")])
clk.tick(6)
run(rt._handle_expired_working(wo))
self.assertEqual(rt.kernel.intents, [])
# ─────────────────────────────────────────────────────────────────────────────
# G6 — pump notifications
# ─────────────────────────────────────────────────────────────────────────────
class TestPumpNotifications(unittest.TestCase):
def test_full_fill_clears_working(self):
rt = make_runtime()
_register_working_entry(rt)
rt.kernel.venue.reconcile_queue.append([fill_event("t1")])
applied = run(rt.pump_venue_events())
self.assertEqual(applied, 1)
self.assertIsNone(rt.exec_router.working("t1"))
self.assertNotIn("t1", rt._working_intents)
def test_venue_cancel_accelerates_deadline_not_removal(self):
clk = FakeClock()
rt = make_runtime(clock=clk, entry_miss="market")
wo = _register_working_entry(rt)
original_deadline = wo.deadline
ev = fill_event("t1", kind=KernelEventKind.CANCEL_ACK,
status=VenueEventStatus.CANCELED)
rt.kernel.venue.reconcile_queue.append([ev])
run(rt.pump_venue_events())
wo2 = rt.exec_router.working("t1")
self.assertIsNotNone(wo2) # NOT dropped (G6)
self.assertLess(wo2.deadline, original_deadline)
self.assertLessEqual(wo2.deadline, clk()) # expired now → TTL loop resolves
def test_unrelated_events_ignored(self):
rt = make_runtime()
_register_working_entry(rt)
rt.kernel.venue.reconcile_queue.append([fill_event("someone_else")])
run(rt.pump_venue_events())
self.assertIsNotNone(rt.exec_router.working("t1"))
# ─────────────────────────────────────────────────────────────────────────────
# Urgent-exit preempt + plan glue
# ─────────────────────────────────────────────────────────────────────────────
class TestPreemptAndPlanGlue(unittest.TestCase):
def test_exec_cancel_working_sends_cancel_and_clears(self):
rt = make_runtime()
_register_working_exit(rt)
run(rt._exec_cancel_working("t1", reason="urgent_exit_preempt"))
self.assertEqual([i.action for i in rt.kernel.intents],
[KernelCommandType.CANCEL])
self.assertIsNone(rt.exec_router.working("t1"))
self.assertNotIn("t1", rt._working_intents)
def test_exec_cancel_working_noop_when_not_working(self):
rt = make_runtime()
run(rt._exec_cancel_working("ghost", reason="x"))
self.assertEqual(rt.kernel.intents, [])
def test_plan_for_none_router_returns_none(self):
rt = make_runtime()
rt.exec_router = None
decision = SimpleNamespace(action=None, reason="TAKE_PROFIT")
self.assertIsNone(rt._exec_plan_for(decision, make_intent(), SimpleNamespace()))
def test_plan_for_router_crash_degrades_to_taker(self):
rt = make_runtime()
class Boom:
config = SimpleNamespace(style="maker_both")
def plan_entry(self, **kw):
raise RuntimeError("router on fire")
rt.exec_router = Boom()
from prod.clean_arch.dita import DecisionAction
decision = SimpleNamespace(action=DecisionAction.ENTER, reason="")
self.assertIsNone(rt._exec_plan_for(decision, make_intent(), SimpleNamespace()))
# ─────────────────────────────────────────────────────────────────────────────
# Chaos — randomized kernel behavior through the full expiry path
# ─────────────────────────────────────────────────────────────────────────────
class TestRuntimeChaos(unittest.TestCase):
def test_random_kernel_behaviors_never_double_enter(self):
import random
for seed in range(30):
rng = random.Random(seed)
clk = FakeClock()
rt = make_runtime(
clock=clk,
entry_miss=rng.choice(["skip", "retry", "market"]),
entry_retries=rng.randint(0, 2),
retry_exhaust=rng.choice(["skip", "market"]),
)
def on_intent(intent, k, rng=rng):
if intent.action == KernelCommandType.CANCEL:
roll = rng.random()
if roll < 0.25: # cancel raced a fill
k.slot0.trade_id = intent.trade_id
k.slot0.size = 0.5
k.slot0.fsm_state = TradeStage.POSITION_OPEN
elif roll < 0.35: # foreign trade grabbed the slot
k.slot0.trade_id = "foreign"
k.slot0.size = 0.3
k.slot0.fsm_state = TradeStage.POSITION_OPEN
else: # clean cancel
k.slot0.trade_id = ""
k.slot0.size = 0.0
k.slot0.fsm_state = TradeStage.IDLE
elif intent.action == KernelCommandType.ENTER:
if rng.random() < 0.5: # immediate fill
k.slot0.trade_id = intent.trade_id
k.slot0.size = float(intent.target_size)
k.slot0.fsm_state = TradeStage.POSITION_OPEN
# else rests (slot unchanged)
rt.kernel.on_intent = on_intent
wo = _register_working_entry(rt, tid=f"c{seed}")
for _round in range(6):
clk.tick(10)
expired = rt.exec_router.expired()
if not expired:
break
run(rt._handle_expired_working(expired[0]))
# INVARIANT: never two ENTER intents without an intervening
# CANCEL, and never an ENTER while the slot is occupied by
# someone else.
last_action = None
for it in rt.kernel.intents:
if it.action == KernelCommandType.ENTER:
self.assertNotEqual(last_action, KernelCommandType.ENTER,
f"seed={seed}: back-to-back ENTERs")
last_action = it.action
entries = [w for w in rt.exec_router.working_orders()
if w.action == "ENTER"]
self.assertLessEqual(len(entries), 1, f"seed={seed}")
# registry always resolvable: nothing may rest forever past deadline
clk.tick(1000)
for w in rt.exec_router.expired():
run(rt._handle_expired_working(w))
self.assertEqual(
[w for w in rt.exec_router.working_orders()
if w.deadline < clk()], [],
f"seed={seed}: unresolved expired orders")
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,460 @@
"""
test_reset_and_seed.py — painstaking coverage of reset_and_seed().
Scenarios tested:
Basic correctness
01 zeros all K-accumulators
02 K = seed = capital after reset
03 e_wallet_balance set to capital
04 reconcile_delta = 0 after reset
05 capital_frozen = False after reset (unfreeze)
Guard against invalid input
06 capital = 0 → no-op (kernel unchanged)
07 capital < 0 → no-op
08 capital = NaN → no-op
09 capital = Inf → no-op
Idempotency / sequencing
10 double reset is idempotent
11 reset from a frozen state unfreezes
12 reset from a WARN state also resolves cleanly
Preservation of non-accumulator state
13 seen_account_event_ids preserved (WS-replay dedup survives)
14 calibration_ratio preserved
15 calibration_samples preserved
16 open slot unchanged by reset
Post-reset accumulation is correct
17 new FILL_SETTLED after reset accumulates correctly into K
18 new PREDICTED_FILL after reset accumulates correctly
19 ACCOUNT_UPDATE after reset updates E and re-runs reconcile
20 funding fee after reset accumulated in k_funding_net
WS-replay dedup scenario (the original bug)
21 replayed fill event (same event_id) is ignored after reset
22 fresh fill event (new event_id) is processed after reset
Race / ordering
23 reset_and_seed is the last thing called before WS stream starts
(structural: verify connect() order in pink_direct.py source)
24 reset → FILL_SETTLED → ACCOUNT_UPDATE → delta stays small
Python-layer
25 ExecutionKernel.reset_and_seed delegates to Rust (smoke)
26 set_seed_capital still works independently (not broken by addition)
"""
import math
import json
import re
import sys
import asyncio
import inspect
import unittest
from pathlib import Path
from unittest.mock import MagicMock, AsyncMock, patch, call
# ── path setup ────────────────────────────────────────────────────────────────
_ROOT = Path(__file__).parents[3]
sys.path.insert(0, str(_ROOT))
sys.path.insert(0, str(_ROOT / "prod"))
sys.path.insert(0, str(_ROOT / "prod" / "clean_arch"))
from prod.clean_arch.dita_v2.rust_backend import ExecutionKernel, _get_rust
# ── helpers ───────────────────────────────────────────────────────────────────
def _make_kernel() -> ExecutionKernel:
return ExecutionKernel(max_slots=1)
def _account(k: ExecutionKernel) -> dict:
"""Return full account dict from save_state_json."""
state = json.loads(_get_rust().save_state(k._backend))
return state["account"]
def _fill_settled(k: ExecutionKernel, pnl: float, fee: float,
is_maker: bool = False, event_id: str = "") -> dict:
ev = {"kind": "FILL_SETTLED", "realized_pnl": pnl, "fee": fee,
"is_maker": is_maker}
if event_id:
ev["event_id"] = event_id
return k.on_account_event(ev)
def _account_update(k: ExecutionKernel, wb: float, avail: float = 0.0) -> dict:
return k.on_account_event({
"kind": "ACCOUNT_UPDATE",
"wallet_balance": wb,
"available_margin": avail,
"used_margin": 0.0,
"maint_margin": 0.0,
})
def _accumulate(k: ExecutionKernel, seed: float = 100_000.0) -> None:
"""Bring kernel to a known dirty state: filled, fees, frozen."""
k.set_seed_capital(seed)
_account_update(k, seed * 0.9) # E = 90k → sets e_wallet_balance
_fill_settled(k, pnl=500.0, fee=20.0, is_maker=False, event_id="ev-001")
_fill_settled(k, pnl=-200.0, fee=15.0, is_maker=True, event_id="ev-002")
# After fills k_realized_pnl ≠ 0 and reconcile status may be WARN/ERROR
# ══════════════════════════════════════════════════════════════════════════════
# 01-05 Basic correctness
# ══════════════════════════════════════════════════════════════════════════════
class TestResetAndSeedBasic(unittest.TestCase):
def test_01_zeros_all_k_accumulators(self):
k = _make_kernel()
_accumulate(k)
k.reset_and_seed(100_000.0)
acc = _account(k)
self.assertAlmostEqual(acc["k_realized_pnl"], 0.0, places=9)
self.assertAlmostEqual(acc["k_taker_fees"], 0.0, places=9)
self.assertAlmostEqual(acc["k_maker_fees"], 0.0, places=9)
self.assertAlmostEqual(acc["k_maker_rebates"],0.0, places=9)
self.assertAlmostEqual(acc["k_fees_paid"], 0.0, places=9)
self.assertAlmostEqual(acc["k_funding_net"], 0.0, places=9)
def test_02_k_capital_equals_seed_equals_capital(self):
k = _make_kernel()
_accumulate(k)
k.reset_and_seed(123_456.78)
acc = _account(k)
self.assertAlmostEqual(acc["seed_capital"], 123_456.78, places=6)
self.assertAlmostEqual(acc["k_capital"], 123_456.78, places=6)
def test_03_e_wallet_balance_set_to_capital(self):
k = _make_kernel()
_accumulate(k)
k.reset_and_seed(99_999.99)
acc = _account(k)
self.assertAlmostEqual(acc["e_wallet_balance"], 99_999.99, places=6)
def test_04_reconcile_delta_is_zero(self):
k = _make_kernel()
_accumulate(k)
k.reset_and_seed(100_000.0)
acc = _account(k)
self.assertAlmostEqual(acc["reconcile_delta"], 0.0, places=6)
self.assertEqual(acc["reconcile_status"], "OK")
def test_05_capital_unfrozen_after_reset(self):
k = _make_kernel()
# Force a frozen state via a large K > E discrepancy
k.set_seed_capital(100_000.0)
_account_update(k, 50_000.0) # E = 50k while K = 100k → ERROR → frozen
self.assertTrue(k.is_capital_frozen(), "pre-condition: should be frozen")
k.reset_and_seed(50_000.0)
self.assertFalse(k.is_capital_frozen())
# ══════════════════════════════════════════════════════════════════════════════
# 06-09 Guard against invalid input
# ══════════════════════════════════════════════════════════════════════════════
class TestResetAndSeedInvalidInput(unittest.TestCase):
def _assert_no_change(self, k: ExecutionKernel, capital_before: float) -> None:
acc = _account(k)
self.assertAlmostEqual(acc["seed_capital"], capital_before, places=6,
msg="seed_capital must not change on invalid input")
def test_06_zero_capital_is_noop(self):
k = _make_kernel()
k.set_seed_capital(75_000.0)
k.reset_and_seed(0.0)
self._assert_no_change(k, 75_000.0)
def test_07_negative_capital_is_noop(self):
k = _make_kernel()
k.set_seed_capital(75_000.0)
k.reset_and_seed(-1.0)
self._assert_no_change(k, 75_000.0)
def test_08_nan_capital_is_noop(self):
k = _make_kernel()
k.set_seed_capital(75_000.0)
k.reset_and_seed(float("nan"))
self._assert_no_change(k, 75_000.0)
def test_09_inf_capital_is_noop(self):
k = _make_kernel()
k.set_seed_capital(75_000.0)
k.reset_and_seed(float("inf"))
self._assert_no_change(k, 75_000.0)
# ══════════════════════════════════════════════════════════════════════════════
# 10-12 Idempotency / sequencing
# ══════════════════════════════════════════════════════════════════════════════
class TestResetAndSeedIdempotency(unittest.TestCase):
def test_10_double_reset_is_idempotent(self):
k = _make_kernel()
_accumulate(k)
k.reset_and_seed(100_000.0)
acc1 = _account(k)
k.reset_and_seed(100_000.0)
acc2 = _account(k)
self.assertAlmostEqual(acc1["k_capital"], acc2["k_capital"], places=6)
self.assertAlmostEqual(acc1["reconcile_delta"], acc2["reconcile_delta"], places=6)
self.assertEqual(acc1["reconcile_status"], acc2["reconcile_status"])
self.assertEqual(acc1["capital_frozen"], acc2["capital_frozen"])
def test_11_reset_from_frozen_state_unfreezes(self):
k = _make_kernel()
k.set_seed_capital(100_000.0)
_account_update(k, 50_000.0)
self.assertTrue(k.is_capital_frozen())
k.reset_and_seed(50_000.0)
self.assertFalse(k.is_capital_frozen())
acc = _account(k)
self.assertEqual(acc["reconcile_status"], "OK")
def test_12_reset_from_warn_state_resolves(self):
k = _make_kernel()
k.set_seed_capital(100_000.0)
# 10 USDT gap → WARN (< 20 threshold)
_account_update(k, 99_990.0)
acc_before = _account(k)
self.assertEqual(acc_before["reconcile_status"], "WARN")
k.reset_and_seed(99_990.0)
acc = _account(k)
self.assertEqual(acc["reconcile_status"], "OK")
self.assertAlmostEqual(acc["reconcile_delta"], 0.0, places=6)
# ══════════════════════════════════════════════════════════════════════════════
# 13-16 Preservation of non-accumulator state
# ══════════════════════════════════════════════════════════════════════════════
class TestResetAndSeedPreservation(unittest.TestCase):
def test_13_seen_account_event_ids_preserved(self):
"""The WS-replay dedup list must survive reset so old replays stay idempotent."""
k = _make_kernel()
k.set_seed_capital(100_000.0)
_account_update(k, 100_000.0)
# Process a fill with a known event_id
r1 = _fill_settled(k, pnl=100.0, fee=5.0, event_id="unique-event-XYZ")
self.assertFalse(r1.get("duplicate_event", False), "first call must not be duplicate")
k.reset_and_seed(100_100.0) # reset does NOT clear dedup list
# Replay the same event_id — must still be recognised as duplicate
r2 = _fill_settled(k, pnl=100.0, fee=5.0, event_id="unique-event-XYZ")
self.assertTrue(r2.get("duplicate_event", False),
"replayed event_id must be deduplicated after reset_and_seed")
def test_14_calibration_ratio_preserved(self):
k = _make_kernel()
k.set_seed_capital(100_000.0)
k.set_exchange_config({"taker_rate": 0.0005, "maker_rate": 0.0002})
# Run a calibration that shifts ratio away from 1.0
fill_price, fill_qty = 60_000.0, 1.0
actual_fee = 120.0 # much higher than predicted → ratio > 1
k.calibrate_fee(fill_price, fill_qty, actual_fee, is_maker=False)
acc_before = _account(k)
ratio_before = acc_before.get("last_calibration_ratio", 1.0)
k.reset_and_seed(100_000.0)
acc_after = _account(k)
self.assertAlmostEqual(acc_after.get("last_calibration_ratio", 1.0),
ratio_before, places=6,
msg="calibration_ratio must survive reset_and_seed")
def test_15_calibration_samples_preserved(self):
k = _make_kernel()
k.set_seed_capital(100_000.0)
k.set_exchange_config({"taker_rate": 0.0005, "maker_rate": 0.0002})
k.calibrate_fee(60_000.0, 1.0, 30.0, is_maker=False)
acc_before = _account(k)
samples_before = acc_before.get("fee_config", {}).get("calibration_samples", 0)
k.reset_and_seed(100_000.0)
acc_after = _account(k)
samples_after = acc_after.get("fee_config", {}).get("calibration_samples", 0)
self.assertEqual(samples_after, samples_before,
"calibration_samples must survive reset_and_seed")
def test_16_open_slot_unchanged_by_reset(self):
"""reset_and_seed touches only AccountState — slot FSM must be untouched."""
from prod.clean_arch.dita_v2.rust_backend import ExecutionKernel
k = _make_kernel()
k.set_seed_capital(100_000.0)
slot_before = _get_rust().get_slot_json(k._backend, 0)
k.reset_and_seed(100_000.0)
slot_after = _get_rust().get_slot_json(k._backend, 0)
self.assertEqual(slot_before.get("fsm_state"), slot_after.get("fsm_state"))
self.assertEqual(slot_before.get("trade_id"), slot_after.get("trade_id"))
# ══════════════════════════════════════════════════════════════════════════════
# 17-20 Post-reset accumulation is correct
# ══════════════════════════════════════════════════════════════════════════════
class TestResetAndSeedPostAccumulation(unittest.TestCase):
def test_17_fill_settled_after_reset_accumulates(self):
k = _make_kernel()
_accumulate(k)
k.reset_and_seed(100_000.0)
_account_update(k, 100_000.0)
_fill_settled(k, pnl=200.0, fee=10.0, is_maker=False, event_id="new-ev-001")
acc = _account(k)
# k_realized_pnl should reflect only the new fill
self.assertGreater(acc["k_realized_pnl"], 0.0,
"new fill should increase k_realized_pnl")
# taker fee should be > 0
self.assertGreater(acc["k_taker_fees"], 0.0)
def test_18_predicted_fill_after_reset_accumulates(self):
k = _make_kernel()
k.reset_and_seed(100_000.0)
k.on_account_event({
"kind": "PREDICTED_FILL",
"fill_price": 60_000.0,
"fill_qty": 1.0,
"realized_pnl": 150.0,
"is_maker": False,
})
acc = _account(k)
self.assertGreater(acc["k_realized_pnl"], 0.0)
def test_19_account_update_after_reset_updates_e_and_reconciles(self):
k = _make_kernel()
k.reset_and_seed(100_000.0)
# Push E slightly above K — should stay OK/WARN (not ERROR)
_account_update(k, 100_010.0)
acc = _account(k)
self.assertAlmostEqual(acc["e_wallet_balance"], 100_010.0, places=6)
# delta = |100_000 - 100_010| = 10 → WARN, not ERROR → not frozen
self.assertFalse(acc["capital_frozen"],
"10 USDT delta is WARN, not ERROR — should not freeze")
def test_20_funding_fee_after_reset_accumulated(self):
k = _make_kernel()
k.reset_and_seed(100_000.0)
_account_update(k, 100_000.0)
k.on_account_event({"kind": "FUNDING_FEE", "funding_amount": -50.0})
acc = _account(k)
# k_funding_net += 50 (paid) → k_capital = seed - 50 = 99_950
self.assertAlmostEqual(acc["k_funding_net"], 50.0, places=6)
self.assertAlmostEqual(acc["k_capital"], 99_950.0, places=6)
# ══════════════════════════════════════════════════════════════════════════════
# 21-22 WS-replay dedup (the original bug)
# ══════════════════════════════════════════════════════════════════════════════
class TestResetAndSeedReplayDedup(unittest.TestCase):
def test_21_replayed_fill_ignored_after_reset(self):
"""
Scenario: PINK made fills in session N. Snapshot carries seen_account_event_ids.
Session N+1: reset_and_seed called. BingX WS replays old fill.
Expected: replay is deduplicated → k_realized_pnl stays 0.
"""
k = _make_kernel()
k.set_seed_capital(100_000.0)
_account_update(k, 100_000.0)
# Session N: process a fill
_fill_settled(k, pnl=300.0, fee=15.0, event_id="session-n-fill-1")
# Session N+1 startup: reset_and_seed
k.reset_and_seed(100_300.0) # BingX balance reflects the prior trade
acc_after_reset = _account(k)
self.assertAlmostEqual(acc_after_reset["k_realized_pnl"], 0.0, places=9,
msg="accumulators must be zero right after reset")
# WS replays the old fill — must be deduplicated
_fill_settled(k, pnl=300.0, fee=15.0, event_id="session-n-fill-1")
acc_after_replay = _account(k)
self.assertAlmostEqual(acc_after_replay["k_realized_pnl"], 0.0, places=9,
msg="replayed fill must not add to k_realized_pnl")
self.assertAlmostEqual(acc_after_replay["reconcile_delta"], 0.0, places=6,
msg="replay must not re-freeze capital")
def test_22_fresh_fill_processed_after_reset(self):
"""New event_id after reset must be processed normally."""
k = _make_kernel()
k.set_seed_capital(100_000.0)
_account_update(k, 100_000.0)
_fill_settled(k, pnl=300.0, fee=15.0, event_id="old-fill")
k.reset_and_seed(100_300.0)
_account_update(k, 100_300.0)
# New fill in session N+1
r = _fill_settled(k, pnl=50.0, fee=2.5, event_id="new-fill-session-n+1")
self.assertFalse(r.get("duplicate_event", False),
"new event_id must not be flagged as duplicate")
acc = _account(k)
self.assertGreater(acc["k_realized_pnl"], 0.0,
"new fill must accumulate k_realized_pnl")
# ══════════════════════════════════════════════════════════════════════════════
# 23-24 Race / ordering
# ══════════════════════════════════════════════════════════════════════════════
class TestResetAndSeedOrdering(unittest.TestCase):
def test_23_reset_called_before_ws_stream_starts_in_connect(self):
"""
Structural: inspect pink_direct.py connect() source to verify
reset_and_seed is called before asyncio.create_task(_run_account_stream).
This guarantees no WS events arrive before reset completes.
"""
src_path = (Path(__file__).parent.parent /
"runtime" / "pink_direct.py")
source = src_path.read_text()
connect_body = re.search(
r"async def connect\(.*?\n async def ", source, re.DOTALL
)
self.assertIsNotNone(connect_body, "could not locate connect() body")
body = connect_body.group(0)
pos_reset = body.find("reset_and_seed")
pos_stream = body.find("create_task")
self.assertGreater(pos_reset, 0, "reset_and_seed must be in connect()")
self.assertGreater(pos_stream, 0, "create_task must be in connect()")
self.assertLess(pos_reset, pos_stream,
"reset_and_seed must appear BEFORE create_task in connect()")
def test_24_reset_fill_account_update_cycle_stays_clean(self):
"""
Simulate: reset → new fill arrives → ACCOUNT_UPDATE from WS (E now includes fill).
Delta should stay < 20 USDT (no freeze).
"""
k = _make_kernel()
k.reset_and_seed(100_000.0)
# A trade fills at market: realized PnL +80, taker fee -30
_fill_settled(k, pnl=80.0, fee=30.0, is_maker=False, event_id="trade-ev-1")
# BingX settles: wallet_balance = 100_000 + 80 - 30 = 100_050
_account_update(k, 100_050.0)
acc = _account(k)
# K = seed(100k) + 80 - 30 = 100_050; E = 100_050 → delta = 0
self.assertAlmostEqual(acc["reconcile_delta"], 0.0, places=4)
self.assertEqual(acc["reconcile_status"], "OK")
self.assertFalse(acc["capital_frozen"])
# ══════════════════════════════════════════════════════════════════════════════
# 25-26 Python-layer smoke tests
# ══════════════════════════════════════════════════════════════════════════════
class TestResetAndSeedPythonLayer(unittest.TestCase):
def test_25_execution_kernel_reset_and_seed_smoke(self):
"""ExecutionKernel.reset_and_seed delegates correctly; K=E after call."""
k = _make_kernel()
_accumulate(k)
k.reset_and_seed(88_888.0)
acc = _account(k)
self.assertAlmostEqual(acc["k_capital"], 88_888.0, places=4)
self.assertEqual(acc["reconcile_status"], "OK")
def test_26_set_seed_capital_still_works(self):
"""Ensure set_seed_capital was not accidentally broken by our addition."""
k = _make_kernel()
k.set_seed_capital(55_555.0)
acc = _account(k)
self.assertAlmostEqual(acc["seed_capital"], 55_555.0, places=4)
if __name__ == "__main__":
unittest.main(verbosity=2)

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,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