Snapshot PINK DITAv2 system + Sprint 0 flaw-fix verification
First commit of the previously-untracked PINK-on-DITAv2 migration system (execution moves to the Rust kernel; policy stays on legacy DITA, so Alpha Engine algorithmic integrity is preserved). BLUE is untouched. Sprint 0 (safety snapshot + flaw-fix verification, MARKET single-leg scope): - Verified Rust FSM fixes (flaws 2,4,10,11,13) by source read of lib.rs. - Hardened 5 vacuous/guarded assertions in test_flaws.py so each flaw test genuinely exercises its fix. Most important: Flaw 5 now asserts capital moves by EXACTLY realized PnL (was entering/exiting at the same price). - Offline suites: 533 passed, 0 failed (35 flaws + 402 kernel/accounting/ bridge + 96 runtime/persistence/multi-exit/restart/seams). - GATE PASS: MARKET-path-critical flaws 1,2,5 confirmed fixed + green. - Added SPRINT0_FLAW_VERIFICATION.md report and _rust_kernel/.gitignore (excludes Rust target/ build artifacts). LIMIT/partial-fill remain explicitly out of scope (MARKET-only bring-up). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
95
prod/clean_arch/dita_v2/_backup_20260530/__init__.py
Normal file
95
prod/clean_arch/dita_v2/_backup_20260530/__init__.py
Normal 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",
|
||||
]
|
||||
337
prod/clean_arch/dita_v2/_backup_20260530/_build_pink_bodies.py
Normal file
337
prod/clean_arch/dita_v2/_backup_20260530/_build_pink_bodies.py
Normal 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")
|
||||
170
prod/clean_arch/dita_v2/_backup_20260530/_build_pink_extended.py
Normal file
170
prod/clean_arch/dita_v2/_backup_20260530/_build_pink_extended.py
Normal 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")
|
||||
1244
prod/clean_arch/dita_v2/_backup_20260530/_gen_test.py
Normal file
1244
prod/clean_arch/dita_v2/_backup_20260530/_gen_test.py
Normal file
File diff suppressed because it is too large
Load Diff
123
prod/clean_arch/dita_v2/_backup_20260530/account.py
Normal file
123
prod/clean_arch/dita_v2/_backup_20260530/account.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""Account projection for DITAv2."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Iterable, Optional
|
||||
import math
|
||||
|
||||
from .contracts import TradeSide, TradeSlot, TradeStage
|
||||
from .utils import safe_float
|
||||
|
||||
|
||||
@dataclass
|
||||
class AccountSnapshot:
|
||||
"""Derived account state."""
|
||||
|
||||
capital: float
|
||||
equity: float
|
||||
realized_pnl: float = 0.0
|
||||
unrealized_pnl: float = 0.0
|
||||
open_positions: int = 0
|
||||
open_notional: float = 0.0
|
||||
fees_paid: float = 0.0
|
||||
trade_seq: int = 0
|
||||
peak_capital: float = 0.0
|
||||
|
||||
@property
|
||||
def leverage(self) -> float:
|
||||
if self.capital <= 0 or self.open_notional <= 0:
|
||||
return 0.0
|
||||
return self.open_notional / self.capital
|
||||
|
||||
|
||||
@dataclass
|
||||
class AccountProjection:
|
||||
"""Aggregate account view over all active slots."""
|
||||
|
||||
runtime_namespace: str = "dita_v2"
|
||||
strategy_namespace: str = "dita_v2"
|
||||
event_namespace: str = "dita_v2"
|
||||
actor_name: str = "ExecutionKernel"
|
||||
exec_venue: str = "bingx"
|
||||
data_venue: str = "binance"
|
||||
ledger_authority: str = "exchange"
|
||||
min_capital: float = 0.0
|
||||
max_capital: Optional[float] = None
|
||||
snapshot: AccountSnapshot = field(default_factory=lambda: AccountSnapshot(capital=25_000.0, equity=25_000.0))
|
||||
|
||||
def observe_slots(self, slots: Iterable[TradeSlot]) -> None:
|
||||
open_positions = 0
|
||||
open_notional = 0.0
|
||||
unrealized_pnl = 0.0
|
||||
for slot in slots:
|
||||
if slot.closed or slot.size <= 0:
|
||||
continue
|
||||
if slot.fsm_state in {TradeStage.POSITION_OPEN, TradeStage.POSITION_OPENED, TradeStage.ENTRY_WORKING, TradeStage.EXIT_WORKING}:
|
||||
open_positions += 1
|
||||
mark = safe_float(slot.entry_price, 0.0)
|
||||
mark = safe_float(slot.metadata.get("mark_price"), mark)
|
||||
open_notional += abs(slot.size) * abs(mark)
|
||||
unrealized_pnl += float(slot.unrealized_pnl or 0.0)
|
||||
self.snapshot.open_positions = open_positions
|
||||
self.snapshot.open_notional = open_notional
|
||||
self.snapshot.unrealized_pnl = unrealized_pnl
|
||||
self.snapshot.equity = self.snapshot.capital + unrealized_pnl
|
||||
if not math.isfinite(self.snapshot.equity):
|
||||
self.snapshot.equity = self.snapshot.capital
|
||||
if open_notional > 0 and self.snapshot.capital > 0:
|
||||
self.snapshot.peak_capital = max(self.snapshot.peak_capital, self.snapshot.capital)
|
||||
|
||||
def settle(self, realized_pnl: float, fees: float = 0.0) -> None:
|
||||
realized_pnl = safe_float(realized_pnl, 0.0)
|
||||
new_capital = safe_float(self.snapshot.capital + realized_pnl, self.snapshot.capital)
|
||||
if self.max_capital is not None:
|
||||
new_capital = min(new_capital, self.max_capital)
|
||||
new_capital = max(self.min_capital, new_capital)
|
||||
self.snapshot.capital = new_capital
|
||||
self.snapshot.realized_pnl += realized_pnl
|
||||
self.snapshot.fees_paid += safe_float(fees, 0.0)
|
||||
self.snapshot.equity = self.snapshot.capital + self.snapshot.unrealized_pnl
|
||||
if not math.isfinite(self.snapshot.equity):
|
||||
self.snapshot.equity = self.snapshot.capital
|
||||
|
||||
def to_account_event(
|
||||
self,
|
||||
*,
|
||||
timestamp: datetime,
|
||||
trade_id: str,
|
||||
asset: str,
|
||||
side: TradeSide,
|
||||
stage: TradeStage,
|
||||
reason: str,
|
||||
pnl: float = 0.0,
|
||||
pnl_pct: float = 0.0,
|
||||
bars_held: int = 0,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
self.snapshot.equity = self.snapshot.capital + self.snapshot.unrealized_pnl
|
||||
return {
|
||||
"timestamp": timestamp.isoformat() if hasattr(timestamp, "isoformat") else str(timestamp),
|
||||
"runtime_namespace": self.runtime_namespace,
|
||||
"strategy_namespace": self.strategy_namespace,
|
||||
"event_namespace": self.event_namespace,
|
||||
"actor_name": self.actor_name,
|
||||
"exec_venue": self.exec_venue,
|
||||
"data_venue": self.data_venue,
|
||||
"ledger_authority": self.ledger_authority,
|
||||
"capital": float(self.snapshot.capital),
|
||||
"equity": float(self.snapshot.equity),
|
||||
"open_positions": int(self.snapshot.open_positions),
|
||||
"current_open_notional": float(self.snapshot.open_notional),
|
||||
"current_account_leverage": float(self.snapshot.leverage),
|
||||
"trade_id": trade_id,
|
||||
"asset": asset,
|
||||
"side": side.value,
|
||||
"reason": reason,
|
||||
"stage": stage.value,
|
||||
"pnl": float(pnl),
|
||||
"pnl_pct": float(pnl_pct),
|
||||
"bars_held": int(bars_held),
|
||||
"metadata": dict(metadata or {}),
|
||||
}
|
||||
590
prod/clean_arch/dita_v2/_backup_20260530/bingx_venue.py
Normal file
590
prod/clean_arch/dita_v2/_backup_20260530/bingx_venue.py
Normal file
@@ -0,0 +1,590 @@
|
||||
"""DITAv2 BingX venue adapter.
|
||||
|
||||
This is a thin normalization layer over the existing direct BingX execution
|
||||
surface. It converts BingX REST/account/order payloads into DITAv2
|
||||
``VenueEvent`` / ``VenueOrder`` objects without reimplementing exchange logic.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import concurrent.futures
|
||||
import inspect
|
||||
import itertools
|
||||
import re
|
||||
import threading
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Iterable, List, Optional
|
||||
|
||||
from prod.clean_arch.dita import DecisionAction as LegacyDecisionAction
|
||||
from prod.clean_arch.dita import Intent as LegacyIntent
|
||||
from prod.clean_arch.dita import TradeSide as LegacyTradeSide
|
||||
|
||||
from prod.bingx.http import BingxHttpError
|
||||
|
||||
from .contracts import (
|
||||
KernelCommandType,
|
||||
KernelEventKind,
|
||||
KernelIntent,
|
||||
TradeSide,
|
||||
VenueEvent,
|
||||
VenueEventStatus,
|
||||
VenueOrder,
|
||||
VenueOrderStatus,
|
||||
)
|
||||
from .utils import json_safe
|
||||
from .utils import safe_float
|
||||
from .venue import VenueAdapter
|
||||
|
||||
|
||||
def _row_text(row: dict[str, Any], *keys: str, default: str = "") -> str:
|
||||
for key in keys:
|
||||
value = row.get(key)
|
||||
if value is None:
|
||||
continue
|
||||
text = str(value)
|
||||
if text:
|
||||
return text
|
||||
return default
|
||||
|
||||
|
||||
def _row_float(row: dict[str, Any], *keys: str, default: float = 0.0) -> float:
|
||||
for key in keys:
|
||||
try:
|
||||
value = float(row.get(key) or 0.0)
|
||||
except Exception:
|
||||
continue
|
||||
if value == value and value not in (float("inf"), float("-inf")) and value != 0.0:
|
||||
return value
|
||||
return default
|
||||
|
||||
|
||||
def _normalize_status(status: str) -> str:
|
||||
return str(status or "").strip().upper()
|
||||
|
||||
|
||||
def _trade_side_from_row(row: dict[str, Any], *, fallback: TradeSide = TradeSide.FLAT) -> TradeSide:
|
||||
side_raw = _row_text(row, "side", "positionSide", default="").upper()
|
||||
signed_qty = _row_float(row, "positionAmt", "positionQty", "positionSize", "quantity", "pa", default=0.0)
|
||||
if side_raw in {"BUY", "LONG"}:
|
||||
return TradeSide.LONG
|
||||
if side_raw in {"SELL", "SHORT"}:
|
||||
return TradeSide.SHORT
|
||||
if signed_qty < 0:
|
||||
return TradeSide.SHORT
|
||||
if signed_qty > 0:
|
||||
return TradeSide.LONG
|
||||
return fallback
|
||||
|
||||
|
||||
def _venue_event_status_from_row(status: str) -> VenueEventStatus:
|
||||
normalized = _normalize_status(status)
|
||||
if normalized in {"NEW", "ACKED", "PENDING", "CREATED"}:
|
||||
return VenueEventStatus.ACKED
|
||||
if normalized in {"RATE_LIMITED", "THROTTLED"}:
|
||||
return VenueEventStatus.RATE_LIMITED
|
||||
if normalized in {"PARTIALLY_FILLED", "PARTIAL_FILL"}:
|
||||
return VenueEventStatus.PARTIALLY_FILLED
|
||||
if normalized in {"FILLED", "FULL_FILL"}:
|
||||
return VenueEventStatus.FILLED
|
||||
if normalized in {"CANCELED", "CANCELLED", "EXPIRED"}:
|
||||
return VenueEventStatus.CANCELED
|
||||
if normalized in {"REJECTED", "FAILED"}:
|
||||
return VenueEventStatus.REJECTED
|
||||
if normalized in {"CANCEL_REJECTED", "CANCEL_REJECT"}:
|
||||
return VenueEventStatus.CANCELED_REJECTED
|
||||
return VenueEventStatus.ACKED
|
||||
|
||||
|
||||
def _venue_order_status_from_row(status: str) -> VenueOrderStatus:
|
||||
normalized = _normalize_status(status)
|
||||
if normalized in {"NEW", "ACKED", "PENDING", "CREATED"}:
|
||||
return VenueOrderStatus.NEW
|
||||
if normalized in {"RATE_LIMITED", "THROTTLED"}:
|
||||
return VenueOrderStatus.NEW
|
||||
if normalized in {"PARTIALLY_FILLED", "PARTIAL_FILL"}:
|
||||
return VenueOrderStatus.PARTIALLY_FILLED
|
||||
if normalized in {"FILLED", "FULL_FILL"}:
|
||||
return VenueOrderStatus.FILLED
|
||||
if normalized in {"CANCELED", "CANCELLED", "EXPIRED"}:
|
||||
return VenueOrderStatus.CANCELED
|
||||
if normalized in {"REJECTED", "FAILED"}:
|
||||
return VenueOrderStatus.REJECTED
|
||||
return VenueOrderStatus.NEW
|
||||
|
||||
|
||||
def _position_qty(row: dict[str, Any]) -> float:
|
||||
qty = _row_float(row, "positionAmt", "positionQty", "positionSize", "quantity", "pa", default=0.0)
|
||||
if qty != 0.0:
|
||||
return abs(qty)
|
||||
return abs(_row_float(row, "executedQty", "filledQty", "z", default=0.0))
|
||||
|
||||
|
||||
def _position_price(row: dict[str, Any]) -> float:
|
||||
return _row_float(row, "entryPrice", "avgPrice", "avgEntryPrice", "ep", "ap", "price", "lastFillPrice", "tradePrice")
|
||||
|
||||
|
||||
def _mapping_for_snapshot(rows: Iterable[dict[str, Any]]) -> dict[str, dict[str, Any]]:
|
||||
mapping: dict[str, dict[str, Any]] = {}
|
||||
for row in rows:
|
||||
client_id = _row_text(row, "clientOrderID", "clientOrderId", default="")
|
||||
order_id = _row_text(row, "orderId", "orderID", "id", default="")
|
||||
key = client_id or order_id
|
||||
if key:
|
||||
mapping[key] = dict(row)
|
||||
if order_id and order_id not in mapping:
|
||||
mapping[order_id] = dict(row)
|
||||
return mapping
|
||||
|
||||
|
||||
def _venue_order_from_row(
|
||||
row: dict[str, Any],
|
||||
*,
|
||||
internal_trade_id: str = "",
|
||||
fallback_side: TradeSide = TradeSide.FLAT,
|
||||
) -> VenueOrder:
|
||||
side = _trade_side_from_row(row, fallback=fallback_side)
|
||||
client_id = _row_text(row, "clientOrderID", "clientOrderId", default="")
|
||||
order_id = _row_text(row, "orderId", "orderID", "id", default="")
|
||||
intended = _row_float(row, "origQty", "quantity", "q", "positionAmt", "positionQty", default=0.0)
|
||||
if intended <= 0:
|
||||
intended = _position_qty(row)
|
||||
return VenueOrder(
|
||||
internal_trade_id=internal_trade_id or client_id or order_id,
|
||||
venue_order_id=order_id,
|
||||
venue_client_id=client_id,
|
||||
side=side,
|
||||
intended_size=abs(float(intended or 0.0)),
|
||||
filled_size=abs(_row_float(row, "executedQty", "filledQty", "z", "lastFilledQty", default=0.0)),
|
||||
average_fill_price=_position_price(row),
|
||||
status=_venue_order_status_from_row(_row_text(row, "status", "X", default="NEW")),
|
||||
metadata={"raw": dict(row)},
|
||||
)
|
||||
|
||||
|
||||
def _event_id(seq: itertools.count) -> str:
|
||||
return f"EV-{next(seq):08d}"
|
||||
|
||||
|
||||
def _rate_limit_retry_after_ms(row: dict[str, Any]) -> int:
|
||||
raw_retry = row.get("retryAfter") or row.get("retry_after_ms") or row.get("retryAfterMs")
|
||||
if raw_retry is None:
|
||||
msg = _row_text(row, "msg", "message", default="")
|
||||
match = re.search(r"unblocked after (\d+)", msg)
|
||||
if match:
|
||||
try:
|
||||
ts = int(match.group(1))
|
||||
now_ms = int(datetime.now(timezone.utc).timestamp() * 1000)
|
||||
return max(0, ts - now_ms)
|
||||
except Exception:
|
||||
return 0
|
||||
return 0
|
||||
try:
|
||||
return max(0, int(float(raw_retry)))
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
class BingxVenueAdapter(VenueAdapter):
|
||||
"""Normalizes BingX execution responses into DITAv2 venue events."""
|
||||
|
||||
# Shared thread-pool executor reused across all adapter instances and
|
||||
# all calls. Threads are created once and recycled, eliminating the
|
||||
# per-call creation/destruction overhead of the old pattern.
|
||||
_EXECUTOR: concurrent.futures.ThreadPoolExecutor | None = None
|
||||
_EXECUTOR_LOCK: threading.Lock = threading.Lock()
|
||||
|
||||
@classmethod
|
||||
def _get_executor(cls) -> concurrent.futures.ThreadPoolExecutor:
|
||||
if cls._EXECUTOR is None:
|
||||
with cls._EXECUTOR_LOCK:
|
||||
if cls._EXECUTOR is None:
|
||||
# max_workers=3 so three concurrent HTTP calls (balance,
|
||||
# positions, openOrders) can proceed simultaneously without
|
||||
# serialising on the pool.
|
||||
cls._EXECUTOR = concurrent.futures.ThreadPoolExecutor(
|
||||
max_workers=3,
|
||||
thread_name_prefix="bingx_adapter",
|
||||
)
|
||||
return cls._EXECUTOR
|
||||
|
||||
def __init__(self, backend: Any | None = None, *, config: Any | None = None) -> None:
|
||||
if backend is None:
|
||||
if config is None:
|
||||
raise ValueError("BingxVenueAdapter requires a backend or config")
|
||||
from prod.clean_arch.adapters.bingx_direct import BingxDirectExecutionAdapter
|
||||
|
||||
backend = BingxDirectExecutionAdapter(config)
|
||||
self.backend = backend
|
||||
self._event_seq = itertools.count(1)
|
||||
# Thread-safe snapshot cache — reads from a snapshot may arrive from
|
||||
# the kernel thread while _backend_snapshot writes from the pool thread.
|
||||
self._snap_lock = threading.Lock()
|
||||
self._last_snapshot = None
|
||||
self._snapshot_ready = threading.Event()
|
||||
self._snapshot_ready.set() # initially ready (no pending write)
|
||||
|
||||
def _run(self, result: Any) -> Any:
|
||||
if inspect.isawaitable(result):
|
||||
try:
|
||||
asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
return asyncio.run(result)
|
||||
# Inside a running event loop: submit to the shared singleton
|
||||
# executor so threads are reused across calls.
|
||||
pool = self._get_executor()
|
||||
return pool.submit(asyncio.run, result).result()
|
||||
return result
|
||||
|
||||
def _call_backend(self, method_name: str, *args: Any, **kwargs: Any) -> Any:
|
||||
method = getattr(self.backend, method_name, None)
|
||||
if method is None:
|
||||
raise AttributeError(f"backend has no method {method_name}")
|
||||
return self._run(method(*args, **kwargs))
|
||||
|
||||
def _backend_snapshot(self, *, include_history: bool = False, timeout_ms: float = 5000.0):
|
||||
"""Fetch a fresh snapshot from the backend and cache it thread-safely.
|
||||
|
||||
Design (industry best-practice reader-writer pattern):
|
||||
- A caller that needs a fresh snapshot *waits* on ``_snapshot_ready``
|
||||
before reading, so it never sees a stale partial write.
|
||||
- While a snapshot fetch is in-flight, the lock is cleared; concurrent
|
||||
callers block on ``_snapshot_ready`` with a timeout. If the fetch
|
||||
succeeds in time they get the fresh snapshot; if it times out they
|
||||
fall back to ``_last_snapshot`` (an eventually-consistent design —
|
||||
stale data that *was* consistent is safer than no data).
|
||||
- The write is guarded by ``_snap_lock`` so concurrent writes are
|
||||
serialised and ``_last_snapshot`` is never partially assigned.
|
||||
"""
|
||||
if not self._snapshot_ready.wait(timeout=timeout_ms / 1000.0):
|
||||
# Timeout waiting for a previous snapshot write — return the
|
||||
# last-known-good snapshot rather than blocking the caller.
|
||||
with self._snap_lock:
|
||||
return self._last_snapshot
|
||||
|
||||
self._snapshot_ready.clear()
|
||||
try:
|
||||
snapshot = self._call_backend("refresh_state", None, include_history=include_history)
|
||||
except Exception:
|
||||
self._snapshot_ready.set()
|
||||
raise
|
||||
|
||||
with self._snap_lock:
|
||||
self._last_snapshot = snapshot
|
||||
self._snapshot_ready.set()
|
||||
return snapshot
|
||||
|
||||
@staticmethod
|
||||
def _legacy_intent(intent: KernelIntent) -> LegacyIntent:
|
||||
action = LegacyDecisionAction.ENTER if intent.action == KernelCommandType.ENTER else LegacyDecisionAction.EXIT
|
||||
side = LegacyTradeSide.SHORT if intent.side == TradeSide.SHORT else LegacyTradeSide.LONG
|
||||
return LegacyIntent(
|
||||
timestamp=intent.timestamp,
|
||||
trade_id=intent.trade_id,
|
||||
decision_id=intent.intent_id,
|
||||
asset=intent.asset,
|
||||
action=action,
|
||||
side=side,
|
||||
reason=intent.reason,
|
||||
target_size=float(intent.target_size),
|
||||
leverage=float(intent.leverage),
|
||||
reference_price=float(intent.reference_price),
|
||||
confidence=1.0,
|
||||
bars_held=0,
|
||||
exit_leg_ratios=tuple(intent.exit_leg_ratios or (1.0,)),
|
||||
metadata=dict(intent.metadata),
|
||||
)
|
||||
|
||||
def connect(self) -> bool:
|
||||
result = getattr(self.backend, "connect", None)
|
||||
if result is not None:
|
||||
self._run(result())
|
||||
self._backend_snapshot(include_history=True)
|
||||
return True
|
||||
|
||||
def cancel(self, order: VenueOrder, *, reason: str = "") -> List[VenueEvent]:
|
||||
snapshot_before = self._backend_snapshot(include_history=True)
|
||||
response = None
|
||||
if hasattr(self.backend, "cancel_order"):
|
||||
response = self._call_backend("cancel_order", order, reason=reason)
|
||||
elif hasattr(self.backend, "cancel"):
|
||||
response = self._call_backend("cancel", order, reason=reason)
|
||||
else:
|
||||
client = getattr(self.backend, "_client", None)
|
||||
instrument_symbol = ""
|
||||
if hasattr(self.backend, "_instrument_venue_symbol"):
|
||||
asset = str(order.metadata.get("asset") or order.internal_trade_id or order.venue_client_id or "")
|
||||
instrument_symbol = str(self.backend._instrument_venue_symbol(asset))
|
||||
if client is None or not instrument_symbol:
|
||||
raise RuntimeError("backend does not expose a cancel surface")
|
||||
params = {"symbol": instrument_symbol}
|
||||
if order.venue_order_id:
|
||||
params["orderId"] = order.venue_order_id
|
||||
else:
|
||||
params["clientOrderId"] = order.venue_client_id
|
||||
try:
|
||||
response = self._run(client.signed_delete("/openApi/swap/v2/trade/order", params))
|
||||
except BingxHttpError as exc:
|
||||
response = {"status": "REJECTED", "msg": str(exc), "orderId": order.venue_order_id, "clientOrderId": order.venue_client_id}
|
||||
snapshot_after = self._backend_snapshot(include_history=True)
|
||||
return self._events_from_cancel(order, response, snapshot_before, snapshot_after, reason=reason)
|
||||
|
||||
def open_orders(self) -> List[VenueOrder]:
|
||||
snapshot = self._backend_snapshot(include_history=False)
|
||||
return [_venue_order_from_row(row) for row in (snapshot.open_orders or [])]
|
||||
|
||||
def open_positions(self) -> List[dict[str, Any]]:
|
||||
snapshot = self._backend_snapshot(include_history=False)
|
||||
return [dict(row) for row in (snapshot.open_positions or {}).values()]
|
||||
|
||||
def reconcile(self) -> List[VenueEvent]:
|
||||
snapshot = self._backend_snapshot(include_history=True)
|
||||
return self._events_from_snapshot(snapshot)
|
||||
|
||||
def submit(self, intent: KernelIntent) -> List[VenueEvent]:
|
||||
snapshot_before = self._backend_snapshot(include_history=True)
|
||||
receipt = self._call_backend("submit_intent", self._legacy_intent(intent))
|
||||
snapshot_after = self._backend_snapshot(include_history=True)
|
||||
return self._events_from_submit(intent, receipt, snapshot_before, snapshot_after)
|
||||
|
||||
def _events_from_submit(self, intent: KernelIntent, receipt: Any, before, after) -> List[VenueEvent]: # noqa: ANN001
|
||||
ack_row = dict(getattr(receipt, "raw_ack", {}) or {})
|
||||
status = _normalize_status(getattr(receipt, "status", "") or _row_text(ack_row, "status", default="NEW"))
|
||||
order_id = _row_text(ack_row, "orderId", "orderID", default=str(getattr(receipt, "order_id", "") or ""))
|
||||
client_order_id = _row_text(ack_row, "clientOrderID", "clientOrderId", default=str(getattr(receipt, "client_order_id", "") or intent.intent_id))
|
||||
if status in {"RATE_LIMITED", "THROTTLED"}:
|
||||
return [
|
||||
VenueEvent(
|
||||
timestamp=getattr(receipt, "timestamp", datetime.now(timezone.utc)),
|
||||
event_id=_event_id(self._event_seq),
|
||||
trade_id=intent.trade_id,
|
||||
slot_id=intent.slot_id,
|
||||
kind=KernelEventKind.RATE_LIMITED,
|
||||
status=VenueEventStatus.RATE_LIMITED,
|
||||
venue_order_id=order_id,
|
||||
venue_client_id=client_order_id,
|
||||
side=intent.side,
|
||||
asset=intent.asset,
|
||||
price=safe_float(getattr(receipt, "price", 0.0), 0.0),
|
||||
size=float(intent.target_size or 0.0),
|
||||
filled_size=0.0,
|
||||
remaining_size=float(intent.target_size or 0.0),
|
||||
reason=_row_text(ack_row, "msg", "message", default="BINGX_RATE_LIMITED"),
|
||||
raw_payload=ack_row or json_safe(receipt),
|
||||
metadata={"intent_id": intent.intent_id, "action": intent.action.value, "retry_after_ms": _rate_limit_retry_after_ms(ack_row)},
|
||||
)
|
||||
]
|
||||
base_event = VenueEvent(
|
||||
timestamp=getattr(receipt, "timestamp", datetime.now(timezone.utc)),
|
||||
event_id=_event_id(self._event_seq),
|
||||
trade_id=intent.trade_id,
|
||||
slot_id=intent.slot_id,
|
||||
kind=KernelEventKind.ORDER_ACK,
|
||||
status=VenueEventStatus.ACKED,
|
||||
venue_order_id=order_id,
|
||||
venue_client_id=client_order_id,
|
||||
side=intent.side,
|
||||
asset=intent.asset,
|
||||
price=safe_float(getattr(receipt, "price", 0.0), 0.0),
|
||||
size=float(intent.target_size or 0.0),
|
||||
filled_size=0.0,
|
||||
remaining_size=float(intent.target_size or 0.0),
|
||||
reason="",
|
||||
raw_payload=ack_row or json_safe(receipt),
|
||||
metadata={"intent_id": intent.intent_id, "action": intent.action.value},
|
||||
)
|
||||
if status in {"REJECTED", "FAILED"}:
|
||||
return [
|
||||
VenueEvent(
|
||||
**{**base_event.__dict__, "event_id": _event_id(self._event_seq), "kind": KernelEventKind.ORDER_REJECT, "status": VenueEventStatus.REJECTED, "reason": _row_text(ack_row, "msg", "message", default="BINGX_ORDER_REJECTED")},
|
||||
)
|
||||
]
|
||||
events = [base_event]
|
||||
fill_status = _venue_event_status_from_row(status)
|
||||
filled_size = _row_float(ack_row, "executedQty", "cumFilledQty", "filledQty", "lastFilledQty", default=0.0)
|
||||
snapshot_fill_size = self._filled_size_from_snapshots(before, after, intent.asset)
|
||||
if filled_size <= 0:
|
||||
filled_size = snapshot_fill_size
|
||||
emit_fill = fill_status in {VenueEventStatus.PARTIALLY_FILLED, VenueEventStatus.FILLED} or snapshot_fill_size > 0.0
|
||||
if emit_fill:
|
||||
if filled_size <= 0:
|
||||
filled_size = float(intent.target_size or 0.0)
|
||||
remaining_size = max(0.0, float(intent.target_size or 0.0) - float(filled_size))
|
||||
fill_kind = KernelEventKind.FULL_FILL if fill_status == VenueEventStatus.FILLED or remaining_size <= 1e-12 else KernelEventKind.PARTIAL_FILL
|
||||
events.append(
|
||||
VenueEvent(
|
||||
timestamp=base_event.timestamp,
|
||||
event_id=_event_id(self._event_seq),
|
||||
trade_id=intent.trade_id,
|
||||
slot_id=intent.slot_id,
|
||||
kind=fill_kind,
|
||||
status=VenueEventStatus.FILLED if fill_kind == KernelEventKind.FULL_FILL else VenueEventStatus.PARTIALLY_FILLED,
|
||||
venue_order_id=order_id,
|
||||
venue_client_id=client_order_id,
|
||||
side=intent.side,
|
||||
asset=intent.asset,
|
||||
price=safe_float(_row_float(ack_row, "avgPrice", "ap", "price", "lastFillPrice", default=getattr(receipt, "price", 0.0)), 0.0),
|
||||
size=float(intent.target_size or 0.0),
|
||||
filled_size=float(filled_size),
|
||||
remaining_size=float(remaining_size),
|
||||
reason="",
|
||||
raw_payload=ack_row or json_safe(receipt),
|
||||
metadata={"intent_id": intent.intent_id, "action": intent.action.value},
|
||||
)
|
||||
)
|
||||
return events
|
||||
|
||||
def _events_from_cancel(self, order: VenueOrder, response: Any, before, after, *, reason: str = "") -> List[VenueEvent]: # noqa: ANN001
|
||||
raw = response if isinstance(response, dict) else {}
|
||||
status = _normalize_status(_row_text(raw, "status", default="CANCELED"))
|
||||
if status in {"RATE_LIMITED", "THROTTLED"}:
|
||||
return [
|
||||
VenueEvent(
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
event_id=_event_id(self._event_seq),
|
||||
trade_id=order.internal_trade_id or order.venue_client_id,
|
||||
slot_id=int(order.metadata.get("slot_id", 0) or 0),
|
||||
kind=KernelEventKind.RATE_LIMITED,
|
||||
status=VenueEventStatus.RATE_LIMITED,
|
||||
venue_order_id=order.venue_order_id,
|
||||
venue_client_id=order.venue_client_id,
|
||||
side=order.side,
|
||||
asset=str(order.metadata.get("asset") or ""),
|
||||
price=safe_float(_row_float(raw, "avgPrice", "ap", "price", "lastFillPrice", default=order.average_fill_price), 0.0),
|
||||
size=float(order.intended_size or 0.0),
|
||||
filled_size=float(order.filled_size or 0.0),
|
||||
remaining_size=float(order.remaining_size),
|
||||
reason=reason or _row_text(raw, "msg", "message", default="BINGX_RATE_LIMITED"),
|
||||
raw_payload=raw or {"orderId": order.venue_order_id, "clientOrderId": order.venue_client_id, "status": status or "RATE_LIMITED"},
|
||||
metadata={**dict(order.metadata), "retry_after_ms": _rate_limit_retry_after_ms(raw)},
|
||||
)
|
||||
]
|
||||
event_status = _venue_event_status_from_row(status)
|
||||
kind = KernelEventKind.CANCEL_ACK if event_status == VenueEventStatus.CANCELED else KernelEventKind.CANCEL_REJECT
|
||||
if event_status == VenueEventStatus.CANCELED_REJECTED:
|
||||
kind = KernelEventKind.CANCEL_REJECT
|
||||
return [
|
||||
VenueEvent(
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
event_id=_event_id(self._event_seq),
|
||||
trade_id=order.internal_trade_id or order.venue_client_id,
|
||||
slot_id=int(order.metadata.get("slot_id", 0) or 0),
|
||||
kind=kind,
|
||||
status=event_status,
|
||||
venue_order_id=order.venue_order_id,
|
||||
venue_client_id=order.venue_client_id,
|
||||
side=order.side,
|
||||
asset=str(order.metadata.get("asset") or ""),
|
||||
price=safe_float(_row_float(raw, "avgPrice", "ap", "price", "lastFillPrice", default=order.average_fill_price), 0.0),
|
||||
size=float(order.intended_size or 0.0),
|
||||
filled_size=float(order.filled_size or 0.0),
|
||||
remaining_size=float(order.remaining_size),
|
||||
reason=reason or _row_text(raw, "msg", "message", default="BINGX_CANCEL_ACK" if kind == KernelEventKind.CANCEL_ACK else "BINGX_CANCEL_REJECT"),
|
||||
raw_payload=raw or {"orderId": order.venue_order_id, "clientOrderId": order.venue_client_id, "status": status or event_status.value},
|
||||
metadata=dict(order.metadata),
|
||||
)
|
||||
]
|
||||
|
||||
def _events_from_snapshot(self, snapshot: Any) -> List[VenueEvent]: # noqa: ANN001
|
||||
events: list[VenueEvent] = []
|
||||
seen: set[tuple[str, str, str]] = set()
|
||||
for row in getattr(snapshot, "open_orders", []) or []:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
event = self._event_from_row(row, slot_id=0)
|
||||
key = (event.venue_client_id, event.venue_order_id, event.kind.value)
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
events.append(event)
|
||||
for row in getattr(snapshot, "all_orders", []) or []:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
event = self._event_from_row(row, slot_id=0)
|
||||
key = (event.venue_client_id, event.venue_order_id, event.kind.value)
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
events.append(event)
|
||||
for row in getattr(snapshot, "all_fills", []) or []:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
event = self._fill_event_from_row(row)
|
||||
key = (event.venue_client_id, event.venue_order_id, event.kind.value)
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
events.append(event)
|
||||
return events
|
||||
|
||||
def _event_from_row(self, row: dict[str, Any], *, slot_id: int) -> VenueEvent:
|
||||
status = _normalize_status(_row_text(row, "status", "X", default="NEW"))
|
||||
event_status = _venue_event_status_from_row(status)
|
||||
kind = {
|
||||
VenueEventStatus.ACKED: KernelEventKind.ORDER_ACK,
|
||||
VenueEventStatus.PARTIALLY_FILLED: KernelEventKind.PARTIAL_FILL,
|
||||
VenueEventStatus.FILLED: KernelEventKind.FULL_FILL,
|
||||
VenueEventStatus.CANCELED: KernelEventKind.CANCEL_ACK,
|
||||
VenueEventStatus.REJECTED: KernelEventKind.ORDER_REJECT,
|
||||
VenueEventStatus.CANCELED_REJECTED: KernelEventKind.CANCEL_REJECT,
|
||||
VenueEventStatus.RATE_LIMITED: KernelEventKind.RATE_LIMITED,
|
||||
}.get(event_status, KernelEventKind.ORDER_ACK)
|
||||
size = _row_float(row, "origQty", "quantity", "q", "positionAmt", default=0.0)
|
||||
filled = _row_float(row, "executedQty", "cumFilledQty", "filledQty", "z", "lastFilledQty", default=0.0)
|
||||
if filled <= 0.0 and kind in {KernelEventKind.PARTIAL_FILL, KernelEventKind.FULL_FILL}:
|
||||
filled = size
|
||||
return VenueEvent(
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
event_id=_event_id(self._event_seq),
|
||||
trade_id=_row_text(row, "tradeId", "trade_id", default=_row_text(row, "clientOrderId", "clientOrderID", default="")),
|
||||
slot_id=slot_id,
|
||||
kind=kind,
|
||||
status=event_status,
|
||||
venue_order_id=_row_text(row, "orderId", "orderID", "id", default=""),
|
||||
venue_client_id=_row_text(row, "clientOrderID", "clientOrderId", "c", default=""),
|
||||
side=_trade_side_from_row(row),
|
||||
asset=_row_text(row, "symbol", default=""),
|
||||
price=safe_float(_row_float(row, "avgPrice", "ap", "price", "lastFillPrice", default=0.0), 0.0),
|
||||
size=abs(float(size or 0.0)),
|
||||
filled_size=abs(float(filled or 0.0)),
|
||||
remaining_size=max(0.0, abs(float(size or 0.0)) - abs(float(filled or 0.0))),
|
||||
reason=_row_text(row, "msg", "message", default=""),
|
||||
raw_payload=dict(row),
|
||||
metadata={"source": "bingx"},
|
||||
)
|
||||
|
||||
def _fill_event_from_row(self, row: dict[str, Any]) -> VenueEvent:
|
||||
status = _normalize_status(_row_text(row, "status", "X", default="FILLED"))
|
||||
event_status = _venue_event_status_from_row(status)
|
||||
kind = KernelEventKind.FULL_FILL if event_status == VenueEventStatus.FILLED else KernelEventKind.PARTIAL_FILL
|
||||
return VenueEvent(
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
event_id=_event_id(self._event_seq),
|
||||
trade_id=_row_text(row, "tradeId", "trade_id", default=_row_text(row, "clientOrderId", "clientOrderID", default="")),
|
||||
slot_id=0,
|
||||
kind=kind,
|
||||
status=event_status,
|
||||
venue_order_id=_row_text(row, "orderId", "orderID", "id", default=""),
|
||||
venue_client_id=_row_text(row, "clientOrderID", "clientOrderId", "c", default=""),
|
||||
side=_trade_side_from_row(row),
|
||||
asset=_row_text(row, "symbol", default=""),
|
||||
price=safe_float(_row_float(row, "lastFillPrice", "L", "price", "ap", default=0.0), 0.0),
|
||||
size=abs(_row_float(row, "executedQty", "z", "lastFilledQty", default=0.0)),
|
||||
filled_size=abs(_row_float(row, "lastFilledQty", "l", "z", default=0.0)),
|
||||
remaining_size=max(0.0, abs(_row_float(row, "executedQty", "z", "lastFilledQty", default=0.0)) - abs(_row_float(row, "lastFilledQty", "l", "z", default=0.0))),
|
||||
reason=_row_text(row, "msg", "message", default=""),
|
||||
raw_payload=dict(row),
|
||||
metadata={"source": "bingx"},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _filled_size_from_snapshots(before: Any, after: Any, asset: str) -> float: # noqa: ANN001
|
||||
def _lookup(snapshot: Any) -> float:
|
||||
positions = getattr(snapshot, "open_positions", {}) or {}
|
||||
for key, row in positions.items():
|
||||
symbol = _row_text(row, "symbol", default=str(key))
|
||||
if symbol.replace("-", "").replace("_", "").upper() == asset.replace("-", "").replace("_", "").upper():
|
||||
return _position_qty(row)
|
||||
return 0.0
|
||||
|
||||
before_qty = _lookup(before)
|
||||
after_qty = _lookup(after)
|
||||
diff = abs(before_qty - after_qty)
|
||||
return diff
|
||||
327
prod/clean_arch/dita_v2/_backup_20260530/contracts.py
Normal file
327
prod/clean_arch/dita_v2/_backup_20260530/contracts.py
Normal file
@@ -0,0 +1,327 @@
|
||||
"""Canonical v2 contracts for the DITAv2 execution kernel."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, Mapping, Optional, Sequence, Tuple
|
||||
|
||||
|
||||
class TradeSide(str, Enum):
|
||||
"""Trade side."""
|
||||
|
||||
LONG = "LONG"
|
||||
SHORT = "SHORT"
|
||||
FLAT = "FLAT"
|
||||
|
||||
|
||||
class TradeStage(str, Enum):
|
||||
"""Execution stage for a trade slot."""
|
||||
|
||||
IDLE = "IDLE"
|
||||
DECISION_CREATED = "DECISION_CREATED"
|
||||
INTENT_CREATED = "INTENT_CREATED"
|
||||
ORDER_REQUESTED = "ORDER_REQUESTED"
|
||||
ORDER_SENT = "ORDER_SENT"
|
||||
ORDER_ACKED = "ORDER_ACKED"
|
||||
ORDER_REJECTED = "ORDER_REJECTED"
|
||||
ENTRY_WORKING = "ENTRY_WORKING"
|
||||
PARTIAL_FILL = "PARTIAL_FILL"
|
||||
POSITION_OPENED = "POSITION_OPENED"
|
||||
POSITION_OPEN = "POSITION_OPEN"
|
||||
EXIT_REQUESTED = "EXIT_REQUESTED"
|
||||
EXIT_SENT = "EXIT_SENT"
|
||||
EXIT_ACKED = "EXIT_ACKED"
|
||||
EXIT_REJECTED = "EXIT_REJECTED"
|
||||
EXIT_WORKING = "EXIT_WORKING"
|
||||
POSITION_PARTIALLY_CLOSED = "POSITION_PARTIALLY_CLOSED"
|
||||
POSITION_CLOSED = "POSITION_CLOSED"
|
||||
CLOSED = "CLOSED"
|
||||
TRADE_TERMINAL_WRITTEN = "TRADE_TERMINAL_WRITTEN"
|
||||
STALE_STATE_RECONCILING = "STALE_STATE_RECONCILING"
|
||||
|
||||
|
||||
class KernelCommandType(str, Enum):
|
||||
"""Kernel command types."""
|
||||
|
||||
ENTER = "ENTER"
|
||||
EXIT = "EXIT"
|
||||
MARK_PRICE = "MARK_PRICE"
|
||||
RECONCILE = "RECONCILE"
|
||||
CONTROL = "CONTROL"
|
||||
CANCEL = "CANCEL"
|
||||
|
||||
|
||||
class KernelEventKind(str, Enum):
|
||||
"""Normalized venue event kinds."""
|
||||
|
||||
ORDER_ACK = "ORDER_ACK"
|
||||
ORDER_REJECT = "ORDER_REJECT"
|
||||
RATE_LIMITED = "RATE_LIMITED"
|
||||
PARTIAL_FILL = "PARTIAL_FILL"
|
||||
FULL_FILL = "FULL_FILL"
|
||||
CANCEL_ACK = "CANCEL_ACK"
|
||||
CANCEL_REJECT = "CANCEL_REJECT"
|
||||
MARK_PRICE = "MARK_PRICE"
|
||||
RECONCILE = "RECONCILE"
|
||||
CONTROL = "CONTROL"
|
||||
|
||||
|
||||
class KernelDiagnosticCode(str, Enum):
|
||||
"""Structured diagnostic codes emitted by the kernel."""
|
||||
|
||||
OK = "OK"
|
||||
RATE_LIMITED = "RATE_LIMITED"
|
||||
INVALID_SLOT_ID = "INVALID_SLOT_ID"
|
||||
UNSUPPORTED_INTENT = "UNSUPPORTED_INTENT"
|
||||
SLOT_BUSY = "SLOT_BUSY"
|
||||
NO_OPEN_POSITION = "NO_OPEN_POSITION"
|
||||
NO_ACTIVE_EXIT_ORDER = "NO_ACTIVE_EXIT_ORDER"
|
||||
UNKNOWN_EVENT_KIND = "UNKNOWN_EVENT_KIND"
|
||||
ORDER_REJECTED = "ORDER_REJECTED"
|
||||
ENTRY_ORDER_REJECTED = "ENTRY_ORDER_REJECTED"
|
||||
EXIT_ORDER_REJECTED = "EXIT_ORDER_REJECTED"
|
||||
CANCEL_REJECTED = "CANCEL_REJECTED"
|
||||
STALE_STATE_RECONCILE = "STALE_STATE_RECONCILE"
|
||||
RECONCILED = "RECONCILED"
|
||||
DUPLICATE_EVENT = "DUPLICATE_EVENT"
|
||||
UNRESOLVED_SLOT = "UNRESOLVED_SLOT"
|
||||
INVALID_TRANSITION = "INVALID_TRANSITION"
|
||||
TERMINAL_STATE = "TERMINAL_STATE"
|
||||
|
||||
|
||||
class KernelSeverity(str, Enum):
|
||||
"""Severity classification for kernel outcomes."""
|
||||
|
||||
INFO = "INFO"
|
||||
WARNING = "WARNING"
|
||||
ERROR = "ERROR"
|
||||
CRITICAL = "CRITICAL"
|
||||
|
||||
|
||||
class VenueOrderStatus(str, Enum):
|
||||
"""Order status surface mirrored from venue truth."""
|
||||
|
||||
NEW = "NEW"
|
||||
ACKED = "ACKED"
|
||||
PARTIALLY_FILLED = "PARTIALLY_FILLED"
|
||||
FILLED = "FILLED"
|
||||
CANCELED = "CANCELED"
|
||||
REJECTED = "REJECTED"
|
||||
|
||||
|
||||
class VenueEventStatus(str, Enum):
|
||||
"""Status alias for normalized venue events."""
|
||||
|
||||
ACKED = "ACKED"
|
||||
REJECTED = "REJECTED"
|
||||
RATE_LIMITED = "RATE_LIMITED"
|
||||
PARTIALLY_FILLED = "PARTIALLY_FILLED"
|
||||
FILLED = "FILLED"
|
||||
CANCELED = "CANCELED"
|
||||
CANCELED_REJECTED = "CANCEL_REJECTED"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class VenueOrder:
|
||||
"""Venue-specific order identity and fill state."""
|
||||
|
||||
internal_trade_id: str
|
||||
venue_order_id: str
|
||||
venue_client_id: str
|
||||
side: TradeSide
|
||||
intended_size: float
|
||||
filled_size: float = 0.0
|
||||
average_fill_price: float = 0.0
|
||||
status: VenueOrderStatus = VenueOrderStatus.NEW
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@property
|
||||
def remaining_size(self) -> float:
|
||||
return max(0.0, float(self.intended_size) - float(self.filled_size))
|
||||
|
||||
|
||||
@dataclass
|
||||
class TradeSlot:
|
||||
"""A single execution slot managed by the v2 kernel."""
|
||||
|
||||
slot_id: int
|
||||
trade_id: str = ""
|
||||
asset: str = ""
|
||||
side: TradeSide = TradeSide.FLAT
|
||||
entry_price: float = 0.0
|
||||
size: float = 0.0
|
||||
initial_size: float = 0.0
|
||||
leverage: float = 0.0
|
||||
entry_time: Optional[datetime] = None
|
||||
unrealized_pnl: float = 0.0
|
||||
realized_pnl: float = 0.0
|
||||
closed: bool = False
|
||||
exit_leg_ratios: Tuple[float, ...] = (1.0,)
|
||||
active_leg_index: int = 0
|
||||
active_exit_order: Optional[VenueOrder] = None
|
||||
active_entry_order: Optional[VenueOrder] = None
|
||||
fsm_state: TradeStage = TradeStage.IDLE
|
||||
close_reason: str = ""
|
||||
last_event_time: Optional[datetime] = None
|
||||
seen_event_ids: Tuple[str, ...] = ()
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def is_free(self) -> bool:
|
||||
return self.fsm_state in {TradeStage.IDLE, TradeStage.CLOSED} and float(self.size or 0.0) <= 0.0 and not self.active_entry_order and not self.active_exit_order
|
||||
|
||||
def is_open(self) -> bool:
|
||||
return self.fsm_state in {
|
||||
TradeStage.ENTRY_WORKING,
|
||||
TradeStage.POSITION_OPENED,
|
||||
TradeStage.POSITION_OPEN,
|
||||
TradeStage.EXIT_WORKING,
|
||||
} and not self.closed
|
||||
|
||||
def mark_price(self, price: float) -> None:
|
||||
if price is None or price != price or price <= 0:
|
||||
return
|
||||
self.entry_price = self.entry_price or price
|
||||
if self.entry_price <= 0 or self.size <= 0:
|
||||
self.unrealized_pnl = 0.0
|
||||
return
|
||||
delta = (price - self.entry_price) / self.entry_price
|
||||
if self.side == TradeSide.SHORT:
|
||||
delta = -delta
|
||||
self.unrealized_pnl = delta * self.size * self.entry_price * self.leverage
|
||||
|
||||
def next_exit_ratio(self) -> float:
|
||||
if self.active_leg_index < len(self.exit_leg_ratios):
|
||||
ratio = float(self.exit_leg_ratios[self.active_leg_index])
|
||||
return max(0.0, min(1.0, ratio))
|
||||
return 1.0
|
||||
|
||||
def consume_exit_leg(self) -> float:
|
||||
ratio = self.next_exit_ratio()
|
||||
self.active_leg_index = min(self.active_leg_index + 1, max(len(self.exit_leg_ratios), 1))
|
||||
return ratio
|
||||
|
||||
def remaining_size(self) -> float:
|
||||
return max(0.0, float(self.size))
|
||||
|
||||
def attach_entry_order(self, order: VenueOrder) -> None:
|
||||
self.active_entry_order = order
|
||||
|
||||
def attach_exit_order(self, order: VenueOrder) -> None:
|
||||
self.active_exit_order = order
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
def _order_dict(order: Optional[VenueOrder]) -> Optional[Dict[str, Any]]:
|
||||
if order is None:
|
||||
return None
|
||||
return {
|
||||
"internal_trade_id": order.internal_trade_id,
|
||||
"venue_order_id": order.venue_order_id,
|
||||
"venue_client_id": order.venue_client_id,
|
||||
"side": order.side.value,
|
||||
"intended_size": float(order.intended_size or 0.0),
|
||||
"filled_size": float(order.filled_size or 0.0),
|
||||
"average_fill_price": float(order.average_fill_price or 0.0),
|
||||
"status": order.status.value,
|
||||
"metadata": dict(order.metadata),
|
||||
}
|
||||
|
||||
return {
|
||||
"slot_id": self.slot_id,
|
||||
"trade_id": self.trade_id,
|
||||
"asset": self.asset,
|
||||
"side": self.side.value,
|
||||
"entry_price": float(self.entry_price or 0.0),
|
||||
"size": float(self.size or 0.0),
|
||||
"initial_size": float(self.initial_size or 0.0),
|
||||
"leverage": float(self.leverage or 0.0),
|
||||
"entry_time": self.entry_time.isoformat() if hasattr(self.entry_time, "isoformat") else None,
|
||||
"unrealized_pnl": float(self.unrealized_pnl or 0.0),
|
||||
"realized_pnl": float(self.realized_pnl or 0.0),
|
||||
"closed": bool(self.closed),
|
||||
"exit_leg_ratios": [float(r) for r in self.exit_leg_ratios],
|
||||
"active_leg_index": int(self.active_leg_index or 0),
|
||||
"active_exit_order": _order_dict(self.active_exit_order),
|
||||
"active_entry_order": _order_dict(self.active_entry_order),
|
||||
"fsm_state": self.fsm_state.value,
|
||||
"close_reason": self.close_reason,
|
||||
"last_event_time": self.last_event_time.isoformat() if hasattr(self.last_event_time, "isoformat") else None,
|
||||
"seen_event_ids": list(self.seen_event_ids),
|
||||
"metadata": dict(self.metadata),
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class KernelIntent:
|
||||
"""Command emitted by the algo and written to the hot-path intent region."""
|
||||
|
||||
timestamp: datetime
|
||||
intent_id: str
|
||||
trade_id: str
|
||||
slot_id: int
|
||||
asset: str
|
||||
side: TradeSide
|
||||
action: KernelCommandType
|
||||
reference_price: float
|
||||
target_size: float
|
||||
leverage: float
|
||||
exit_leg_ratios: Tuple[float, ...] = (1.0,)
|
||||
reason: str = ""
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
stage: TradeStage = TradeStage.INTENT_CREATED
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class VenueEvent:
|
||||
"""Normalized venue truth mapped into DITAv2 semantics."""
|
||||
|
||||
timestamp: datetime
|
||||
event_id: str
|
||||
trade_id: str
|
||||
slot_id: int
|
||||
kind: KernelEventKind
|
||||
status: VenueEventStatus
|
||||
venue_order_id: str = ""
|
||||
venue_client_id: str = ""
|
||||
side: TradeSide = TradeSide.FLAT
|
||||
asset: str = ""
|
||||
price: float = 0.0
|
||||
size: float = 0.0
|
||||
filled_size: float = 0.0
|
||||
remaining_size: float = 0.0
|
||||
reason: str = ""
|
||||
raw_payload: Dict[str, Any] = field(default_factory=dict)
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class KernelTransition:
|
||||
"""Durable kernel transition used for debug journaling."""
|
||||
|
||||
timestamp: datetime
|
||||
trade_id: str
|
||||
slot_id: int
|
||||
prev_state: TradeStage
|
||||
next_state: TradeStage
|
||||
trigger: str
|
||||
intent_id: str = ""
|
||||
event_id: str = ""
|
||||
control_mode: str = ""
|
||||
control_verbosity: str = ""
|
||||
details: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class KernelOutcome:
|
||||
"""Result of applying a command or venue event."""
|
||||
|
||||
accepted: bool
|
||||
slot_id: int
|
||||
trade_id: str
|
||||
state: TradeStage
|
||||
diagnostic_code: KernelDiagnosticCode = KernelDiagnosticCode.OK
|
||||
severity: KernelSeverity = KernelSeverity.INFO
|
||||
transitions: Tuple[KernelTransition, ...] = ()
|
||||
emitted_events: Tuple[VenueEvent, ...] = ()
|
||||
details: Dict[str, Any] = field(default_factory=dict)
|
||||
217
prod/clean_arch/dita_v2/_backup_20260530/control.py
Normal file
217
prod/clean_arch/dita_v2/_backup_20260530/control.py
Normal 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)
|
||||
438
prod/clean_arch/dita_v2/_backup_20260530/gen2.py
Normal file
438
prod/clean_arch/dita_v2/_backup_20260530/gen2.py
Normal 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]}")
|
||||
688
prod/clean_arch/dita_v2/_backup_20260530/gen_live_tests.py
Normal file
688
prod/clean_arch/dita_v2/_backup_20260530/gen_live_tests.py
Normal 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]}")
|
||||
@@ -0,0 +1,67 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any, Protocol
|
||||
|
||||
from .contracts import KernelTransition, TradeSlot
|
||||
from .control import KernelControlSnapshot
|
||||
from .journal import _transition_row
|
||||
from .projection import build_position_state_row
|
||||
from .utils import json_safe
|
||||
|
||||
|
||||
class HazelcastClientLike(Protocol):
|
||||
def get_map(self, name: str): ...
|
||||
def get_topic(self, name: str): ...
|
||||
|
||||
|
||||
class HazelcastProjector:
|
||||
"""Durable BLUE/PINK-compatible projection mirror."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client: HazelcastClientLike | None = None,
|
||||
*,
|
||||
active_slots_map: str = "dita_active_slots",
|
||||
events_topic: str = "dita_trade_events",
|
||||
) -> None:
|
||||
self.client = client
|
||||
self.active_slots_map = active_slots_map
|
||||
self.events_topic = events_topic
|
||||
|
||||
def publish_slot(self, slot: TradeSlot) -> None:
|
||||
if self.client is None:
|
||||
return
|
||||
self.client.get_map(self.active_slots_map).put(slot.trade_id, build_position_state_row(slot))
|
||||
|
||||
def publish_event(self, event_type: str, payload: dict[str, Any]) -> None:
|
||||
if self.client is None:
|
||||
return
|
||||
topic = self.client.get_topic(self.events_topic)
|
||||
topic.publish(
|
||||
json.dumps(
|
||||
{"event_type": event_type, "payload": json_safe(payload)},
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
default=str,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class HazelcastRowWriter:
|
||||
"""Callback bridge for ``HazelcastProjection`` writer hooks."""
|
||||
|
||||
def __init__(self, client: HazelcastClientLike) -> None:
|
||||
self.client = client
|
||||
|
||||
def __call__(self, name: str, row: dict[str, Any]) -> None:
|
||||
if name.endswith("trade_events"):
|
||||
self.client.get_topic(name).publish(
|
||||
json.dumps(row, ensure_ascii=False, sort_keys=True, default=str)
|
||||
)
|
||||
return
|
||||
if name.endswith("control"):
|
||||
key = "control"
|
||||
else:
|
||||
key = str(row.get("trade_id", row.get("slot_id", row.get("event_id", ""))))
|
||||
self.client.get_map(name).put(key, json_safe(row))
|
||||
102
prod/clean_arch/dita_v2/_backup_20260530/journal.py
Normal file
102
prod/clean_arch/dita_v2/_backup_20260530/journal.py
Normal 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()),
|
||||
}
|
||||
8
prod/clean_arch/dita_v2/_backup_20260530/kernel.py
Normal file
8
prod/clean_arch/dita_v2/_backup_20260530/kernel.py
Normal 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"]
|
||||
|
||||
350
prod/clean_arch/dita_v2/_backup_20260530/launcher.py
Normal file
350
prod/clean_arch/dita_v2/_backup_20260530/launcher.py
Normal file
@@ -0,0 +1,350 @@
|
||||
"""Operator-facing bootstrap helpers for DITAv2.
|
||||
|
||||
This module keeps the wiring explicit:
|
||||
- control plane selection
|
||||
- Zinc plane selection
|
||||
- projection sink selection
|
||||
- venue adapter selection
|
||||
|
||||
The defaults stay safe and testable. Real shared-memory or live BingX wiring
|
||||
is only enabled when the caller opts in via arguments or environment.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
import asyncio
|
||||
import inspect
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from prod.bingx.config import BingxExecClientConfig
|
||||
from prod.bingx.config import BingxInstrumentProviderConfig
|
||||
from prod.bingx.enums import BingxEnvironment
|
||||
|
||||
from .bingx_venue import BingxVenueAdapter
|
||||
from .control import BackendMode
|
||||
from .control import ControlPlane
|
||||
from .control import ControlUpdate
|
||||
from .control import KernelControlSnapshot
|
||||
from .control import KernelMode
|
||||
from .control import KernelVerbosity
|
||||
from .control import build_control_plane
|
||||
from .mock_venue import MockVenueAdapter
|
||||
from .mock_venue import MockVenueScenario
|
||||
from .projection import HazelcastProjection
|
||||
from .projection import build_projection
|
||||
from .real_control_plane import RealZincControlPlane
|
||||
from .real_control_plane import RealZincUnavailable
|
||||
from .real_zinc_plane import RealZincPlane
|
||||
from .real_zinc_plane import RealZincUnavailable as RealZincPlaneUnavailable
|
||||
from .rust_backend import ExecutionKernel
|
||||
from .venue import VenueAdapter
|
||||
from .zinc_plane import InMemoryZincPlane
|
||||
from .zinc_plane import ZincPlane
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[3]
|
||||
load_dotenv(PROJECT_ROOT / ".env")
|
||||
|
||||
|
||||
class LauncherVenueMode(str, Enum):
|
||||
MOCK = "MOCK"
|
||||
BINGX = "BINGX"
|
||||
|
||||
|
||||
class LauncherZincMode(str, Enum):
|
||||
IN_MEMORY = "IN_MEMORY"
|
||||
REAL = "REAL"
|
||||
|
||||
|
||||
@dataclass
|
||||
class DITAv2LauncherBundle:
|
||||
"""Concrete runtime components assembled by the launcher."""
|
||||
|
||||
kernel: ExecutionKernel
|
||||
control_plane: ControlPlane
|
||||
projection: HazelcastProjection
|
||||
zinc_plane: ZincPlane
|
||||
venue: VenueAdapter
|
||||
|
||||
def close(self) -> None:
|
||||
_maybe_close(self.venue)
|
||||
_maybe_close(self.zinc_plane)
|
||||
_maybe_close(self.control_plane)
|
||||
|
||||
|
||||
def _env_upper(name: str, default: str = "") -> str:
|
||||
return str(os.environ.get(name, default)).strip().upper()
|
||||
|
||||
|
||||
def _env_bool(name: str, default: bool = False) -> bool:
|
||||
raw = os.environ.get(name)
|
||||
if raw is None:
|
||||
return default
|
||||
return str(raw).strip().lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
def _resolve_control_mode() -> KernelMode | None:
|
||||
raw = _env_upper("DITA_V2_MODE", "")
|
||||
if raw == KernelMode.DEBUG.value:
|
||||
return KernelMode.DEBUG
|
||||
if raw == KernelMode.NORMAL.value:
|
||||
return KernelMode.NORMAL
|
||||
return None
|
||||
|
||||
|
||||
def _resolve_control_verbosity() -> KernelVerbosity | None:
|
||||
raw = _env_upper("DITA_V2_VERBOSITY", "")
|
||||
if raw == KernelVerbosity.TRACE.value:
|
||||
return KernelVerbosity.TRACE
|
||||
if raw == KernelVerbosity.VERBOSE.value:
|
||||
return KernelVerbosity.VERBOSE
|
||||
if raw == KernelVerbosity.QUIET.value:
|
||||
return KernelVerbosity.QUIET
|
||||
return None
|
||||
|
||||
|
||||
def _resolve_backend_mode() -> BackendMode | None:
|
||||
raw = _env_upper("DITA_V2_BACKEND_MODE", "")
|
||||
if raw == BackendMode.BINGX.value:
|
||||
return BackendMode.BINGX
|
||||
if raw == BackendMode.MOCK.value:
|
||||
return BackendMode.MOCK
|
||||
return None
|
||||
|
||||
|
||||
def _control_update_from_env() -> ControlUpdate | None:
|
||||
fields: dict[str, Any] = {}
|
||||
mode = _resolve_control_mode()
|
||||
if mode is not None:
|
||||
fields["mode"] = mode
|
||||
verbosity = _resolve_control_verbosity()
|
||||
if verbosity is not None:
|
||||
fields["verbosity"] = verbosity
|
||||
backend_mode = _resolve_backend_mode()
|
||||
if backend_mode is not None:
|
||||
fields["backend_mode"] = backend_mode
|
||||
raw = os.environ.get("DITA_V2_DEBUG_CLICKHOUSE")
|
||||
if raw is not None:
|
||||
fields["debug_clickhouse_enabled"] = _env_bool("DITA_V2_DEBUG_CLICKHOUSE", True)
|
||||
raw = os.environ.get("DITA_V2_TRACE_TRANSITIONS")
|
||||
if raw is not None:
|
||||
fields["trace_transitions"] = _env_bool("DITA_V2_TRACE_TRANSITIONS", False)
|
||||
raw = os.environ.get("DITA_V2_MIRROR_TO_HAZELCAST")
|
||||
if raw is not None:
|
||||
fields["mirror_to_hazelcast"] = _env_bool("DITA_V2_MIRROR_TO_HAZELCAST", True)
|
||||
raw = os.environ.get("DITA_V2_ACTIVE_SLOT_LIMIT")
|
||||
if raw is not None:
|
||||
try:
|
||||
fields["active_slot_limit"] = max(1, int(str(raw).strip()))
|
||||
except Exception:
|
||||
pass
|
||||
raw = os.environ.get("DITA_V2_RECONCILE_ON_RESTART")
|
||||
if raw is not None:
|
||||
fields["reconcile_on_restart"] = _env_bool("DITA_V2_RECONCILE_ON_RESTART", True)
|
||||
return ControlUpdate(**fields) if fields else None
|
||||
|
||||
|
||||
def _resolve_venue_mode(venue_mode: Optional[str] = None) -> LauncherVenueMode:
|
||||
raw = _env_upper("DITA_V2_VENUE", venue_mode or LauncherVenueMode.MOCK.value)
|
||||
if raw == LauncherVenueMode.BINGX.value:
|
||||
return LauncherVenueMode.BINGX
|
||||
return LauncherVenueMode.MOCK
|
||||
|
||||
|
||||
def _resolve_zinc_mode(zinc_mode: Optional[str] = None) -> LauncherZincMode:
|
||||
raw = _env_upper("DITA_V2_ZINC", zinc_mode or LauncherZincMode.IN_MEMORY.value)
|
||||
if raw == LauncherZincMode.REAL.value:
|
||||
return LauncherZincMode.REAL
|
||||
return LauncherZincMode.IN_MEMORY
|
||||
|
||||
|
||||
def _resolve_hazelcast_real(prefer_real_hazelcast: Optional[bool] = None) -> bool:
|
||||
if prefer_real_hazelcast is not None:
|
||||
return bool(prefer_real_hazelcast)
|
||||
raw = _env_upper("DITA_V2_HAZELCAST", "")
|
||||
return raw in {"REAL", "REAL_HZ", "HAZELCAST"}
|
||||
|
||||
|
||||
def build_bingx_exec_client_config(
|
||||
*,
|
||||
environment: Optional[BingxEnvironment] = None,
|
||||
allow_mainnet: Optional[bool] = None,
|
||||
recv_window_ms: Optional[int] = None,
|
||||
default_leverage: Optional[int] = None,
|
||||
exchange_leverage_cap: Optional[int] = None,
|
||||
prefer_websocket: Optional[bool] = None,
|
||||
sizing_mode: Optional[str] = None,
|
||||
) -> BingxExecClientConfig:
|
||||
"""Build the direct BingX config used by the DITAv2 launcher."""
|
||||
|
||||
resolved_environment = environment or (
|
||||
BingxEnvironment.LIVE if _env_upper("DOLPHIN_BINGX_ENV", "VST") == "LIVE" else BingxEnvironment.VST
|
||||
)
|
||||
resolved_allow_mainnet = _env_bool("DOLPHIN_BINGX_ALLOW_MAINNET", False) if allow_mainnet is None else bool(allow_mainnet)
|
||||
resolved_recv_window = int(os.environ.get("DOLPHIN_BINGX_RECV_WINDOW_MS", "5000")) if recv_window_ms is None else int(recv_window_ms)
|
||||
resolved_default_leverage = int(os.environ.get("DOLPHIN_BINGX_DEFAULT_LEVERAGE", "1")) if default_leverage is None else int(default_leverage)
|
||||
resolved_exchange_cap = int(os.environ.get("DOLPHIN_BINGX_EXCHANGE_LEVERAGE_CAP", "3")) if exchange_leverage_cap is None else int(exchange_leverage_cap)
|
||||
resolved_prefer_ws = _env_bool("DOLPHIN_BINGX_PREFER_WEBSOCKET", False) if prefer_websocket is None else bool(prefer_websocket)
|
||||
resolved_sizing_mode = sizing_mode or os.environ.get("DOLPHIN_BINGX_SIZING_MODE", "testnet")
|
||||
return BingxExecClientConfig(
|
||||
api_key=os.environ.get("BINGX_API_KEY"),
|
||||
secret_key=os.environ.get("BINGX_SECRET_KEY"),
|
||||
environment=resolved_environment,
|
||||
allow_mainnet=resolved_allow_mainnet,
|
||||
recv_window_ms=max(1, resolved_recv_window),
|
||||
default_leverage=max(1, resolved_default_leverage),
|
||||
exchange_leverage_cap=max(1, resolved_exchange_cap),
|
||||
prefer_websocket=resolved_prefer_ws,
|
||||
sizing_mode=resolved_sizing_mode,
|
||||
journal_strategy=os.environ.get("DOLPHIN_BINGX_JOURNAL_STRATEGY", "dita_v2"),
|
||||
journal_db=os.environ.get("DOLPHIN_BINGX_JOURNAL_DB", "dolphin_pink"),
|
||||
instrument_provider=BingxInstrumentProviderConfig(load_all=True),
|
||||
)
|
||||
|
||||
|
||||
def _build_control_plane(
|
||||
*,
|
||||
prefix: str,
|
||||
control_plane: Optional[ControlPlane] = None,
|
||||
) -> ControlPlane:
|
||||
plane = control_plane or build_control_plane(prefix=prefix)
|
||||
update = _control_update_from_env()
|
||||
if update is not None:
|
||||
plane.update(update)
|
||||
return plane
|
||||
|
||||
|
||||
def _build_zinc_plane(
|
||||
*,
|
||||
prefix: str,
|
||||
slot_count: int,
|
||||
zinc_mode: Optional[LauncherZincMode] = None,
|
||||
zinc_plane: Optional[ZincPlane] = None,
|
||||
) -> ZincPlane:
|
||||
if zinc_plane is not None:
|
||||
return zinc_plane
|
||||
resolved_mode = zinc_mode or _resolve_zinc_mode()
|
||||
if resolved_mode is LauncherZincMode.REAL:
|
||||
try:
|
||||
return RealZincPlane(prefix=prefix, slot_count=slot_count, create=True)
|
||||
except (RealZincPlaneUnavailable, RealZincUnavailable, Exception):
|
||||
pass
|
||||
return InMemoryZincPlane()
|
||||
|
||||
|
||||
def _build_venue(
|
||||
*,
|
||||
venue_mode: Optional[LauncherVenueMode] = None,
|
||||
mock_scenario: Optional[MockVenueScenario] = None,
|
||||
bingx_config: Optional[BingxExecClientConfig] = None,
|
||||
bingx_backend: Optional[Any] = None,
|
||||
venue: Optional[VenueAdapter] = None,
|
||||
) -> VenueAdapter:
|
||||
if venue is not None:
|
||||
return venue
|
||||
resolved_mode = venue_mode or _resolve_venue_mode()
|
||||
if resolved_mode is LauncherVenueMode.BINGX:
|
||||
backend = bingx_backend
|
||||
if backend is None:
|
||||
from prod.clean_arch.adapters.bingx_direct import BingxDirectExecutionAdapter
|
||||
|
||||
backend = BingxDirectExecutionAdapter(bingx_config or build_bingx_exec_client_config())
|
||||
return BingxVenueAdapter(backend=backend)
|
||||
return MockVenueAdapter(mock_scenario)
|
||||
|
||||
|
||||
def _maybe_close(obj: Any) -> None:
|
||||
for method_name in ("close", "disconnect"):
|
||||
method = getattr(obj, method_name, None)
|
||||
if method is None:
|
||||
continue
|
||||
try:
|
||||
result = method()
|
||||
except TypeError:
|
||||
continue
|
||||
if inspect.isawaitable(result):
|
||||
try:
|
||||
asyncio.run(result)
|
||||
except RuntimeError:
|
||||
pass
|
||||
break
|
||||
|
||||
|
||||
def build_launcher_bundle(
|
||||
*,
|
||||
max_slots: int = 10,
|
||||
prefix: Optional[str] = None,
|
||||
control_plane: Optional[ControlPlane] = None,
|
||||
projection: Optional[HazelcastProjection] = None,
|
||||
projection_client: Optional[Any] = None,
|
||||
zinc_plane: Optional[ZincPlane] = None,
|
||||
venue: Optional[VenueAdapter] = None,
|
||||
venue_mode: Optional[LauncherVenueMode | str] = None,
|
||||
zinc_mode: Optional[LauncherZincMode | str] = None,
|
||||
bingx_config: Optional[BingxExecClientConfig] = None,
|
||||
bingx_backend: Optional[Any] = None,
|
||||
mock_scenario: Optional[MockVenueScenario] = None,
|
||||
) -> DITAv2LauncherBundle:
|
||||
"""Build a fully wired DITAv2 runtime bundle.
|
||||
|
||||
Defaults stay non-destructive:
|
||||
- in-memory Zinc plane
|
||||
- in-process control plane
|
||||
- mock venue
|
||||
- callback projection unless a Hazelcast client is supplied
|
||||
"""
|
||||
|
||||
resolved_prefix = (prefix or os.environ.get("DITA_V2_PREFIX", "dita_v2")).strip() or "dita_v2"
|
||||
if isinstance(venue_mode, LauncherVenueMode):
|
||||
resolved_venue_mode = venue_mode
|
||||
elif isinstance(venue_mode, str):
|
||||
resolved_venue_mode = LauncherVenueMode(venue_mode.strip().upper())
|
||||
else:
|
||||
resolved_venue_mode = None
|
||||
if isinstance(zinc_mode, LauncherZincMode):
|
||||
resolved_zinc_mode = zinc_mode
|
||||
elif isinstance(zinc_mode, str):
|
||||
resolved_zinc_mode = LauncherZincMode(zinc_mode.strip().upper())
|
||||
else:
|
||||
resolved_zinc_mode = None
|
||||
|
||||
active_control_plane = _build_control_plane(prefix=resolved_prefix, control_plane=control_plane)
|
||||
control_snapshot = active_control_plane.read()
|
||||
active_projection = projection or build_projection(
|
||||
client=projection_client,
|
||||
prefer_real_hazelcast=_resolve_hazelcast_real(),
|
||||
control_snapshot=control_snapshot,
|
||||
)
|
||||
active_zinc_plane = _build_zinc_plane(
|
||||
prefix=resolved_prefix,
|
||||
slot_count=int(max_slots),
|
||||
zinc_mode=resolved_zinc_mode,
|
||||
zinc_plane=zinc_plane,
|
||||
)
|
||||
active_venue = _build_venue(
|
||||
venue_mode=resolved_venue_mode,
|
||||
mock_scenario=mock_scenario,
|
||||
bingx_config=bingx_config,
|
||||
bingx_backend=bingx_backend,
|
||||
venue=venue,
|
||||
)
|
||||
kernel = ExecutionKernel(
|
||||
max_slots=int(max_slots),
|
||||
control_plane=active_control_plane,
|
||||
venue=active_venue,
|
||||
projection=active_projection,
|
||||
projection_client=projection_client,
|
||||
zinc_plane=active_zinc_plane,
|
||||
)
|
||||
return DITAv2LauncherBundle(
|
||||
kernel=kernel,
|
||||
control_plane=active_control_plane,
|
||||
projection=active_projection,
|
||||
zinc_plane=active_zinc_plane,
|
||||
venue=active_venue,
|
||||
)
|
||||
203
prod/clean_arch/dita_v2/_backup_20260530/mock_venue.py
Normal file
203
prod/clean_arch/dita_v2/_backup_20260530/mock_venue.py
Normal file
@@ -0,0 +1,203 @@
|
||||
"""Deterministic mock venue for DITAv2 tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, List, Optional
|
||||
import itertools
|
||||
|
||||
from .contracts import (
|
||||
KernelCommandType,
|
||||
KernelEventKind,
|
||||
KernelIntent,
|
||||
TradeSide,
|
||||
VenueEvent,
|
||||
VenueEventStatus,
|
||||
VenueOrder,
|
||||
VenueOrderStatus,
|
||||
)
|
||||
from .venue import VenueAdapter
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MockVenueScenario:
|
||||
"""Failure knobs for the mock venue."""
|
||||
|
||||
reject_entries: bool = False
|
||||
reject_exits: bool = False
|
||||
partial_fill_ratio: float = 1.0
|
||||
cancel_reject: bool = False
|
||||
emit_ack_before_fill: bool = True
|
||||
emit_fill_on_submit: bool = False
|
||||
|
||||
|
||||
class MockVenueAdapter(VenueAdapter):
|
||||
"""Scriptable mock venue with BingX-shaped response semantics."""
|
||||
|
||||
def __init__(self, scenario: Optional[MockVenueScenario] = None):
|
||||
self.scenario = scenario or MockVenueScenario()
|
||||
self._order_seq = itertools.count(1)
|
||||
self._event_seq = itertools.count(1)
|
||||
self._open_orders: Dict[str, VenueOrder] = {}
|
||||
self._open_positions: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
def submit(self, intent: KernelIntent) -> List[VenueEvent]:
|
||||
is_entry = intent.action == KernelCommandType.ENTER
|
||||
should_reject = self.scenario.reject_entries if is_entry else self.scenario.reject_exits
|
||||
order_id = f"V-{next(self._order_seq):08d}"
|
||||
client_id = f"{intent.trade_id}:{intent.intent_id}"
|
||||
order = VenueOrder(
|
||||
internal_trade_id=intent.trade_id,
|
||||
venue_order_id=order_id,
|
||||
venue_client_id=client_id,
|
||||
side=intent.side,
|
||||
intended_size=float(intent.target_size),
|
||||
status=VenueOrderStatus.NEW,
|
||||
metadata={"intent_id": intent.intent_id, "action": intent.action.value, "slot_id": intent.slot_id},
|
||||
)
|
||||
if should_reject:
|
||||
order = VenueOrder(
|
||||
internal_trade_id=order.internal_trade_id,
|
||||
venue_order_id=order.venue_order_id,
|
||||
venue_client_id=order.venue_client_id,
|
||||
side=order.side,
|
||||
intended_size=order.intended_size,
|
||||
filled_size=0.0,
|
||||
average_fill_price=0.0,
|
||||
status=VenueOrderStatus.REJECTED,
|
||||
metadata=dict(order.metadata),
|
||||
)
|
||||
return [self._event_from_order(intent, order, KernelEventKind.ORDER_REJECT, VenueEventStatus.REJECTED, reason="MOCK_REJECT")]
|
||||
|
||||
self._open_orders[order_id] = order
|
||||
events: List[VenueEvent] = []
|
||||
if self.scenario.emit_ack_before_fill or not self.scenario.emit_fill_on_submit:
|
||||
events.append(self._event_from_order(intent, order, KernelEventKind.ORDER_ACK, VenueEventStatus.ACKED))
|
||||
if self.scenario.emit_fill_on_submit or self.scenario.partial_fill_ratio > 0:
|
||||
fill_ratio = max(0.0, min(1.0, float(self.scenario.partial_fill_ratio)))
|
||||
fill_size = float(intent.target_size) * fill_ratio
|
||||
event_kind = KernelEventKind.FULL_FILL if fill_ratio >= 1.0 else KernelEventKind.PARTIAL_FILL
|
||||
event_status = VenueEventStatus.FILLED if fill_ratio >= 1.0 else VenueEventStatus.PARTIALLY_FILLED
|
||||
fill_event = self._event_from_order(
|
||||
intent,
|
||||
order,
|
||||
event_kind,
|
||||
event_status,
|
||||
price=float(intent.reference_price or 0.0),
|
||||
fill_size=fill_size,
|
||||
remaining_size=max(0.0, float(intent.target_size) - fill_size),
|
||||
)
|
||||
events.append(fill_event)
|
||||
order = VenueOrder(
|
||||
internal_trade_id=order.internal_trade_id,
|
||||
venue_order_id=order.venue_order_id,
|
||||
venue_client_id=order.venue_client_id,
|
||||
side=order.side,
|
||||
intended_size=order.intended_size,
|
||||
filled_size=fill_size,
|
||||
average_fill_price=float(intent.reference_price or 0.0),
|
||||
status=VenueOrderStatus.FILLED if fill_ratio >= 1.0 else VenueOrderStatus.PARTIALLY_FILLED,
|
||||
metadata=dict(order.metadata),
|
||||
)
|
||||
self._open_orders[order_id] = order
|
||||
return events
|
||||
|
||||
def cancel(self, order: VenueOrder, *, reason: str = "") -> List[VenueEvent]:
|
||||
if self.scenario.cancel_reject:
|
||||
return [
|
||||
self._event_from_order(
|
||||
self._dummy_intent(order),
|
||||
order,
|
||||
KernelEventKind.CANCEL_REJECT,
|
||||
VenueEventStatus.CANCELED_REJECTED,
|
||||
reason=reason or "MOCK_CANCEL_REJECT",
|
||||
)
|
||||
]
|
||||
existing = self._open_orders.get(order.venue_order_id, order)
|
||||
canceled = VenueOrder(
|
||||
internal_trade_id=existing.internal_trade_id,
|
||||
venue_order_id=existing.venue_order_id,
|
||||
venue_client_id=existing.venue_client_id,
|
||||
side=existing.side,
|
||||
intended_size=existing.intended_size,
|
||||
filled_size=existing.filled_size,
|
||||
average_fill_price=existing.average_fill_price,
|
||||
status=VenueOrderStatus.CANCELED,
|
||||
metadata=dict(existing.metadata),
|
||||
)
|
||||
self._open_orders.pop(order.venue_order_id, None)
|
||||
return [
|
||||
self._event_from_order(
|
||||
self._dummy_intent(order),
|
||||
canceled,
|
||||
KernelEventKind.CANCEL_ACK,
|
||||
VenueEventStatus.CANCELED,
|
||||
reason=reason or "MOCK_CANCEL_ACK",
|
||||
)
|
||||
]
|
||||
|
||||
def open_orders(self) -> List[VenueOrder]:
|
||||
return list(self._open_orders.values())
|
||||
|
||||
def open_positions(self) -> List[Dict[str, Any]]:
|
||||
return list(self._open_positions.values())
|
||||
|
||||
def reconcile(self) -> List[VenueEvent]:
|
||||
return []
|
||||
|
||||
def _dummy_intent(self, order: VenueOrder) -> KernelIntent:
|
||||
return KernelIntent(
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
intent_id=order.venue_client_id,
|
||||
trade_id=order.internal_trade_id,
|
||||
slot_id=int(order.metadata.get("slot_id", 0)),
|
||||
asset=str(order.metadata.get("asset", "")),
|
||||
side=order.side,
|
||||
action=KernelCommandType.EXIT if order.metadata.get("action") == "EXIT" else KernelCommandType.ENTER,
|
||||
reference_price=float(order.metadata.get("reference_price", 0.0)),
|
||||
target_size=float(order.intended_size),
|
||||
leverage=float(order.metadata.get("leverage", 1.0)),
|
||||
reason=str(order.metadata.get("reason", "")),
|
||||
metadata=dict(order.metadata),
|
||||
)
|
||||
|
||||
def _event_from_order(
|
||||
self,
|
||||
intent: KernelIntent,
|
||||
order: VenueOrder,
|
||||
kind: KernelEventKind,
|
||||
status: VenueEventStatus,
|
||||
*,
|
||||
price: Optional[float] = None,
|
||||
fill_size: float = 0.0,
|
||||
remaining_size: float = 0.0,
|
||||
reason: str = "",
|
||||
) -> VenueEvent:
|
||||
event = VenueEvent(
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
event_id=f"EV-{next(self._event_seq):08d}",
|
||||
trade_id=intent.trade_id,
|
||||
slot_id=intent.slot_id,
|
||||
kind=kind,
|
||||
status=status,
|
||||
venue_order_id=order.venue_order_id,
|
||||
venue_client_id=order.venue_client_id,
|
||||
side=order.side,
|
||||
asset=intent.asset,
|
||||
price=float(price if price is not None else intent.reference_price or 0.0),
|
||||
size=float(intent.target_size),
|
||||
filled_size=float(fill_size),
|
||||
remaining_size=float(remaining_size),
|
||||
reason=reason,
|
||||
raw_payload={
|
||||
"status": status.value,
|
||||
"orderId": order.venue_order_id,
|
||||
"clientOrderId": order.venue_client_id,
|
||||
"symbol": intent.asset,
|
||||
"side": order.side.value,
|
||||
"action": intent.action.value,
|
||||
},
|
||||
metadata={"intent_id": intent.intent_id, "action": intent.action.value},
|
||||
)
|
||||
return event
|
||||
97
prod/clean_arch/dita_v2/_backup_20260530/projection.py
Normal file
97
prod/clean_arch/dita_v2/_backup_20260530/projection.py
Normal 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
|
||||
129
prod/clean_arch/dita_v2/_backup_20260530/real_control_plane.py
Normal file
129
prod/clean_arch/dita_v2/_backup_20260530/real_control_plane.py
Normal file
@@ -0,0 +1,129 @@
|
||||
"""Real Zinc-backed control plane for DITAv2."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from .control import BackendMode, ControlPlane, ControlUpdate, KernelControlSnapshot, KernelMode, KernelVerbosity
|
||||
|
||||
_ZINC_ADAPTER_PATH = Path(__file__).resolve().parents[3] / "zinc" / "adapters" / "python"
|
||||
if _ZINC_ADAPTER_PATH.exists() and str(_ZINC_ADAPTER_PATH) not in sys.path:
|
||||
sys.path.insert(0, str(_ZINC_ADAPTER_PATH))
|
||||
|
||||
try: # pragma: no cover - exercised in integration tests
|
||||
from zinc import SharedRegion
|
||||
except Exception as exc: # pragma: no cover
|
||||
SharedRegion = None # type: ignore[assignment]
|
||||
_ZINC_IMPORT_ERROR = exc
|
||||
else:
|
||||
_ZINC_IMPORT_ERROR = None
|
||||
|
||||
|
||||
class RealZincUnavailable(RuntimeError):
|
||||
"""Raised when the Zinc Python adapter cannot be loaded."""
|
||||
|
||||
|
||||
def require_real_zinc() -> None:
|
||||
if SharedRegion is None:
|
||||
raise RealZincUnavailable(str(_ZINC_IMPORT_ERROR))
|
||||
|
||||
|
||||
def _json_default(value: Any) -> Any:
|
||||
if hasattr(value, "value"):
|
||||
return value.value
|
||||
if hasattr(value, "isoformat"):
|
||||
try:
|
||||
return value.isoformat()
|
||||
except Exception:
|
||||
pass
|
||||
if hasattr(value, "__dict__"):
|
||||
return dict(vars(value))
|
||||
raise TypeError(f"Unsupported value: {type(value)!r}")
|
||||
|
||||
|
||||
def _encode_packet(seq: int, payload: Dict[str, Any]) -> bytes:
|
||||
text = json.dumps(payload, sort_keys=True, ensure_ascii=False, default=_json_default, separators=(",", ":")).encode("utf-8")
|
||||
return struct.pack("!QQ", int(seq), len(text)) + text
|
||||
|
||||
|
||||
def _decode_packet(buf: memoryview) -> Dict[str, Any]:
|
||||
if len(buf) < 16:
|
||||
return {}
|
||||
seq, size = struct.unpack_from("!QQ", buf, 0)
|
||||
if size <= 0 or size > len(buf) - 16:
|
||||
return {}
|
||||
payload = bytes(buf[16 : 16 + size]).decode("utf-8")
|
||||
out = json.loads(payload)
|
||||
if isinstance(out, dict):
|
||||
out["_seq"] = seq
|
||||
return out
|
||||
|
||||
|
||||
class RealZincControlPlane(ControlPlane):
|
||||
"""Shared-memory Zinc-backed control plane."""
|
||||
|
||||
def __init__(self, *, prefix: str, create: bool = True) -> None:
|
||||
require_real_zinc()
|
||||
base = prefix.strip("/").replace("/", "_")
|
||||
self.region_name = f"{base}_control"
|
||||
self._seq = 0
|
||||
self._snapshot = KernelControlSnapshot()
|
||||
if create:
|
||||
self.region = SharedRegion.create(self.region_name, 1 << 20)
|
||||
self._write_region(self._seq, self._snapshot.as_dict())
|
||||
else:
|
||||
self.region = SharedRegion.open(self.region_name)
|
||||
payload = _decode_packet(self.region.as_buffer())
|
||||
control = payload.get("control") if isinstance(payload, dict) else None
|
||||
if isinstance(control, dict):
|
||||
self._snapshot = KernelControlSnapshot(**control)
|
||||
|
||||
def close(self) -> None:
|
||||
self.region.close()
|
||||
|
||||
def read(self) -> KernelControlSnapshot:
|
||||
payload = _decode_packet(self.region.as_buffer())
|
||||
control = payload.get("control") if isinstance(payload, dict) else None
|
||||
if not isinstance(control, dict):
|
||||
return self._snapshot
|
||||
self._snapshot = KernelControlSnapshot(**control)
|
||||
return self._snapshot
|
||||
|
||||
def update(self, update: ControlUpdate) -> KernelControlSnapshot:
|
||||
self._snapshot = update.apply(self.read())
|
||||
self._seq += 1
|
||||
self._write_region(self._seq, self._snapshot.as_dict())
|
||||
return self._snapshot
|
||||
|
||||
def mirror(self) -> Dict[str, Any]:
|
||||
return self._snapshot.as_dict()
|
||||
|
||||
def wait(self, timeout_ms: int = 1000) -> bool:
|
||||
try:
|
||||
return bool(self.region.wait(timeout_ms))
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def notify(self) -> None:
|
||||
try:
|
||||
self.region.notify()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _write_region(self, seq: int, control: Dict[str, Any]) -> None:
|
||||
packet = _encode_packet(seq, {"control": control})
|
||||
buf = self.region.as_buffer()
|
||||
if len(packet) > len(buf):
|
||||
raise ValueError(f"payload too large for Zinc control region: {len(packet)} > {len(buf)}")
|
||||
view = memoryview(buf)
|
||||
view[: len(packet)] = packet
|
||||
if len(view) > len(packet):
|
||||
view[len(packet) :] = b"\x00" * (len(view) - len(packet))
|
||||
try:
|
||||
self.region.notify()
|
||||
except Exception:
|
||||
pass
|
||||
263
prod/clean_arch/dita_v2/_backup_20260530/real_zinc_plane.py
Normal file
263
prod/clean_arch/dita_v2/_backup_20260530/real_zinc_plane.py
Normal file
@@ -0,0 +1,263 @@
|
||||
"""Real Zinc-backed hot-path plane for DITAv2.
|
||||
|
||||
This wrapper uses the Zinc Python adapter directly. The kernel still talks to
|
||||
the narrow ``ZincPlane`` interface; this module just makes that interface real.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
import json
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import threading
|
||||
|
||||
from .contracts import KernelIntent, TradeSide, TradeSlot, TradeStage, VenueOrder, VenueOrderStatus
|
||||
from .control import KernelControlSnapshot
|
||||
|
||||
_ZINC_ADAPTER_PATH = Path(__file__).resolve().parents[3] / "zinc" / "adapters" / "python"
|
||||
if _ZINC_ADAPTER_PATH.exists() and str(_ZINC_ADAPTER_PATH) not in sys.path:
|
||||
sys.path.insert(0, str(_ZINC_ADAPTER_PATH))
|
||||
|
||||
try: # pragma: no cover - exercised in integration tests
|
||||
from zinc import SharedRegion
|
||||
except Exception as exc: # pragma: no cover
|
||||
SharedRegion = None # type: ignore[assignment]
|
||||
_ZINC_IMPORT_ERROR = exc
|
||||
else:
|
||||
_ZINC_IMPORT_ERROR = None
|
||||
|
||||
|
||||
class RealZincUnavailable(RuntimeError):
|
||||
"""Raised when the Zinc Python adapter cannot be loaded."""
|
||||
|
||||
|
||||
def require_real_zinc() -> None:
|
||||
if SharedRegion is None:
|
||||
raise RealZincUnavailable(str(_ZINC_IMPORT_ERROR))
|
||||
|
||||
|
||||
def _json_default(value: Any) -> Any:
|
||||
if hasattr(value, "value"):
|
||||
return value.value
|
||||
if hasattr(value, "isoformat"):
|
||||
try:
|
||||
return value.isoformat()
|
||||
except Exception:
|
||||
pass
|
||||
if hasattr(value, "__dict__"):
|
||||
return dict(vars(value))
|
||||
raise TypeError(f"Unsupported value: {type(value)!r}")
|
||||
|
||||
|
||||
def _slot_to_payload(slot: TradeSlot) -> Dict[str, Any]:
|
||||
data = slot.to_dict()
|
||||
return data
|
||||
|
||||
|
||||
def _slot_from_payload(payload: Dict[str, Any]) -> TradeSlot:
|
||||
active_entry_order = None
|
||||
active_exit_order = None
|
||||
if isinstance(payload.get("active_entry_order"), dict):
|
||||
active_entry_order = VenueOrder(
|
||||
internal_trade_id=str(payload.get("trade_id", "")),
|
||||
venue_order_id=str(payload["active_entry_order"].get("venue_order_id", "")),
|
||||
venue_client_id=str(payload["active_entry_order"].get("venue_client_id", "")),
|
||||
side=TradeSide(str(payload["active_entry_order"].get("side", TradeSide.FLAT.value))),
|
||||
intended_size=float(payload["active_entry_order"].get("intended_size", payload.get("size", 0.0))),
|
||||
filled_size=float(payload["active_entry_order"].get("filled_size", 0.0)),
|
||||
average_fill_price=float(payload["active_entry_order"].get("average_fill_price", 0.0)),
|
||||
status=VenueOrderStatus(str(payload["active_entry_order"].get("status", VenueOrderStatus.NEW.value))),
|
||||
metadata=dict(payload["active_entry_order"].get("metadata", {})),
|
||||
)
|
||||
if isinstance(payload.get("active_exit_order"), dict):
|
||||
active_exit_order = VenueOrder(
|
||||
internal_trade_id=str(payload.get("trade_id", "")),
|
||||
venue_order_id=str(payload["active_exit_order"].get("venue_order_id", "")),
|
||||
venue_client_id=str(payload["active_exit_order"].get("venue_client_id", "")),
|
||||
side=TradeSide(str(payload["active_exit_order"].get("side", TradeSide.FLAT.value))),
|
||||
intended_size=float(payload["active_exit_order"].get("intended_size", payload.get("size", 0.0))),
|
||||
filled_size=float(payload["active_exit_order"].get("filled_size", 0.0)),
|
||||
average_fill_price=float(payload["active_exit_order"].get("average_fill_price", 0.0)),
|
||||
status=VenueOrderStatus(str(payload["active_exit_order"].get("status", VenueOrderStatus.NEW.value))),
|
||||
metadata=dict(payload["active_exit_order"].get("metadata", {})),
|
||||
)
|
||||
slot = TradeSlot(
|
||||
slot_id=int(payload.get("slot_id", 0)),
|
||||
trade_id=str(payload.get("trade_id", "")),
|
||||
asset=str(payload.get("asset", "")),
|
||||
side=TradeSide(str(payload.get("side", TradeSide.FLAT.value))),
|
||||
entry_price=float(payload.get("entry_price", 0.0)),
|
||||
size=float(payload.get("size", 0.0)),
|
||||
initial_size=float(payload.get("initial_size", 0.0)),
|
||||
leverage=float(payload.get("leverage", 0.0)),
|
||||
entry_time=datetime.fromisoformat(payload["entry_time"]) if payload.get("entry_time") else None,
|
||||
unrealized_pnl=float(payload.get("unrealized_pnl", 0.0)),
|
||||
realized_pnl=float(payload.get("realized_pnl", 0.0)),
|
||||
closed=bool(payload.get("closed", False)),
|
||||
exit_leg_ratios=tuple(float(r) for r in payload.get("exit_leg_ratios", (1.0,))),
|
||||
active_leg_index=int(payload.get("active_leg_index", 0)),
|
||||
active_exit_order=active_exit_order,
|
||||
active_entry_order=active_entry_order,
|
||||
fsm_state=TradeStage(str(payload.get("fsm_state", TradeStage.IDLE.value))),
|
||||
close_reason=str(payload.get("close_reason", "")),
|
||||
last_event_time=datetime.fromisoformat(payload["last_event_time"]) if payload.get("last_event_time") else None,
|
||||
seen_event_ids=tuple(str(event_id) for event_id in payload.get("seen_event_ids", ())),
|
||||
metadata=dict(payload.get("metadata", {})),
|
||||
)
|
||||
return slot
|
||||
|
||||
|
||||
def _encode_packet(seq: int, payload: Dict[str, Any]) -> bytes:
|
||||
text = json.dumps(payload, sort_keys=True, ensure_ascii=False, default=_json_default, separators=(",", ":")).encode("utf-8")
|
||||
return struct.pack("!QQ", int(seq), len(text)) + text
|
||||
|
||||
|
||||
def _decode_packet(buf: memoryview) -> Dict[str, Any]:
|
||||
if len(buf) < 16:
|
||||
return {}
|
||||
seq, size = struct.unpack_from("!QQ", buf, 0)
|
||||
if size <= 0 or size > len(buf) - 16:
|
||||
return {}
|
||||
payload = bytes(buf[16 : 16 + size]).decode("utf-8")
|
||||
out = json.loads(payload)
|
||||
if isinstance(out, dict):
|
||||
out["_seq"] = seq
|
||||
return out
|
||||
|
||||
|
||||
class RealZincPlane:
|
||||
"""Shared-memory Zinc plane used by the Python prototype."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
prefix: str,
|
||||
slot_count: int = 10,
|
||||
intent_capacity: int = 1 << 20,
|
||||
state_capacity: int = 1 << 20,
|
||||
control_capacity: int = 1 << 20,
|
||||
create: bool = True,
|
||||
) -> None:
|
||||
require_real_zinc()
|
||||
base = prefix.strip("/").replace("/", "_")
|
||||
self.intent_name = f"{base}_intent"
|
||||
self.state_name = f"{base}_state"
|
||||
self.control_name = f"{base}_control"
|
||||
self._intent_seq = 0
|
||||
self._state_seq = 0
|
||||
self._control_seq = 0
|
||||
self._lock = threading.Lock()
|
||||
self._slot_cache: Dict[int, TradeSlot] = {i: TradeSlot(slot_id=i) for i in range(int(slot_count))}
|
||||
self._slot_count = int(slot_count)
|
||||
self._intent_cache: List[Dict[str, Any]] = []
|
||||
self._control_cache = KernelControlSnapshot()
|
||||
if create:
|
||||
self.intent_region = SharedRegion.create(self.intent_name, intent_capacity)
|
||||
self.state_region = SharedRegion.create(self.state_name, state_capacity)
|
||||
self.control_region = SharedRegion.create(self.control_name, control_capacity)
|
||||
self._write_region(self.control_region, self._control_seq, {"control": self._control_cache.as_dict()})
|
||||
self._write_region(
|
||||
self.state_region,
|
||||
self._state_seq,
|
||||
{"slots": [self._slot_cache[key].to_dict() for key in range(self._slot_count)]},
|
||||
)
|
||||
self._write_region(self.intent_region, self._intent_seq, {"items": []})
|
||||
else:
|
||||
self.intent_region = SharedRegion.open(self.intent_name)
|
||||
self.state_region = SharedRegion.open(self.state_name)
|
||||
self.control_region = SharedRegion.open(self.control_name)
|
||||
control_payload = _decode_packet(self.control_region.as_buffer())
|
||||
state_payload = _decode_packet(self.state_region.as_buffer())
|
||||
intent_payload = _decode_packet(self.intent_region.as_buffer())
|
||||
if isinstance(control_payload.get("control"), dict):
|
||||
self._control_cache = KernelControlSnapshot(**control_payload["control"])
|
||||
if isinstance(state_payload.get("slots"), list):
|
||||
for slot_payload in state_payload["slots"]:
|
||||
if isinstance(slot_payload, dict):
|
||||
slot = _slot_from_payload(slot_payload)
|
||||
self._slot_cache[int(slot.slot_id)] = slot
|
||||
if isinstance(intent_payload.get("items"), list):
|
||||
self._intent_cache = list(intent_payload["items"])
|
||||
|
||||
def close(self) -> None:
|
||||
self.intent_region.close()
|
||||
self.state_region.close()
|
||||
self.control_region.close()
|
||||
|
||||
def publish_intent(self, intent: KernelIntent) -> None:
|
||||
with self._lock:
|
||||
self._intent_seq += 1
|
||||
row = intent.__dict__.copy()
|
||||
row["timestamp"] = intent.timestamp.isoformat()
|
||||
row["side"] = intent.side.value
|
||||
row["action"] = intent.action.value
|
||||
row["stage"] = intent.stage.value
|
||||
row["exit_leg_ratios"] = list(intent.exit_leg_ratios)
|
||||
row["metadata"] = json.loads(json.dumps(intent.metadata, default=_json_default))
|
||||
self._intent_cache.append(row)
|
||||
self._write_region(self.intent_region, self._intent_seq, {"items": self._intent_cache[-512:]})
|
||||
|
||||
def write_slot(self, slot: TradeSlot) -> None:
|
||||
with self._lock:
|
||||
self._state_seq += 1
|
||||
self._slot_cache[int(slot.slot_id)] = slot
|
||||
payload = {
|
||||
"slots": [self._slot_cache[key].to_dict() for key in range(self._slot_count)],
|
||||
}
|
||||
self._write_region(self.state_region, self._state_seq, payload)
|
||||
|
||||
def read_slots(self) -> List[TradeSlot]:
|
||||
payload = _decode_packet(self.state_region.as_buffer())
|
||||
slots = payload.get("slots", []) if isinstance(payload, dict) else []
|
||||
return [_slot_from_payload(slot) for slot in sorted(slots, key=lambda row: int(row.get("slot_id", 0)))]
|
||||
|
||||
def read_intents(self) -> List[Dict[str, Any]]:
|
||||
payload = _decode_packet(self.intent_region.as_buffer())
|
||||
items = payload.get("items", []) if isinstance(payload, dict) else []
|
||||
return list(items)
|
||||
|
||||
def update_control(self, control: KernelControlSnapshot) -> None:
|
||||
with self._lock:
|
||||
self._control_seq += 1
|
||||
self._control_cache = control
|
||||
self._write_region(self.control_region, self._control_seq, {"control": control.as_dict()})
|
||||
|
||||
def read_control(self) -> KernelControlSnapshot:
|
||||
payload = _decode_packet(self.control_region.as_buffer())
|
||||
control = payload.get("control") if isinstance(payload, dict) else None
|
||||
if not isinstance(control, dict):
|
||||
return self._control_cache
|
||||
return KernelControlSnapshot(**control)
|
||||
|
||||
def wait_on_state(self, timeout_ms: int = 1000) -> bool:
|
||||
return bool(self.state_region.wait(timeout_ms))
|
||||
|
||||
def notify_state(self) -> None:
|
||||
self.state_region.notify()
|
||||
|
||||
def wait_on_control(self, timeout_ms: int = 1000) -> bool:
|
||||
return bool(self.control_region.wait(timeout_ms))
|
||||
|
||||
def notify_control(self) -> None:
|
||||
self.control_region.notify()
|
||||
|
||||
def wait_on_intent(self, timeout_ms: int = 1000) -> bool:
|
||||
return bool(self.intent_region.wait(timeout_ms))
|
||||
|
||||
def notify_intent(self) -> None:
|
||||
self.intent_region.notify()
|
||||
|
||||
def _write_region(self, region: Any, seq: int, payload: Dict[str, Any]) -> None:
|
||||
packet = _encode_packet(seq, payload)
|
||||
buf = region.as_buffer()
|
||||
if len(packet) > len(buf):
|
||||
raise ValueError(f"payload too large for Zinc region: {len(packet)} > {len(buf)}")
|
||||
view = memoryview(buf)
|
||||
view[:] = b"\x00" * len(view)
|
||||
view[: len(packet)] = packet
|
||||
region.notify()
|
||||
683
prod/clean_arch/dita_v2/_backup_20260530/rust_backend.py
Normal file
683
prod/clean_arch/dita_v2/_backup_20260530/rust_backend.py
Normal file
@@ -0,0 +1,683 @@
|
||||
"""Rust-backed DITAv2 execution kernel.
|
||||
|
||||
This module keeps the Python API shape stable while moving the kernel state
|
||||
machine into a Rust shared library. Slot views write through to the backend on
|
||||
assignment, then the Python side mirrors the resulting state into Zinc and the
|
||||
existing projections/journals.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterable, List, Optional, Sequence
|
||||
import ctypes
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from .account import AccountProjection
|
||||
from .control import ControlPlane, ControlUpdate, KernelControlSnapshot, KernelVerbosity, build_control_plane
|
||||
from .contracts import (
|
||||
KernelCommandType,
|
||||
KernelDiagnosticCode,
|
||||
KernelEventKind,
|
||||
KernelIntent,
|
||||
KernelOutcome,
|
||||
KernelSeverity,
|
||||
KernelTransition,
|
||||
TradeSide,
|
||||
TradeSlot,
|
||||
TradeStage,
|
||||
VenueEvent,
|
||||
VenueOrder,
|
||||
VenueOrderStatus,
|
||||
VenueEventStatus,
|
||||
)
|
||||
from .journal import KernelJournal, MemoryKernelJournal
|
||||
from .mock_venue import MockVenueAdapter
|
||||
from .projection import HazelcastProjection
|
||||
from .projection import build_projection
|
||||
from .utils import json_safe
|
||||
from .venue import VenueAdapter
|
||||
from .zinc_plane import InMemoryZincPlane, ZincPlane
|
||||
|
||||
|
||||
def _repo_root() -> Path:
|
||||
return Path(__file__).resolve().parents[3]
|
||||
|
||||
|
||||
def _crate_dir() -> Path:
|
||||
return Path(__file__).resolve().with_name("_rust_kernel")
|
||||
|
||||
|
||||
def _library_path() -> Path:
|
||||
if sys.platform == "darwin":
|
||||
name = "libdita_v2_kernel.dylib"
|
||||
elif os.name == "nt":
|
||||
name = "dita_v2_kernel.dll"
|
||||
else:
|
||||
name = "libdita_v2_kernel.so"
|
||||
return _crate_dir() / "target" / "release" / name
|
||||
|
||||
|
||||
def _build_library() -> None:
|
||||
crate_dir = _crate_dir()
|
||||
if not crate_dir.exists():
|
||||
raise FileNotFoundError(f"Missing Rust kernel crate: {crate_dir}")
|
||||
subprocess.run(
|
||||
["cargo", "build", "--release", "--manifest-path", str(crate_dir / "Cargo.toml")],
|
||||
cwd=_repo_root(),
|
||||
check=True,
|
||||
)
|
||||
|
||||
|
||||
def _ensure_library() -> Path:
|
||||
path = _library_path()
|
||||
if not path.exists():
|
||||
_build_library()
|
||||
return path
|
||||
|
||||
|
||||
class _RustKernelLib:
|
||||
def __init__(self) -> None:
|
||||
path = _ensure_library()
|
||||
self.lib = ctypes.CDLL(str(path))
|
||||
self.lib.dita_kernel_create.argtypes = [ctypes.c_size_t]
|
||||
self.lib.dita_kernel_create.restype = ctypes.c_void_p
|
||||
self.lib.dita_kernel_destroy.argtypes = [ctypes.c_void_p]
|
||||
self.lib.dita_kernel_destroy.restype = None
|
||||
self.lib.dita_kernel_free_string.argtypes = [ctypes.c_void_p]
|
||||
self.lib.dita_kernel_free_string.restype = None
|
||||
self.lib.dita_kernel_get_slot_json.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
|
||||
self.lib.dita_kernel_get_slot_json.restype = ctypes.c_void_p
|
||||
self.lib.dita_kernel_set_slot_json.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_char_p]
|
||||
self.lib.dita_kernel_set_slot_json.restype = ctypes.c_int
|
||||
self.lib.dita_kernel_process_intent_json.argtypes = [
|
||||
ctypes.c_void_p,
|
||||
ctypes.c_char_p,
|
||||
ctypes.c_char_p,
|
||||
ctypes.c_char_p,
|
||||
]
|
||||
self.lib.dita_kernel_process_intent_json.restype = ctypes.c_void_p
|
||||
self.lib.dita_kernel_on_venue_event_json.argtypes = [
|
||||
ctypes.c_void_p,
|
||||
ctypes.c_char_p,
|
||||
ctypes.c_char_p,
|
||||
ctypes.c_char_p,
|
||||
]
|
||||
self.lib.dita_kernel_on_venue_event_json.restype = ctypes.c_void_p
|
||||
self.lib.dita_kernel_reconcile_slots_json.argtypes = [
|
||||
ctypes.c_void_p,
|
||||
ctypes.c_char_p,
|
||||
ctypes.c_char_p,
|
||||
ctypes.c_char_p,
|
||||
]
|
||||
self.lib.dita_kernel_reconcile_slots_json.restype = ctypes.c_void_p
|
||||
self.lib.dita_kernel_snapshot_json.argtypes = [ctypes.c_void_p]
|
||||
self.lib.dita_kernel_snapshot_json.restype = ctypes.c_void_p
|
||||
|
||||
def create(self, max_slots: int) -> ctypes.c_void_p:
|
||||
handle = self.lib.dita_kernel_create(ctypes.c_size_t(max_slots))
|
||||
if not handle:
|
||||
raise RuntimeError("dita_kernel_create failed")
|
||||
return ctypes.c_void_p(handle)
|
||||
|
||||
def destroy(self, handle: ctypes.c_void_p) -> None:
|
||||
if handle and handle.value:
|
||||
self.lib.dita_kernel_destroy(handle)
|
||||
|
||||
def _take_string(self, raw: ctypes.c_void_p) -> str:
|
||||
if not raw:
|
||||
raise RuntimeError("Rust kernel returned null string")
|
||||
text = ctypes.cast(raw, ctypes.c_char_p).value
|
||||
if text is None:
|
||||
self.lib.dita_kernel_free_string(raw)
|
||||
raise RuntimeError("Rust kernel returned empty string")
|
||||
try:
|
||||
return text.decode("utf-8")
|
||||
finally:
|
||||
self.lib.dita_kernel_free_string(raw)
|
||||
|
||||
def get_slot_json(self, handle: ctypes.c_void_p, slot_id: int) -> Dict[str, Any]:
|
||||
raw = self.lib.dita_kernel_get_slot_json(handle, ctypes.c_size_t(slot_id))
|
||||
if not raw:
|
||||
raise IndexError(f"Invalid slot id: {slot_id}")
|
||||
return json.loads(self._take_string(raw))
|
||||
|
||||
def set_slot_json(self, handle: ctypes.c_void_p, slot_id: int, payload: Dict[str, Any]) -> None:
|
||||
encoded = json.dumps(json_safe(payload), separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
||||
rc = self.lib.dita_kernel_set_slot_json(handle, ctypes.c_size_t(slot_id), ctypes.c_char_p(encoded))
|
||||
if rc != 0:
|
||||
raise RuntimeError(f"dita_kernel_set_slot_json failed rc={rc}")
|
||||
|
||||
def process_intent(
|
||||
self,
|
||||
handle: ctypes.c_void_p,
|
||||
payload: Dict[str, Any],
|
||||
*,
|
||||
mode: str,
|
||||
verbosity: str,
|
||||
) -> Dict[str, Any]:
|
||||
encoded = json.dumps(json_safe(payload), separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
||||
raw = self.lib.dita_kernel_process_intent_json(
|
||||
handle,
|
||||
ctypes.c_char_p(encoded),
|
||||
ctypes.c_char_p(mode.encode("utf-8")),
|
||||
ctypes.c_char_p(verbosity.encode("utf-8")),
|
||||
)
|
||||
return json.loads(self._take_string(raw))
|
||||
|
||||
def on_venue_event(
|
||||
self,
|
||||
handle: ctypes.c_void_p,
|
||||
payload: Dict[str, Any],
|
||||
*,
|
||||
mode: str,
|
||||
verbosity: str,
|
||||
) -> Dict[str, Any]:
|
||||
encoded = json.dumps(json_safe(payload), separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
||||
raw = self.lib.dita_kernel_on_venue_event_json(
|
||||
handle,
|
||||
ctypes.c_char_p(encoded),
|
||||
ctypes.c_char_p(mode.encode("utf-8")),
|
||||
ctypes.c_char_p(verbosity.encode("utf-8")),
|
||||
)
|
||||
return json.loads(self._take_string(raw))
|
||||
|
||||
def reconcile_slots(
|
||||
self,
|
||||
handle: ctypes.c_void_p,
|
||||
payload: Sequence[Dict[str, Any]],
|
||||
*,
|
||||
mode: str,
|
||||
verbosity: str,
|
||||
) -> Dict[str, Any]:
|
||||
encoded = json.dumps(json_safe(list(payload)), separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
||||
raw = self.lib.dita_kernel_reconcile_slots_json(
|
||||
handle,
|
||||
ctypes.c_char_p(encoded),
|
||||
ctypes.c_char_p(mode.encode("utf-8")),
|
||||
ctypes.c_char_p(verbosity.encode("utf-8")),
|
||||
)
|
||||
return json.loads(self._take_string(raw))
|
||||
|
||||
def snapshot(self, handle: ctypes.c_void_p) -> Dict[str, Any]:
|
||||
raw = self.lib.dita_kernel_snapshot_json(handle)
|
||||
return json.loads(self._take_string(raw))
|
||||
|
||||
|
||||
_RUST: _RustKernelLib | None = None # lazy init — avoids Rust build on import
|
||||
|
||||
|
||||
def _get_rust() -> _RustKernelLib:
|
||||
global _RUST
|
||||
if _RUST is None:
|
||||
_RUST = _RustKernelLib()
|
||||
return _RUST
|
||||
|
||||
|
||||
def _slot_to_payload(slot: TradeSlot) -> Dict[str, Any]:
|
||||
return slot.to_dict()
|
||||
|
||||
|
||||
def _order_to_payload(order: Optional[VenueOrder]) -> Optional[Dict[str, Any]]:
|
||||
if order is None:
|
||||
return None
|
||||
return {
|
||||
"internal_trade_id": order.internal_trade_id,
|
||||
"venue_order_id": order.venue_order_id,
|
||||
"venue_client_id": order.venue_client_id,
|
||||
"side": order.side.value,
|
||||
"intended_size": float(order.intended_size or 0.0),
|
||||
"filled_size": float(order.filled_size or 0.0),
|
||||
"average_fill_price": float(order.average_fill_price or 0.0),
|
||||
"status": order.status.value,
|
||||
"metadata": dict(order.metadata),
|
||||
}
|
||||
|
||||
|
||||
def _order_from_payload(payload: Optional[Dict[str, Any]], *, trade_id: str) -> Optional[VenueOrder]:
|
||||
if not isinstance(payload, dict):
|
||||
return None
|
||||
return VenueOrder(
|
||||
internal_trade_id=trade_id,
|
||||
venue_order_id=str(payload.get("venue_order_id", "")),
|
||||
venue_client_id=str(payload.get("venue_client_id", "")),
|
||||
side=TradeSide(str(payload.get("side", TradeSide.FLAT.value))),
|
||||
intended_size=float(payload.get("intended_size", 0.0)),
|
||||
filled_size=float(payload.get("filled_size", 0.0)),
|
||||
average_fill_price=float(payload.get("average_fill_price", 0.0)),
|
||||
status=VenueOrderStatus(str(payload.get("status", VenueOrderStatus.NEW.value))),
|
||||
metadata=dict(payload.get("metadata", {})),
|
||||
)
|
||||
|
||||
|
||||
def _slot_from_payload(payload: Dict[str, Any]) -> TradeSlot:
|
||||
return TradeSlot(
|
||||
slot_id=int(payload.get("slot_id", 0)),
|
||||
trade_id=str(payload.get("trade_id", "")),
|
||||
asset=str(payload.get("asset", "")),
|
||||
side=TradeSide(str(payload.get("side", TradeSide.FLAT.value))),
|
||||
entry_price=float(payload.get("entry_price", 0.0)),
|
||||
size=float(payload.get("size", 0.0)),
|
||||
initial_size=float(payload.get("initial_size", 0.0)),
|
||||
leverage=float(payload.get("leverage", 0.0)),
|
||||
entry_time=datetime.fromisoformat(payload["entry_time"]) if payload.get("entry_time") else None,
|
||||
unrealized_pnl=float(payload.get("unrealized_pnl", 0.0)),
|
||||
realized_pnl=float(payload.get("realized_pnl", 0.0)),
|
||||
closed=bool(payload.get("closed", False)),
|
||||
exit_leg_ratios=tuple(float(r) for r in payload.get("exit_leg_ratios", (1.0,))),
|
||||
active_leg_index=int(payload.get("active_leg_index", 0)),
|
||||
active_exit_order=_order_from_payload(payload.get("active_exit_order"), trade_id=str(payload.get("trade_id", ""))),
|
||||
active_entry_order=_order_from_payload(payload.get("active_entry_order"), trade_id=str(payload.get("trade_id", ""))),
|
||||
fsm_state=TradeStage(str(payload.get("fsm_state", TradeStage.IDLE.value))),
|
||||
close_reason=str(payload.get("close_reason", "")),
|
||||
last_event_time=datetime.fromisoformat(payload["last_event_time"]) if payload.get("last_event_time") else None,
|
||||
seen_event_ids=tuple(str(event_id) for event_id in payload.get("seen_event_ids", ())),
|
||||
metadata=dict(payload.get("metadata", {})),
|
||||
)
|
||||
|
||||
|
||||
def _intent_to_payload(intent: KernelIntent) -> Dict[str, Any]:
|
||||
return {
|
||||
"timestamp": intent.timestamp.isoformat() if hasattr(intent.timestamp, "isoformat") else str(intent.timestamp),
|
||||
"intent_id": intent.intent_id,
|
||||
"trade_id": intent.trade_id,
|
||||
"slot_id": intent.slot_id,
|
||||
"asset": intent.asset,
|
||||
"side": intent.side.value,
|
||||
"action": intent.action.value,
|
||||
"reference_price": float(intent.reference_price or 0.0),
|
||||
"target_size": float(intent.target_size or 0.0),
|
||||
"leverage": float(intent.leverage or 0.0),
|
||||
"exit_leg_ratios": list(intent.exit_leg_ratios),
|
||||
"reason": intent.reason,
|
||||
"metadata": dict(intent.metadata),
|
||||
"stage": intent.stage.value,
|
||||
}
|
||||
|
||||
|
||||
def _event_to_payload(event: VenueEvent) -> Dict[str, Any]:
|
||||
return {
|
||||
"timestamp": event.timestamp.isoformat() if hasattr(event.timestamp, "isoformat") else str(event.timestamp),
|
||||
"event_id": event.event_id,
|
||||
"trade_id": event.trade_id,
|
||||
"slot_id": event.slot_id,
|
||||
"kind": event.kind.value,
|
||||
"status": event.status.value,
|
||||
"venue_order_id": event.venue_order_id,
|
||||
"venue_client_id": event.venue_client_id,
|
||||
"side": event.side.value,
|
||||
"asset": event.asset,
|
||||
"price": float(event.price or 0.0),
|
||||
"size": float(event.size or 0.0),
|
||||
"filled_size": float(event.filled_size or 0.0),
|
||||
"remaining_size": float(event.remaining_size or 0.0),
|
||||
"reason": event.reason,
|
||||
"raw_payload": dict(event.raw_payload),
|
||||
"metadata": dict(event.metadata),
|
||||
}
|
||||
|
||||
|
||||
def _transition_from_payload(payload: Dict[str, Any]) -> KernelTransition:
|
||||
return KernelTransition(
|
||||
timestamp=datetime.fromisoformat(payload["timestamp"]),
|
||||
trade_id=str(payload.get("trade_id", "")),
|
||||
slot_id=int(payload.get("slot_id", 0)),
|
||||
prev_state=TradeStage(str(payload.get("prev_state", TradeStage.IDLE.value))),
|
||||
next_state=TradeStage(str(payload.get("next_state", TradeStage.IDLE.value))),
|
||||
trigger=str(payload.get("trigger", "")),
|
||||
intent_id=str(payload.get("intent_id", "")),
|
||||
event_id=str(payload.get("event_id", "")),
|
||||
control_mode=str(payload.get("control_mode", "")),
|
||||
control_verbosity=str(payload.get("control_verbosity", "")),
|
||||
details=dict(payload.get("details", {})),
|
||||
)
|
||||
|
||||
|
||||
def _outcome_from_payload(payload: Dict[str, Any]) -> KernelOutcome:
|
||||
return KernelOutcome(
|
||||
accepted=bool(payload.get("accepted", False)),
|
||||
slot_id=int(payload.get("slot_id", 0)),
|
||||
trade_id=str(payload.get("trade_id", "")),
|
||||
state=TradeStage(str(payload.get("state", TradeStage.IDLE.value))),
|
||||
diagnostic_code=KernelDiagnosticCode(str(payload.get("diagnostic_code", KernelDiagnosticCode.OK.value))),
|
||||
severity=KernelSeverity(str(payload.get("severity", KernelSeverity.INFO.value))),
|
||||
transitions=tuple(_transition_from_payload(row) for row in payload.get("transitions", [])),
|
||||
emitted_events=tuple(
|
||||
VenueEvent(
|
||||
timestamp=datetime.fromisoformat(row["timestamp"]),
|
||||
event_id=str(row.get("event_id", "")),
|
||||
trade_id=str(row.get("trade_id", "")),
|
||||
slot_id=int(row.get("slot_id", 0)),
|
||||
kind=KernelEventKind(str(row.get("kind", KernelEventKind.ORDER_ACK.value))),
|
||||
status=VenueEventStatus(str(row.get("status", VenueEventStatus.ACKED.value))),
|
||||
venue_order_id=str(row.get("venue_order_id", "")),
|
||||
venue_client_id=str(row.get("venue_client_id", "")),
|
||||
side=TradeSide(str(row.get("side", TradeSide.FLAT.value))),
|
||||
asset=str(row.get("asset", "")),
|
||||
price=float(row.get("price", 0.0)),
|
||||
size=float(row.get("size", 0.0)),
|
||||
filled_size=float(row.get("filled_size", 0.0)),
|
||||
remaining_size=float(row.get("remaining_size", 0.0)),
|
||||
reason=str(row.get("reason", "")),
|
||||
raw_payload=dict(row.get("raw_payload", {})),
|
||||
metadata=dict(row.get("metadata", {})),
|
||||
)
|
||||
for row in payload.get("emitted_events", [])
|
||||
),
|
||||
details=dict(payload.get("details", {})),
|
||||
)
|
||||
|
||||
|
||||
def _enum_text(value: Any) -> str:
|
||||
if hasattr(value, "value"):
|
||||
return str(getattr(value, "value"))
|
||||
return str(value)
|
||||
|
||||
|
||||
class KernelSlotView:
|
||||
"""Write-through view over a Rust-backed slot."""
|
||||
|
||||
def __init__(self, kernel: "ExecutionKernel", slot_id: int) -> None:
|
||||
object.__setattr__(self, "_kernel", kernel)
|
||||
object.__setattr__(self, "_slot_id", int(slot_id))
|
||||
|
||||
@property
|
||||
def slot_id(self) -> int:
|
||||
return object.__getattribute__(self, "_slot_id")
|
||||
|
||||
def _snapshot(self) -> TradeSlot:
|
||||
return self._kernel._get_slot(self.slot_id)
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
slot = self._snapshot()
|
||||
if hasattr(slot, name):
|
||||
return getattr(slot, name)
|
||||
raise AttributeError(name)
|
||||
|
||||
def __setattr__(self, name: str, value: Any) -> None:
|
||||
if name in {"_kernel", "_slot_id"}:
|
||||
object.__setattr__(self, name, value)
|
||||
return
|
||||
slot = self._snapshot()
|
||||
if not hasattr(slot, name):
|
||||
raise AttributeError(name)
|
||||
setattr(slot, name, value)
|
||||
self._kernel._set_slot(slot)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return self._snapshot().to_dict()
|
||||
|
||||
def is_free(self) -> bool:
|
||||
return self._snapshot().is_free()
|
||||
|
||||
def is_open(self) -> bool:
|
||||
return self._snapshot().is_open()
|
||||
|
||||
def mark_price(self, price: float) -> None:
|
||||
slot = self._snapshot()
|
||||
slot.mark_price(price)
|
||||
self._kernel._set_slot(slot)
|
||||
|
||||
def next_exit_ratio(self) -> float:
|
||||
return self._snapshot().next_exit_ratio()
|
||||
|
||||
def consume_exit_leg(self) -> float:
|
||||
slot = self._snapshot()
|
||||
ratio = slot.consume_exit_leg()
|
||||
self._kernel._set_slot(slot)
|
||||
return ratio
|
||||
|
||||
def attach_entry_order(self, order: VenueOrder) -> None:
|
||||
slot = self._snapshot()
|
||||
slot.active_entry_order = order
|
||||
self._kernel._set_slot(slot)
|
||||
|
||||
def attach_exit_order(self, order: VenueOrder) -> None:
|
||||
slot = self._snapshot()
|
||||
slot.active_exit_order = order
|
||||
self._kernel._set_slot(slot)
|
||||
|
||||
def __repr__(self) -> str: # pragma: no cover - debugging helper
|
||||
return f"KernelSlotView(slot_id={self.slot_id}, state={self._snapshot().fsm_state.value})"
|
||||
|
||||
|
||||
class KernelStateView:
|
||||
def __init__(self, kernel: "ExecutionKernel") -> None:
|
||||
self._kernel = kernel
|
||||
self.slots = [KernelSlotView(kernel, slot_id) for slot_id in range(kernel.max_slots)]
|
||||
self.active_trade_index: Dict[str, int] = {}
|
||||
self.venue_order_index: Dict[str, int] = {}
|
||||
self.client_order_index: Dict[str, int] = {}
|
||||
self.refresh()
|
||||
|
||||
def refresh(self) -> None:
|
||||
snapshot = self._kernel._snapshot_backend()
|
||||
self.active_trade_index = dict(snapshot.get("active_trade_index", {}))
|
||||
self.venue_order_index = dict(snapshot.get("venue_order_index", {}))
|
||||
self.client_order_index = dict(snapshot.get("client_order_index", {}))
|
||||
|
||||
|
||||
class ExecutionKernel:
|
||||
"""Rust-backed multi-slot execution kernel."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
max_slots: int = 10,
|
||||
control_plane: Optional[ControlPlane] = None,
|
||||
venue: Optional[VenueAdapter] = None,
|
||||
journal: Optional[KernelJournal] = None,
|
||||
account: Optional[AccountProjection] = None,
|
||||
projection: Optional[HazelcastProjection] = None,
|
||||
projection_client: Optional[Any] = None,
|
||||
zinc_plane: Optional[ZincPlane] = None,
|
||||
) -> None:
|
||||
self.max_slots = int(max_slots)
|
||||
self.control_plane = control_plane or build_control_plane()
|
||||
self.venue = venue or MockVenueAdapter()
|
||||
self.journal = journal or MemoryKernelJournal()
|
||||
self.account = account or AccountProjection()
|
||||
self.projection = projection or build_projection(client=projection_client)
|
||||
self.zinc_plane = zinc_plane or InMemoryZincPlane()
|
||||
self._backend = _get_rust().create(self.max_slots)
|
||||
self._control_snapshot = self.control_plane.read()
|
||||
self.projection.write_control(self._control_snapshot)
|
||||
self.zinc_plane.update_control(self._control_snapshot)
|
||||
self.state = KernelStateView(self)
|
||||
self.account.observe_slots([self._get_slot(slot_id) for slot_id in range(self.max_slots)])
|
||||
|
||||
def __del__(self) -> None: # pragma: no cover - cleanup best effort
|
||||
backend = getattr(self, "_backend", None)
|
||||
if backend is not None:
|
||||
try:
|
||||
_get_rust().destroy(backend)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@property
|
||||
def control(self) -> KernelControlSnapshot:
|
||||
return self.control_plane.read()
|
||||
|
||||
def update_control(self, update: ControlUpdate) -> KernelControlSnapshot:
|
||||
snapshot = self.control_plane.update(update)
|
||||
self._control_snapshot = snapshot
|
||||
self.projection.write_control(snapshot)
|
||||
self.zinc_plane.update_control(snapshot)
|
||||
return snapshot
|
||||
|
||||
def _snapshot_backend(self) -> Dict[str, Any]:
|
||||
return _get_rust().snapshot(self._backend)
|
||||
|
||||
def _get_slot(self, slot_id: int) -> TradeSlot:
|
||||
return _slot_from_payload(_get_rust().get_slot_json(self._backend, slot_id))
|
||||
|
||||
def _set_slot(self, slot: TradeSlot, *, journal: bool = False) -> None:
|
||||
payload = _slot_to_payload(slot)
|
||||
_get_rust().set_slot_json(self._backend, slot.slot_id, payload)
|
||||
self.state.refresh()
|
||||
slots = [self._get_slot(slot_id) for slot_id in range(self.max_slots)]
|
||||
self.account.observe_slots(slots)
|
||||
current = self._get_slot(slot.slot_id)
|
||||
self.projection.write_slot(current)
|
||||
self.zinc_plane.write_slot(current)
|
||||
|
||||
def slot(self, slot_id: int) -> KernelSlotView:
|
||||
if not (0 <= int(slot_id) < self.max_slots):
|
||||
raise IndexError(slot_id)
|
||||
return self.state.slots[int(slot_id)]
|
||||
|
||||
def free_slot(self) -> Optional[KernelSlotView]:
|
||||
for slot in self.state.slots:
|
||||
if slot.is_free():
|
||||
return slot
|
||||
return None
|
||||
|
||||
def _record_transitions(self, transitions: Iterable[KernelTransition], slot: TradeSlot, event: Optional[VenueEvent]) -> None:
|
||||
if self.control.debug_clickhouse_enabled:
|
||||
for transition in transitions:
|
||||
self.journal.record_transition(
|
||||
transition=transition,
|
||||
slot=slot,
|
||||
event=event,
|
||||
control=self.control,
|
||||
)
|
||||
|
||||
def process_intent(self, intent: KernelIntent) -> KernelOutcome:
|
||||
self.zinc_plane.publish_intent(intent)
|
||||
if not (0 <= int(intent.slot_id) < self.max_slots):
|
||||
return KernelOutcome(
|
||||
accepted=False,
|
||||
slot_id=int(intent.slot_id),
|
||||
trade_id=intent.trade_id,
|
||||
state=TradeStage.IDLE,
|
||||
diagnostic_code=KernelDiagnosticCode.INVALID_SLOT_ID,
|
||||
details={"reason": "INVALID_SLOT_ID", "slot_id": int(intent.slot_id), "intent_id": intent.intent_id},
|
||||
)
|
||||
payload = _intent_to_payload(intent)
|
||||
result = _get_rust().process_intent(
|
||||
self._backend,
|
||||
payload,
|
||||
mode=_enum_text(self.control.mode),
|
||||
verbosity=_enum_text(self.control.verbosity),
|
||||
)
|
||||
outcome = _outcome_from_payload(result["outcome"])
|
||||
self.state.refresh()
|
||||
emitted_events = []
|
||||
if intent.action in {KernelCommandType.ENTER, KernelCommandType.EXIT}:
|
||||
emitted_events = self.venue.submit(intent)
|
||||
for event in emitted_events:
|
||||
self.on_venue_event(event)
|
||||
elif intent.action == KernelCommandType.CANCEL:
|
||||
emitted_events = self.venue.cancel(self.slot(intent.slot_id).active_exit_order, reason=intent.reason) if self.slot(intent.slot_id).active_exit_order else []
|
||||
for event in emitted_events:
|
||||
self.on_venue_event(event)
|
||||
|
||||
final_slot = self._get_slot(outcome.slot_id)
|
||||
rate_limit_event = next((event for event in emitted_events if event.kind == KernelEventKind.RATE_LIMITED), None)
|
||||
if rate_limit_event is not None:
|
||||
rate_limit_details = dict(outcome.details)
|
||||
rate_limit_details.update(
|
||||
{
|
||||
"reason": rate_limit_event.reason or "RATE_LIMITED",
|
||||
"retry_after_ms": int(rate_limit_event.metadata.get("retry_after_ms", 0) or 0),
|
||||
"venue_event_kind": rate_limit_event.kind.value,
|
||||
"severity": KernelSeverity.WARNING.value,
|
||||
"release_eta": "few minutes",
|
||||
"retryable": True,
|
||||
}
|
||||
)
|
||||
outcome = KernelOutcome(
|
||||
accepted=False,
|
||||
slot_id=outcome.slot_id,
|
||||
trade_id=outcome.trade_id,
|
||||
state=final_slot.fsm_state,
|
||||
diagnostic_code=KernelDiagnosticCode.RATE_LIMITED,
|
||||
severity=KernelSeverity.WARNING,
|
||||
transitions=outcome.transitions,
|
||||
emitted_events=outcome.emitted_events,
|
||||
details=rate_limit_details,
|
||||
)
|
||||
final_outcome = KernelOutcome(
|
||||
accepted=outcome.accepted,
|
||||
slot_id=outcome.slot_id,
|
||||
trade_id=final_slot.trade_id,
|
||||
state=final_slot.fsm_state,
|
||||
diagnostic_code=outcome.diagnostic_code,
|
||||
transitions=outcome.transitions,
|
||||
emitted_events=tuple(emitted_events),
|
||||
details=dict(outcome.details),
|
||||
)
|
||||
slots = [self._get_slot(i) for i in range(self.max_slots)]
|
||||
self.account.observe_slots(slots)
|
||||
current = self._get_slot(final_slot.slot_id)
|
||||
self.projection.write_slot(current)
|
||||
self.zinc_plane.write_slot(current)
|
||||
self._record_transitions(outcome.transitions, final_slot, None)
|
||||
return final_outcome
|
||||
|
||||
def on_venue_event(self, event: VenueEvent) -> KernelOutcome:
|
||||
result = _get_rust().on_venue_event(
|
||||
self._backend,
|
||||
_event_to_payload(event),
|
||||
mode=_enum_text(self.control.mode),
|
||||
verbosity=_enum_text(self.control.verbosity),
|
||||
)
|
||||
outcome = _outcome_from_payload(result["outcome"])
|
||||
slot = _slot_from_payload(result["slot"])
|
||||
self.state.refresh()
|
||||
# Single capital mutation point: settle realiized PnL when a fill
|
||||
# transitions the slot to a terminal closed state. This is the *only*
|
||||
# place post-startup where capital is changed — no external balance
|
||||
# polls overwrite it.
|
||||
if slot.fsm_state in {TradeStage.CLOSED, TradeStage.TRADE_TERMINAL_WRITTEN} and slot.realized_pnl != 0.0:
|
||||
self.account.settle(slot.realized_pnl)
|
||||
slots = [self._get_slot(i) for i in range(self.max_slots)]
|
||||
self.account.observe_slots(slots)
|
||||
current = self._get_slot(slot.slot_id)
|
||||
self.projection.write_slot(current)
|
||||
self.zinc_plane.write_slot(current)
|
||||
self._record_transitions(outcome.transitions, slot, event)
|
||||
return outcome
|
||||
|
||||
def mark_price(self, asset: str, price: float) -> None:
|
||||
for slot in self.state.slots:
|
||||
if slot.asset == asset and slot.is_open():
|
||||
slot.mark_price(price)
|
||||
self.account.observe_slots([self._get_slot(i) for i in range(self.max_slots)])
|
||||
|
||||
def reconcile_from_slots(self, slots: Sequence[TradeSlot]) -> KernelOutcome:
|
||||
payload = [_slot_to_payload(slot) for slot in slots]
|
||||
result = _get_rust().reconcile_slots(
|
||||
self._backend,
|
||||
payload,
|
||||
mode=_enum_text(self.control.mode),
|
||||
verbosity=_enum_text(self.control.verbosity),
|
||||
)
|
||||
outcome = _outcome_from_payload(result["outcome"])
|
||||
self.state.refresh()
|
||||
slots = [self._get_slot(i) for i in range(self.max_slots)]
|
||||
self.account.observe_slots(slots)
|
||||
for current in slots:
|
||||
self.projection.write_slot(current)
|
||||
self.zinc_plane.write_slot(current)
|
||||
return outcome
|
||||
|
||||
def snapshot(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"control": self.control.as_dict(),
|
||||
"slots": [self._get_slot(slot.slot_id).to_dict() for slot in self.state.slots],
|
||||
"account": {
|
||||
"capital": self.account.snapshot.capital,
|
||||
"equity": self.account.snapshot.equity,
|
||||
"realized_pnl": self.account.snapshot.realized_pnl,
|
||||
"unrealized_pnl": self.account.snapshot.unrealized_pnl,
|
||||
"open_positions": self.account.snapshot.open_positions,
|
||||
"open_notional": self.account.snapshot.open_notional,
|
||||
"leverage": self.account.snapshot.leverage,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "dita-v2-kernel"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
libc = "0.2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
1613
prod/clean_arch/dita_v2/_backup_20260530/rust_kernel_src/lib.rs
Normal file
1613
prod/clean_arch/dita_v2/_backup_20260530/rust_kernel_src/lib.rs
Normal file
File diff suppressed because it is too large
Load Diff
43
prod/clean_arch/dita_v2/_backup_20260530/utils.py
Normal file
43
prod/clean_arch/dita_v2/_backup_20260530/utils.py
Normal 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)
|
||||
37
prod/clean_arch/dita_v2/_backup_20260530/venue.py
Normal file
37
prod/clean_arch/dita_v2/_backup_20260530/venue.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""Venue adapter contracts for DITAv2."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional, Protocol
|
||||
|
||||
from .contracts import (
|
||||
KernelCommandType,
|
||||
KernelIntent,
|
||||
KernelEventKind,
|
||||
TradeSide,
|
||||
VenueEvent,
|
||||
VenueEventStatus,
|
||||
VenueOrder,
|
||||
VenueOrderStatus,
|
||||
)
|
||||
|
||||
|
||||
class VenueAdapter(Protocol):
|
||||
"""Abstract venue adapter used by the kernel."""
|
||||
|
||||
def submit(self, intent: KernelIntent) -> List[VenueEvent]:
|
||||
...
|
||||
|
||||
def cancel(self, order: VenueOrder, *, reason: str = "") -> List[VenueEvent]:
|
||||
...
|
||||
|
||||
def open_orders(self) -> List[VenueOrder]:
|
||||
...
|
||||
|
||||
def open_positions(self) -> List[Dict[str, Any]]:
|
||||
...
|
||||
|
||||
def reconcile(self) -> List[VenueEvent]:
|
||||
...
|
||||
135
prod/clean_arch/dita_v2/_backup_20260530/zinc_plane.py
Normal file
135
prod/clean_arch/dita_v2/_backup_20260530/zinc_plane.py
Normal 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
|
||||
Reference in New Issue
Block a user