|
|
|
|
|
""" |
|
|
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: |
|
|
|
|
|
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") |
|
|
|
|
|
|
|
|
order_id = f"ORD-{uuid.uuid4().hex[:12].upper()}" |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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: |
|
|
|
|
|
|
|
|
|
|
|
order.status = OrderStatus.FILLED |
|
|
order.filled_quantity = order.quantity |
|
|
|
|
|
order.average_fill_price = order.price or 50000.0 |
|
|
order.executed_at = datetime.utcnow() |
|
|
|
|
|
|
|
|
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: |
|
|
|
|
|
position = self.db.query(FuturesPosition).filter( |
|
|
and_( |
|
|
FuturesPosition.symbol == order.symbol, |
|
|
FuturesPosition.is_open == True |
|
|
) |
|
|
).first() |
|
|
|
|
|
if position: |
|
|
|
|
|
if position.side == order.side: |
|
|
|
|
|
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: |
|
|
|
|
|
if order.filled_quantity >= position.quantity: |
|
|
|
|
|
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: |
|
|
|
|
|
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: |
|
|
|
|
|
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 |
|
|
} |
|
|
|
|
|
|