"""Trade execution and single-slot FSM.""" from __future__ import annotations from dataclasses import dataclass, field from datetime import datetime from typing import Any, Dict, List, Optional, Sequence from .contracts import DecisionAction, Intent, TradeEvent, TradePosition, TradeSide, TradeStage @dataclass(frozen=True) class TradeExecutionResult: """Result of applying an intent to a trade slot.""" intent: Intent receipt: Optional[Any] stages: Sequence[TradeStage] position_before: Optional[TradePosition] position_after: Optional[TradePosition] partial_close: bool = False class TradeExecutor: """Single-slot trade FSM. Owns the live position and translates executable intents into exchange requests and canonical lifecycle stages. """ def __init__(self) -> None: self.position: Optional[TradePosition] = None self.trade_history: List[TradeEvent] = [] def execute(self, intent: Intent, exchange: Any, capital_before: float) -> TradeExecutionResult: position_before = self._clone_position(self.position) if intent.action == DecisionAction.ENTER: return self._execute_enter(intent, exchange, capital_before, position_before) if intent.action == DecisionAction.EXIT: return self._execute_exit(intent, exchange, capital_before, position_before) return TradeExecutionResult( intent=intent, receipt=None, stages=(TradeStage.INTENT_CREATED,), position_before=position_before, position_after=self._clone_position(self.position), ) def apply_fill(self, receipt: Any, intent: Intent) -> None: if receipt is None: return if intent.action == DecisionAction.ENTER and receipt.status == "FILLED": self.position = TradePosition( trade_id=intent.trade_id, asset=intent.asset, side=intent.side, entry_price=receipt.fill_price, entry_time=intent.timestamp, size=receipt.fill_size, leverage=intent.leverage, entry_velocity_divergence=float( intent.metadata.get("entry_velocity_divergence", intent.metadata.get("velocity_divergence", 0.0)) ), entry_irp_alignment=float(intent.metadata.get("entry_irp_alignment", intent.confidence)), current_price=receipt.fill_price, initial_size=receipt.fill_size, exit_leg_ratios=tuple(intent.exit_leg_ratios), ) return if intent.action == DecisionAction.EXIT and self.position is not None and receipt.status == "FILLED": self.position.size = max(0.0, float(receipt.remaining_size)) self.position.exit_price = receipt.fill_price self.position.realized_pnl += receipt.realized_pnl self.position.mark_price(receipt.fill_price) self.position.closed = self.position.size <= 1e-12 self.position.close_reason = intent.reason if self.position.closed else "PARTIAL_" + intent.reason if self.position.closed: self.position = None def _execute_enter(self, intent: Intent, exchange: Any, capital_before: float, position_before: Optional[TradePosition]) -> TradeExecutionResult: if self.position is not None and not self.position.closed: return TradeExecutionResult(intent=intent, receipt=exchange.reject(intent, "POSITION_ALREADY_OPEN"), stages=(TradeStage.ORDER_REQUESTED,), position_before=position_before, position_after=self._clone_position(self.position)) receipt = exchange.submit(intent) stages = (TradeStage.ORDER_REQUESTED, TradeStage.ORDER_SENT) if receipt and receipt.status == "FILLED": self.apply_fill(receipt, intent) stages = stages + (TradeStage.ORDER_ACKED, TradeStage.POSITION_OPENED) return TradeExecutionResult(intent=intent, receipt=receipt, stages=stages, position_before=position_before, position_after=self._clone_position(self.position)) def _execute_exit(self, intent: Intent, exchange: Any, capital_before: float, position_before: Optional[TradePosition]) -> TradeExecutionResult: if self.position is None or self.position.closed: return TradeExecutionResult(intent=intent, receipt=exchange.reject(intent, "NO_OPEN_POSITION"), stages=(TradeStage.EXIT_REQUESTED,), position_before=position_before, position_after=None) receipt = exchange.submit(intent) stages = [TradeStage.EXIT_REQUESTED, TradeStage.EXIT_SENT] if receipt and receipt.status == "FILLED": self.apply_fill(receipt, intent) stages.append(TradeStage.EXIT_ACKED) if self.position is None: stages.extend([TradeStage.POSITION_CLOSED, TradeStage.TRADE_TERMINAL_WRITTEN]) else: stages.extend([TradeStage.POSITION_PARTIALLY_CLOSED, TradeStage.POSITION_UPDATED]) return TradeExecutionResult( intent=intent, receipt=receipt, stages=tuple(stages), position_before=position_before, position_after=self._clone_position(self.position), partial_close=self.position is not None, ) @staticmethod def _clone_position(position: Optional[TradePosition]) -> Optional[TradePosition]: if position is None: return None return TradePosition( trade_id=position.trade_id, asset=position.asset, side=position.side, entry_price=position.entry_price, entry_time=position.entry_time, size=position.size, leverage=position.leverage, entry_velocity_divergence=position.entry_velocity_divergence, entry_irp_alignment=position.entry_irp_alignment, bars_held=position.bars_held, current_price=position.current_price, realized_pnl=position.realized_pnl, unrealized_pnl=position.unrealized_pnl, exit_price=position.exit_price, closed=position.closed, close_reason=position.close_reason, initial_size=position.initial_size, exit_leg_ratios=position.exit_leg_ratios, exit_leg_index=position.exit_leg_index, ) def decision_confidence_from_intent(intent: Intent) -> float: return max(0.0, min(1.0, float(intent.confidence)))