Files
siloqy/prod/clean_arch/dita/trade.py

140 lines
6.4 KiB
Python
Raw Normal View History

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