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()