Files
crypto_news/execution/order_manager.py

235 lines
7.7 KiB
Python
Raw Normal View History

2026-03-20 07:49:42 +09:00
"""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]