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>
197 lines
6.8 KiB
Python
197 lines
6.8 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
import unittest
|
|
|
|
from prod.clean_arch.dita_v2 import (
|
|
ControlUpdate,
|
|
ExecutionKernel,
|
|
HazelcastProjection,
|
|
KernelCommandType,
|
|
KernelControlSnapshot,
|
|
KernelIntent,
|
|
KernelMode,
|
|
KernelVerbosity,
|
|
MockVenueAdapter,
|
|
MockVenueScenario,
|
|
TradeSide,
|
|
TradeStage,
|
|
TradeSlot,
|
|
build_projection,
|
|
build_position_state_row,
|
|
)
|
|
from prod.clean_arch.dita_v2.hazelcast_projection import HazelcastProjector, HazelcastRowWriter
|
|
|
|
|
|
class CaptureSink:
|
|
def __init__(self) -> None:
|
|
self.rows: list[tuple[str, dict[str, object]]] = []
|
|
|
|
def __call__(self, name: str, row: dict[str, object]) -> None:
|
|
self.rows.append((name, dict(row)))
|
|
|
|
|
|
class FakeMap:
|
|
def __init__(self) -> None:
|
|
self.rows: dict[str, object] = {}
|
|
|
|
def put(self, key: str, value: object) -> None:
|
|
self.rows[key] = value
|
|
|
|
|
|
class FakeTopic:
|
|
def __init__(self) -> None:
|
|
self.messages: list[str] = []
|
|
|
|
def publish(self, message: str) -> None:
|
|
self.messages.append(message)
|
|
|
|
|
|
class FakeHazelcastClient:
|
|
def __init__(self) -> None:
|
|
self.maps: dict[str, FakeMap] = {}
|
|
self.topics: dict[str, FakeTopic] = {}
|
|
|
|
def get_map(self, name: str) -> FakeMap:
|
|
return self.maps.setdefault(name, FakeMap())
|
|
|
|
def get_topic(self, name: str) -> FakeTopic:
|
|
return self.topics.setdefault(name, FakeTopic())
|
|
|
|
|
|
class TestDITAv2Hazelcast(unittest.TestCase):
|
|
def test_build_position_state_row_has_compatibility_fields(self) -> None:
|
|
slot = TradeSlot(
|
|
slot_id=0,
|
|
trade_id="trade-1",
|
|
asset="BTCUSDT",
|
|
side=TradeSide.SHORT,
|
|
entry_price=100.0,
|
|
size=1.0,
|
|
initial_size=1.0,
|
|
leverage=2.0,
|
|
fsm_state=TradeStage.POSITION_OPEN,
|
|
)
|
|
row = build_position_state_row(
|
|
slot,
|
|
KernelControlSnapshot(
|
|
mode=KernelMode.DEBUG,
|
|
verbosity=KernelVerbosity.TRACE,
|
|
runtime_namespace="dita_v2",
|
|
strategy_namespace="dita_v2",
|
|
event_namespace="dita_v2",
|
|
actor_name="ExecutionKernel",
|
|
exec_venue="bingx",
|
|
data_venue="binance",
|
|
ledger_authority="exchange",
|
|
),
|
|
)
|
|
for key in (
|
|
"runtime_namespace",
|
|
"strategy_namespace",
|
|
"event_namespace",
|
|
"actor_name",
|
|
"exec_venue",
|
|
"data_venue",
|
|
"ledger_authority",
|
|
"trade_id",
|
|
"asset",
|
|
"slot_id",
|
|
"fsm_state",
|
|
):
|
|
self.assertIn(key, row)
|
|
self.assertEqual(row["trade_id"], "trade-1")
|
|
self.assertEqual(row["fsm_state"], TradeStage.POSITION_OPEN.value)
|
|
|
|
def test_projection_sink_writes_blue_pink_compatible_rows(self) -> None:
|
|
sink = CaptureSink()
|
|
projection = HazelcastProjection(writer=sink)
|
|
control = KernelControlSnapshot(mode=KernelMode.DEBUG, verbosity=KernelVerbosity.TRACE)
|
|
projection.write_control(control)
|
|
slot = TradeSlot(
|
|
slot_id=1,
|
|
trade_id="trade-2",
|
|
asset="ETHUSDT",
|
|
side=TradeSide.LONG,
|
|
entry_price=50.0,
|
|
size=2.0,
|
|
initial_size=2.0,
|
|
leverage=3.0,
|
|
fsm_state=TradeStage.POSITION_OPEN,
|
|
)
|
|
projection.write_slot(slot)
|
|
self.assertGreaterEqual(len(sink.rows), 2)
|
|
control_name, control_row = sink.rows[0]
|
|
slot_name, slot_row = sink.rows[1]
|
|
self.assertEqual(control_name, "hz:dita_control")
|
|
self.assertEqual(slot_name, "hz:dita_active_slots")
|
|
self.assertEqual(control_row["mode"], KernelMode.DEBUG.value)
|
|
self.assertEqual(slot_row["trade_id"], "trade-2")
|
|
self.assertEqual(slot_row["runtime_namespace"], "dita_v2")
|
|
self.assertEqual(slot_row["ledger_authority"], "exchange")
|
|
|
|
def test_hazelcast_row_writer_routes_maps_and_topics(self) -> None:
|
|
client = FakeHazelcastClient()
|
|
writer = HazelcastRowWriter(client)
|
|
writer("hz:dita_active_slots", {"trade_id": "trade-3", "slot_id": 0})
|
|
writer("hz:dita_control", {"mode": "DEBUG"})
|
|
writer("hz:dita_trade_events", {"event_id": "evt-1", "trade_id": "trade-3"})
|
|
self.assertIn("trade-3", client.get_map("hz:dita_active_slots").rows)
|
|
self.assertIn("control", client.get_map("hz:dita_control").rows)
|
|
self.assertEqual(len(client.get_topic("hz:dita_trade_events").messages), 1)
|
|
|
|
def test_build_projection_uses_client_when_requested(self) -> None:
|
|
client = FakeHazelcastClient()
|
|
projection = build_projection(client=client, prefer_real_hazelcast=True)
|
|
projection.write_control(KernelControlSnapshot(mode=KernelMode.DEBUG, verbosity=KernelVerbosity.TRACE))
|
|
projection.write_slot(
|
|
TradeSlot(
|
|
slot_id=0,
|
|
trade_id="trade-4",
|
|
asset="BTCUSDT",
|
|
side=TradeSide.SHORT,
|
|
entry_price=100.0,
|
|
size=1.0,
|
|
initial_size=1.0,
|
|
leverage=2.0,
|
|
fsm_state=TradeStage.POSITION_OPEN,
|
|
)
|
|
)
|
|
self.assertIn("control", client.get_map("hz:dita_control").rows)
|
|
self.assertIn("trade-4", client.get_map("hz:dita_active_slots").rows)
|
|
|
|
def test_kernel_emits_projection_rows(self) -> None:
|
|
sink = CaptureSink()
|
|
kernel = ExecutionKernel(
|
|
control_plane=None,
|
|
venue=MockVenueAdapter(MockVenueScenario(emit_fill_on_submit=True, partial_fill_ratio=1.0)),
|
|
projection=HazelcastProjection(writer=sink),
|
|
)
|
|
kernel.update_control(ControlUpdate(mode=KernelMode.DEBUG, verbosity=KernelVerbosity.TRACE))
|
|
kernel.process_intent(
|
|
KernelIntent(
|
|
timestamp=datetime.now(timezone.utc),
|
|
intent_id="intent-1",
|
|
trade_id="trade-1",
|
|
slot_id=0,
|
|
asset="BTCUSDT",
|
|
side=TradeSide.SHORT,
|
|
action=KernelCommandType.ENTER,
|
|
reference_price=100.0,
|
|
target_size=1.0,
|
|
leverage=2.0,
|
|
exit_leg_ratios=(1.0,),
|
|
reason="TEST",
|
|
)
|
|
)
|
|
names = [name for name, _ in sink.rows]
|
|
self.assertIn("hz:dita_control", names)
|
|
self.assertIn("hz:dita_active_slots", names)
|
|
slot_rows = [row for name, row in sink.rows if name == "hz:dita_active_slots"]
|
|
self.assertTrue(any(row["trade_id"] == "trade-1" for row in slot_rows))
|
|
self.assertTrue(any(row["runtime_namespace"] == "dita_v2" for row in slot_rows))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|