"""Order execution and management. Handles the lifecycle of exchange orders: creation, monitoring, cancellation, and retry logic. """ from __future__ import annotations import asyncio import uuid from dataclasses import dataclass, field from datetime import datetime from enum import Enum from typing import Any, Dict, List, Optional from loguru import logger from execution.exchange_client import ExchangeClient from risk.risk_manager import RiskManager from strategy.signal_generator import TradeSignal from indicators.multi_timeframe import TradeDirection class OrderStatus(str, Enum): PENDING = "PENDING" FILLED = "FILLED" PARTIALLY_FILLED = "PARTIALLY_FILLED" CANCELLED = "CANCELLED" FAILED = "FAILED" @dataclass class Order: """Represents an exchange order.""" id: str symbol: str side: str # "buy" or "sell" order_type: str # "market", "limit", "stop" amount: float price: Optional[float] = None stop_price: Optional[float] = None status: OrderStatus = OrderStatus.PENDING exchange_order_id: Optional[str] = None filled_price: Optional[float] = None filled_amount: float = 0.0 fee: float = 0.0 created_at: datetime = field(default_factory=datetime.utcnow) updated_at: Optional[datetime] = None raw: Dict[str, Any] = field(default_factory=dict) class OrderManager: """Manage order execution against the exchange. Flow for signal execution: 1. Get RiskManager approval 2. Calculate position size 3. Place entry order (limit first, fallback to market) 4. Place SL/TP orders 5. Record results """ MAX_RETRIES = 3 RETRY_DELAY = 1.0 # seconds def __init__( self, exchange_client: ExchangeClient, risk_manager: RiskManager, ): self.client = exchange_client self.risk = risk_manager self._orders: Dict[str, Order] = {} async def execute_signal(self, signal: TradeSignal, balance: float) -> Optional[Order]: """Execute a trade signal end-to-end. Returns the filled entry Order, or None if rejected/failed. """ # 1. Risk approval approval = self.risk.approve_trade( entry_price=signal.entry_price, stop_loss=signal.stop_loss, balance=balance, ) if not approval.approved: logger.warning( "Trade REJECTED for {}: {}", signal.symbol, approval.reason ) return None size = approval.position_size side = "buy" if signal.direction == TradeDirection.LONG else "sell" # 2. Place entry order (try limit, fallback to market) entry_order = await self._place_with_retry( signal.symbol, side, "limit", size, signal.entry_price ) if entry_order is None or entry_order.status == OrderStatus.FAILED: logger.warning("Limit order failed, trying market order") entry_order = await self._place_with_retry( signal.symbol, side, "market", size ) if entry_order is None or entry_order.status == OrderStatus.FAILED: logger.error("Failed to place entry order for {}", signal.symbol) return None # 3. Place SL order sl_side = "sell" if side == "buy" else "buy" await self._place_stop_loss( signal.symbol, sl_side, size, signal.stop_loss ) self.risk.on_position_opened() logger.info( "Order executed: {} {} {} @ {}", side.upper(), signal.symbol, size, entry_order.filled_price or signal.entry_price, ) return entry_order async def create_order( self, symbol: str, side: str, order_type: str, amount: float, price: float | None = None, ) -> Order: """Create a single order on the exchange.""" order_id = str(uuid.uuid4())[:8] order = Order( id=order_id, symbol=symbol, side=side, order_type=order_type, amount=amount, price=price, ) try: if order_type == "market": if side == "buy": raw = await self.client.create_market_buy(symbol, amount) else: raw = await self.client.create_market_sell(symbol, amount) elif order_type == "limit" and price is not None: if side == "buy": raw = await self.client.create_limit_buy(symbol, amount, price) else: raw = await self.client.create_limit_sell(symbol, amount, price) else: raise ValueError(f"Unsupported order type: {order_type}") order.exchange_order_id = raw.get("id") order.status = OrderStatus.FILLED if raw.get("status") == "closed" else OrderStatus.PENDING order.filled_price = raw.get("average") or raw.get("price") order.filled_amount = raw.get("filled", 0.0) order.fee = raw.get("fee", {}).get("cost", 0.0) order.raw = raw except Exception as e: logger.error("Order failed: {} {} {} -- {}", side, symbol, amount, e) order.status = OrderStatus.FAILED order.updated_at = datetime.utcnow() self._orders[order.id] = order return order async def cancel_order(self, order_id: str, symbol: str) -> bool: """Cancel an order on the exchange.""" try: order = self._orders.get(order_id) exchange_id = order.exchange_order_id if order else order_id await self.client.cancel_order(exchange_id, symbol) if order: order.status = OrderStatus.CANCELLED order.updated_at = datetime.utcnow() return True except Exception as e: logger.error("Cancel failed for {}: {}", order_id, e) return False async def _place_with_retry( self, symbol: str, side: str, order_type: str, amount: float, price: float | None = None, ) -> Optional[Order]: """Place an order with automatic retry on failure.""" for attempt in range(1, self.MAX_RETRIES + 1): order = await self.create_order(symbol, side, order_type, amount, price) if order.status != OrderStatus.FAILED: return order logger.warning( "Retry {}/{} for {} {} {}", attempt, self.MAX_RETRIES, side, symbol, order_type ) await asyncio.sleep(self.RETRY_DELAY * attempt) return None async def _place_stop_loss( self, symbol: str, side: str, amount: float, stop_price: float ) -> Optional[Order]: """Place a stop-loss order.""" order_id = str(uuid.uuid4())[:8] order = Order( id=order_id, symbol=symbol, side=side, order_type="stop", amount=amount, stop_price=stop_price, ) try: raw = await self.client.create_stop_loss(symbol, side, amount, stop_price) order.exchange_order_id = raw.get("id") order.status = OrderStatus.PENDING order.raw = raw except Exception as e: logger.error("Stop-loss order failed: {}", e) order.status = OrderStatus.FAILED self._orders[order.id] = order return order def get_order(self, order_id: str) -> Optional[Order]: return self._orders.get(order_id) def get_open_orders(self) -> List[Order]: return [o for o in self._orders.values() if o.status == OrderStatus.PENDING]