from __future__ import annotations import asyncio from dataclasses import dataclass, field from datetime import datetime, timezone from typing import Any, Optional, List, Dict from prod.clean_arch.dita import ( Decision, Intent, DecisionConfig, DecisionEngine, IntentEngine, TradeSide as LegacyTradeSide, ) from prod.clean_arch.ports.data_feed import MarketSnapshot from prod.clean_arch.runtime.pink_direct import PinkDirectRuntime, _decision_to_kernel_intent from prod.clean_arch.dita_v2.contracts import ( KernelCommandType, KernelDiagnosticCode, KernelIntent, KernelOutcome, KernelSeverity, KernelTransition, TradeSide, TradeSlot, TradeStage, VenueEvent, VenueEventStatus, VenueOrder, VenueOrderStatus, KernelEventKind, ) @dataclass class _FakeFeed: """Fake Hazelcast data feed — returns canned snapshots.""" connected: bool = False _snapshots: list[MarketSnapshot | None] = field(default_factory=list) async def connect(self) -> bool: self.connected = True return True async def disconnect(self) -> None: self.connected = False async def get_latest_snapshot(self, symbol: str) -> MarketSnapshot | None: if self._snapshots: return self._snapshots.pop(0) return None class _FakeMarketStateRuntime: """Fake market state runtime — records calls, returns canned bundle.""" def __init__(self) -> None: self.calls: list[dict[str, Any]] = [] self.latest_bundle_dict: dict[str, Any] = { "market_fingerprint_choppiness_strength": 0.2, "market_fingerprint_trend_persistence": 0.4, "market_state_top_asset_target": "BTCUSDT", } def update_scan_state(self, **kwargs): self.calls.append(dict(kwargs)) return type("Bundle", (), {"as_dict": lambda self: dict(kwargs)})() class _FakeKernelAccount: """Minimal kernel account projection stand-in.""" def __init__(self, capital: float = 25000.0): self.snapshot = type("Snap", (), { "capital": capital, "equity": capital, "peak_capital": capital, "realized_pnl": 0.0, "unrealized_pnl": 0.0, "open_positions": 0, "open_notional": 0.0, "leverage": 0.0, "trade_seq": 0, })() class _FakeSlotView: """Minimal slot view stand-in.""" def __init__(self, slot_dict: dict | None = None): d = slot_dict or { "slot_id": 0, "trade_id": "", "asset": "", "side": "FLAT", "entry_price": 0.0, "size": 0.0, "initial_size": 0.0, "leverage": 0.0, "realized_pnl": 0.0, "unrealized_pnl": 0.0, "closed": False, "close_reason": "", "fsm_state": "IDLE", "exit_leg_ratios": [], "active_leg_index": 0, "active_exit_order": None, "active_entry_order": None, "entry_velocity_divergence": 0.0, "entry_irp_alignment": 0.0, } self._d = d state_str = d.get("fsm_state", "IDLE") # Map string to enum for s in TradeStage: if s.value == state_str: self.fsm_state = s break else: self.fsm_state = TradeStage.IDLE def to_dict(self) -> dict: return dict(self._d) def is_free(self) -> bool: return self.fsm_state in {TradeStage.IDLE, TradeStage.CLOSED} def is_open(self) -> bool: return self.fsm_state in { TradeStage.ENTRY_WORKING, TradeStage.POSITION_OPENED, TradeStage.POSITION_OPEN, TradeStage.EXIT_WORKING, } def mark_price(self, price: float) -> None: self._d["entry_price"] = price class _FakeVenue: """Fake venue for runtime tests — simulates position lifecycle.""" def __init__(self): self._capital = 25000.0 self._position: dict | None = None self._trade_seq = 0 self._connected = False async def connect(self): self._connected = True async def disconnect(self): self._connected = False async def reconcile(self) -> dict: return { "capital": self._capital, "equity": self._capital, "open_positions": {} if self._position is None else {self._position["trade_id"]: self._position}, "open_orders": [], } def open_positions(self) -> list[dict]: return [dict(self._position)] if self._position else [] class _FakeKernel: """Fake DITAv2 ExecutionKernel for runtime tests. Tracks an internal position lifecycle matching the _FakeVenue. """ def __init__(self, capital: float = 25000.0): self.max_slots = 1 self.account = _FakeKernelAccount(capital) self.venue = _FakeVenue() self._slots: dict[int, _FakeSlotView] = {0: _FakeSlotView()} self._capital = capital self._position: dict | None = None def slot(self, slot_id: int) -> _FakeSlotView: return self._slots.get(slot_id, _FakeSlotView()) def snapshot(self) -> dict: return { "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, "trade_seq": self.account.snapshot.trade_seq, }, "slots": [self.slot(0).to_dict()], } def process_intent(self, intent: KernelIntent) -> KernelOutcome: """Simulate entry/exit lifecycle matching old _FakeExecution logic.""" price = float(intent.reference_price or 0.0) qty = float(intent.target_size or 0.0) if intent.action == KernelCommandType.ENTER: self._position = { "trade_id": intent.trade_id, "asset": intent.asset, "side": "SHORT" if intent.side == TradeSide.SHORT else "LONG", "entry_price": price, "size": qty, "leverage": float(intent.leverage or 1.0), } self._slots[0] = _FakeSlotView({ "slot_id": 0, "trade_id": intent.trade_id, "asset": intent.asset, "side": self._position["side"], "entry_price": price, "size": qty, "initial_size": qty, "leverage": float(intent.leverage or 1.0), "realized_pnl": 0.0, "unrealized_pnl": 0.0, "closed": False, "close_reason": "", "fsm_state": "POSITION_OPEN", "exit_leg_ratios": list(intent.exit_leg_ratios), "active_leg_index": 0, "active_exit_order": None, "active_entry_order": None, }) self.account.snapshot.open_positions = 1 self.account.snapshot.open_notional = qty * price self.account.snapshot.trade_seq += 1 elif intent.action == KernelCommandType.EXIT and self._position is not None: current_qty = float(self._position["size"]) remaining = max(0.0, current_qty - qty) entry_price = float(self._position["entry_price"]) leverage = float(self._position.get("leverage", 1.0)) pnl_pct = (entry_price - price) / entry_price # short profit realized = pnl_pct * qty * entry_price * leverage self._capital += realized self.account.snapshot.capital = self._capital self.account.snapshot.realized_pnl += realized self.account.snapshot.peak_capital = max(self.account.snapshot.peak_capital, self._capital) self.account.snapshot.equity = self._capital if remaining <= 1e-12: self._position = None self._slots[0] = _FakeSlotView({ "slot_id": 0, "trade_id": intent.trade_id, "asset": intent.asset, "side": "FLAT", "entry_price": 0.0, "size": 0.0, "initial_size": 0.0, "leverage": 0.0, "realized_pnl": realized, "unrealized_pnl": 0.0, "closed": True, "close_reason": intent.reason, "fsm_state": "CLOSED", "exit_leg_ratios": [], "active_leg_index": 1, "active_exit_order": None, "active_entry_order": None, }) self.account.snapshot.open_positions = 0 self.account.snapshot.open_notional = 0.0 else: self._position["size"] = remaining self._slots[0] = _FakeSlotView({ "slot_id": 0, "trade_id": intent.trade_id, "asset": intent.asset, "side": "SHORT", "entry_price": entry_price, "size": remaining, "initial_size": qty, "leverage": leverage, "realized_pnl": realized, "unrealized_pnl": 0.0, "closed": False, "close_reason": "", "fsm_state": "POSITION_OPEN", "exit_leg_ratios": list(intent.exit_leg_ratios), "active_leg_index": 1, "active_exit_order": None, "active_entry_order": None, }) self.account.snapshot.open_positions = 1 self.account.snapshot.open_notional = remaining * entry_price elif intent.action == KernelCommandType.MARK_PRICE: if self._position: self._position["entry_price"] = price return KernelOutcome( accepted=True, slot_id=0, trade_id=intent.trade_id, state=TradeStage.POSITION_OPEN if self._position else TradeStage.IDLE, diagnostic_code=KernelDiagnosticCode.OK, severity=KernelSeverity.INFO, transitions=(), emitted_events=(), details={}, ) def mark_price(self, asset: str, price: float) -> None: self.slot(0).mark_price(price) def reconcile_from_slots(self, slots: list) -> KernelOutcome: # Populate slot from venue position if present if self.venue._position is not None: p = self.venue._position self._position = dict(p) self.venue._capital = self._capital self._slots[0] = _FakeSlotView({ "slot_id": 0, "trade_id": p.get("trade_id", ""), "asset": p.get("asset", ""), "side": p.get("side", "FLAT"), "entry_price": float(p.get("entry_price", 0.0)), "size": float(p.get("size", 0.0)), "initial_size": float(p.get("size", 0.0)), "leverage": float(p.get("leverage", 1.0)), "realized_pnl": 0.0, "unrealized_pnl": 0.0, "closed": False, "close_reason": "", "fsm_state": "POSITION_OPEN", "exit_leg_ratios": [1.0], "active_leg_index": 0, "active_exit_order": None, "active_entry_order": None, "entry_velocity_divergence": 0.0, "entry_irp_alignment": 0.0, }) self.account.snapshot.open_positions = 1 self.account.snapshot.open_notional = float(p.get("size", 0)) * float(p.get("entry_price", 0)) return KernelOutcome( accepted=True, slot_id=0, trade_id="", state=TradeStage.IDLE, diagnostic_code=KernelDiagnosticCode.OK, ) def _snapshot(price: float, vdiv: float, *, symbol: str = "BTCUSDT") -> MarketSnapshot: return MarketSnapshot( timestamp=datetime.now(timezone.utc), symbol=symbol, price=price, bid=price * 0.9995, ask=price * 1.0005, eigenvalues=[1.0, 0.9, 0.8], eigenvectors=None, velocity_divergence=vdiv, irp_alignment=0.5, scan_number=int(datetime.now(timezone.utc).timestamp()), source="pink_direct_runtime_test", scan_payload={ "version": "NG7", "scan_number": int(datetime.now(timezone.utc).timestamp()), "vel_div": vdiv, "w50_velocity": 0.01, "w750_velocity": 0.02, "posture": "APEX", "assets": [symbol], "asset_prices": [price], "market_fingerprint_choppiness_strength": 0.2, }, ) def test_runtime_handles_open_partial_close_and_terminal_close() -> None: """Full lifecycle: entry → partial exit → terminal exit via DITAv2 kernel.""" feed = _FakeFeed() kernel = _FakeKernel(capital=25000.0) market_state_runtime = _FakeMarketStateRuntime() cfg = DecisionConfig( vel_div_threshold=-0.02, fixed_tp_pct=0.002, capital_fraction=0.01, max_leverage=1.0, exit_leg_ratios=(0.5, 1.0), policy_version="pink_direct_test", ) runtime = PinkDirectRuntime( data_feed=feed, kernel=kernel, decision_engine=DecisionEngine(cfg), intent_engine=IntentEngine(cfg), market_state_runtime=market_state_runtime, ) asyncio.run(runtime.connect(initial_capital=25000.0)) asyncio.run(runtime.step(_snapshot(100.0, -0.1))) slot = kernel.slot(0) assert slot.is_open(), f"Expected open slot after entry, got {slot.fsm_state}" assert slot.to_dict().get("size", 0) > 0 assert market_state_runtime.calls asyncio.run(runtime.step(_snapshot(99.5, 0.05))) slot = kernel.slot(0) remaining = slot.to_dict().get("size", 0) assert remaining > 0, "Should still have position after partial exit" asyncio.run(runtime.step(_snapshot(99.3, 0.05))) slot = kernel.slot(0) # The decision engine decides whether to exit; what matters is that # capital was not corrupted (logic should be profitable). assert kernel.account.snapshot.capital > 25000.0, \ f"Expected capital > 25000 after profitable trades, got {kernel.account.snapshot.capital}" asyncio.run(runtime.disconnect()) assert feed.connected is False def test_runtime_enter_maps_correct_kernel_intent() -> None: """Verify the runtime's decision-to-intent translation is correct.""" from prod.clean_arch.dita import DecisionAction as DAction, TradeStage as TStage cfg = DecisionConfig(policy_version="pink_direct_test") runtime = PinkDirectRuntime( data_feed=_FakeFeed(), kernel=_FakeKernel(), decision_engine=DecisionEngine(cfg), intent_engine=IntentEngine(cfg), ) decision = Decision( timestamp=datetime.now(timezone.utc), decision_id="d-001", asset="BTCUSDT", action=DAction.ENTER, side=LegacyTradeSide.SHORT, reason="test", confidence=0.8, velocity_divergence=-0.03, irp_alignment=0.5, reference_price=65000.0, target_size=0.01, leverage=2.0, bars_held=0, stage=TStage.ORDER_REQUESTED, metadata={}, ) intent = Intent( timestamp=datetime.now(timezone.utc), trade_id="t-001", decision_id="d-001", asset="BTCUSDT", action=DAction.ENTER, side=LegacyTradeSide.SHORT, reason="test", target_size=0.01, leverage=2.0, reference_price=65000.0, confidence=0.8, bars_held=0, stage=TStage.INTENT_CREATED, exit_leg_ratios=(0.5, 1.0), metadata={}, ) ki = _decision_to_kernel_intent(decision, intent, slot_id=0) assert ki.action == KernelCommandType.ENTER assert ki.target_size == 0.01 assert ki.side == TradeSide.SHORT def test_runtime_recovers_from_exchange_state() -> None: """Startup recovery seeds slot from existing exchange position.""" feed = _FakeFeed() kernel = _FakeKernel(capital=25000.0) # Pre-seed a position in the kernel's venue kernel.venue._position = { "trade_id": "BTCUSDT", "asset": "BTCUSDT", "side": "SHORT", "entry_price": 100.0, "size": 1.5, "leverage": 1.0, } cfg = DecisionConfig(policy_version="pink_direct_test") runtime = PinkDirectRuntime( data_feed=feed, kernel=kernel, decision_engine=DecisionEngine(cfg), intent_engine=IntentEngine(cfg), market_state_runtime=_FakeMarketStateRuntime(), ) asyncio.run(runtime.connect(initial_capital=25000.0)) slot = kernel.slot(0) assert slot.is_open(), f"Expected open slot after recovery, got {slot.fsm_state}" assert slot.to_dict().get("size", 0) == 1.5, \ f"Expected size 1.5, got {slot.to_dict().get('size')}"