deploy: 2026-03-20 07:49
This commit is contained in:
234
execution/order_manager.py
Normal file
234
execution/order_manager.py
Normal file
@@ -0,0 +1,234 @@
|
||||
"""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]
|
||||
Reference in New Issue
Block a user