#!/usr/bin/env python3 """ Futures Trading Service ======================== سرویس مدیریت معاملات Futures با قابلیت اجرای دستورات، مدیریت موقعیت‌ها و پیگیری سفارشات """ from typing import Optional, List, Dict, Any from datetime import datetime from sqlalchemy.orm import Session from sqlalchemy import and_ import uuid import logging from database.models import ( Base, FuturesOrder, FuturesPosition, OrderStatus, OrderSide, OrderType ) logger = logging.getLogger(__name__) class FuturesTradingService: """سرویس اصلی مدیریت معاملات Futures""" def __init__(self, db_session: Session): """ Initialize the futures trading service. Args: db_session: SQLAlchemy database session """ self.db = db_session def create_order( self, symbol: str, side: str, order_type: str, quantity: float, price: Optional[float] = None, stop_price: Optional[float] = None, exchange: str = "demo" ) -> Dict[str, Any]: """ Create and execute a futures trading order. Args: symbol: Trading pair (e.g., "BTC/USDT") side: Order side ("buy" or "sell") order_type: Order type ("market", "limit", "stop", "stop_limit") quantity: Order quantity price: Limit price (required for limit orders) stop_price: Stop price (required for stop orders) exchange: Exchange name (default: "demo") Returns: Dict containing order details """ try: # Validate inputs if order_type in ["limit", "stop_limit"] and not price: raise ValueError(f"Price is required for {order_type} orders") if order_type in ["stop", "stop_limit"] and not stop_price: raise ValueError(f"Stop price is required for {order_type} orders") # Generate order ID order_id = f"ORD-{uuid.uuid4().hex[:12].upper()}" # Create order record order = FuturesOrder( order_id=order_id, symbol=symbol.upper(), side=OrderSide.BUY if side.lower() == "buy" else OrderSide.SELL, order_type=OrderType[order_type.upper()], quantity=quantity, price=price, stop_price=stop_price, status=OrderStatus.OPEN if order_type == "market" else OrderStatus.PENDING, exchange=exchange ) self.db.add(order) self.db.commit() self.db.refresh(order) # Execute market orders immediately (in demo mode) if order_type == "market": self._execute_market_order(order) logger.info(f"Created order {order_id} for {symbol} {side} {quantity} @ {price or 'MARKET'}") return self._order_to_dict(order) except Exception as e: self.db.rollback() logger.error(f"Error creating order: {e}", exc_info=True) raise def _execute_market_order(self, order: FuturesOrder) -> None: """ Execute a market order immediately (demo mode). Args: order: The order to execute """ try: # In demo mode, we simulate immediate execution # In production, this would call exchange API order.status = OrderStatus.FILLED order.filled_quantity = order.quantity # Simulate fill price (in production, use actual market price) order.average_fill_price = order.price or 50000.0 # Placeholder order.executed_at = datetime.utcnow() # Create or update position self._update_position_from_order(order) self.db.commit() except Exception as e: logger.error(f"Error executing market order: {e}", exc_info=True) raise def _update_position_from_order(self, order: FuturesOrder) -> None: """ Update position based on filled order. Args: order: The filled order """ try: # Find existing open position position = self.db.query(FuturesPosition).filter( and_( FuturesPosition.symbol == order.symbol, FuturesPosition.is_open == True ) ).first() if position: # Update existing position if position.side == order.side: # Increase position total_value = (position.quantity * position.entry_price) + \ (order.filled_quantity * order.average_fill_price) total_quantity = position.quantity + order.filled_quantity position.entry_price = total_value / total_quantity if total_quantity > 0 else position.entry_price position.quantity = total_quantity else: # Close or reduce position if order.filled_quantity >= position.quantity: # Close position realized_pnl = (order.average_fill_price - position.entry_price) * position.quantity if position.side == OrderSide.SELL: realized_pnl = -realized_pnl position.realized_pnl += realized_pnl position.is_open = False position.closed_at = datetime.utcnow() else: # Reduce position realized_pnl = (order.average_fill_price - position.entry_price) * order.filled_quantity if position.side == OrderSide.SELL: realized_pnl = -realized_pnl position.realized_pnl += realized_pnl position.quantity -= order.filled_quantity else: # Create new position position = FuturesPosition( symbol=order.symbol, side=order.side, quantity=order.filled_quantity, entry_price=order.average_fill_price, current_price=order.average_fill_price, exchange=order.exchange ) self.db.add(position) self.db.commit() except Exception as e: logger.error(f"Error updating position: {e}", exc_info=True) raise def get_positions( self, symbol: Optional[str] = None, is_open: Optional[bool] = True ) -> List[Dict[str, Any]]: """ Retrieve futures positions. Args: symbol: Filter by symbol (optional) is_open: Filter by open status (optional) Returns: List of position dictionaries """ try: query = self.db.query(FuturesPosition) if symbol: query = query.filter(FuturesPosition.symbol == symbol.upper()) if is_open is not None: query = query.filter(FuturesPosition.is_open == is_open) positions = query.order_by(FuturesPosition.opened_at.desc()).all() return [self._position_to_dict(p) for p in positions] except Exception as e: logger.error(f"Error retrieving positions: {e}", exc_info=True) raise def get_orders( self, symbol: Optional[str] = None, status: Optional[str] = None, limit: int = 100 ) -> List[Dict[str, Any]]: """ List all trading orders. Args: symbol: Filter by symbol (optional) status: Filter by status (optional) limit: Maximum number of orders to return Returns: List of order dictionaries """ try: query = self.db.query(FuturesOrder) if symbol: query = query.filter(FuturesOrder.symbol == symbol.upper()) if status: query = query.filter(FuturesOrder.status == OrderStatus[status.upper()]) orders = query.order_by(FuturesOrder.created_at.desc()).limit(limit).all() return [self._order_to_dict(o) for o in orders] except Exception as e: logger.error(f"Error retrieving orders: {e}", exc_info=True) raise def cancel_order(self, order_id: str) -> Dict[str, Any]: """ Cancel a specific order. Args: order_id: The order ID to cancel Returns: Dict containing cancelled order details """ try: order = self.db.query(FuturesOrder).filter( FuturesOrder.order_id == order_id ).first() if not order: raise ValueError(f"Order {order_id} not found") if order.status in [OrderStatus.FILLED, OrderStatus.CANCELLED]: raise ValueError(f"Cannot cancel order with status {order.status.value}") order.status = OrderStatus.CANCELLED order.cancelled_at = datetime.utcnow() self.db.commit() self.db.refresh(order) logger.info(f"Cancelled order {order_id}") return self._order_to_dict(order) except Exception as e: self.db.rollback() logger.error(f"Error cancelling order: {e}", exc_info=True) raise def _order_to_dict(self, order: FuturesOrder) -> Dict[str, Any]: """Convert order model to dictionary.""" return { "id": order.id, "order_id": order.order_id, "symbol": order.symbol, "side": order.side.value if order.side else None, "order_type": order.order_type.value if order.order_type else None, "quantity": order.quantity, "price": order.price, "stop_price": order.stop_price, "status": order.status.value if order.status else None, "filled_quantity": order.filled_quantity, "average_fill_price": order.average_fill_price, "exchange": order.exchange, "created_at": order.created_at.isoformat() if order.created_at else None, "updated_at": order.updated_at.isoformat() if order.updated_at else None, "executed_at": order.executed_at.isoformat() if order.executed_at else None, "cancelled_at": order.cancelled_at.isoformat() if order.cancelled_at else None } def _position_to_dict(self, position: FuturesPosition) -> Dict[str, Any]: """Convert position model to dictionary.""" return { "id": position.id, "symbol": position.symbol, "side": position.side.value if position.side else None, "quantity": position.quantity, "entry_price": position.entry_price, "current_price": position.current_price, "leverage": position.leverage, "unrealized_pnl": position.unrealized_pnl, "realized_pnl": position.realized_pnl, "exchange": position.exchange, "is_open": position.is_open, "opened_at": position.opened_at.isoformat() if position.opened_at else None, "closed_at": position.closed_at.isoformat() if position.closed_at else None, "updated_at": position.updated_at.isoformat() if position.updated_at else None }