986 lines
33 KiB
Python
986 lines
33 KiB
Python
"""Strategy Executor for end-to-end orchestration.
|
|
|
|
This module provides complete strategy execution including:
|
|
- Signal generation to order conversion
|
|
- Order submission and execution
|
|
- Position and portfolio management
|
|
- Error handling with retries
|
|
- Comprehensive logging and monitoring
|
|
|
|
Issue #37: [STRAT-36] Strategy executor - end-to-end orchestration
|
|
|
|
Design Principles:
|
|
- Full trade lifecycle management
|
|
- Robust error handling with retries
|
|
- Comprehensive logging
|
|
- Event-driven architecture
|
|
"""
|
|
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime, timedelta
|
|
from decimal import Decimal
|
|
from enum import Enum
|
|
from typing import Any, Callable, Dict, List, Optional, Protocol, Tuple
|
|
import asyncio
|
|
import logging
|
|
import time
|
|
import uuid
|
|
|
|
from tradingagents.strategy.signal_to_order import (
|
|
SignalToOrderConverter,
|
|
TradingSignal,
|
|
SignalType,
|
|
ConversionConfig,
|
|
ConversionResult,
|
|
)
|
|
from tradingagents.execution.broker_base import (
|
|
OrderRequest,
|
|
OrderSide,
|
|
Order,
|
|
OrderStatus,
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# Logging Setup
|
|
# ============================================================================
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# ============================================================================
|
|
# Enums
|
|
# ============================================================================
|
|
|
|
class ExecutionStatus(str, Enum):
|
|
"""Status of strategy execution."""
|
|
PENDING = "pending" # Not started
|
|
CONVERTING = "converting" # Converting signals to orders
|
|
SUBMITTING = "submitting" # Submitting orders
|
|
EXECUTING = "executing" # Orders executing
|
|
MONITORING = "monitoring" # Monitoring positions
|
|
COMPLETED = "completed" # Execution complete
|
|
FAILED = "failed" # Execution failed
|
|
CANCELLED = "cancelled" # Execution cancelled
|
|
|
|
|
|
class RetryPolicy(str, Enum):
|
|
"""Retry policy for failed operations."""
|
|
NONE = "none" # No retries
|
|
IMMEDIATE = "immediate" # Retry immediately
|
|
EXPONENTIAL = "exponential" # Exponential backoff
|
|
FIXED = "fixed" # Fixed delay
|
|
|
|
|
|
class ExecutionEvent(str, Enum):
|
|
"""Events during execution."""
|
|
STARTED = "started"
|
|
SIGNAL_RECEIVED = "signal_received"
|
|
ORDER_CREATED = "order_created"
|
|
ORDER_SUBMITTED = "order_submitted"
|
|
ORDER_FILLED = "order_filled"
|
|
ORDER_REJECTED = "order_rejected"
|
|
ORDER_CANCELLED = "order_cancelled"
|
|
POSITION_OPENED = "position_opened"
|
|
POSITION_CLOSED = "position_closed"
|
|
STOP_TRIGGERED = "stop_triggered"
|
|
TARGET_REACHED = "target_reached"
|
|
ERROR = "error"
|
|
RETRY = "retry"
|
|
COMPLETED = "completed"
|
|
FAILED = "failed"
|
|
|
|
|
|
# ============================================================================
|
|
# Protocols
|
|
# ============================================================================
|
|
|
|
class SignalProvider(Protocol):
|
|
"""Protocol for signal generation."""
|
|
|
|
def get_signals(self, symbols: List[str]) -> List[TradingSignal]:
|
|
"""Generate trading signals for symbols.
|
|
|
|
Args:
|
|
symbols: List of symbols to analyze
|
|
|
|
Returns:
|
|
List of trading signals
|
|
"""
|
|
...
|
|
|
|
|
|
class OrderExecutor(Protocol):
|
|
"""Protocol for order execution."""
|
|
|
|
async def submit_order(self, order: OrderRequest) -> Order:
|
|
"""Submit an order for execution.
|
|
|
|
Args:
|
|
order: Order request to submit
|
|
|
|
Returns:
|
|
Submitted order with ID
|
|
"""
|
|
...
|
|
|
|
async def cancel_order(self, order_id: str) -> bool:
|
|
"""Cancel an order.
|
|
|
|
Args:
|
|
order_id: ID of order to cancel
|
|
|
|
Returns:
|
|
True if cancelled successfully
|
|
"""
|
|
...
|
|
|
|
async def get_order_status(self, order_id: str) -> Order:
|
|
"""Get current order status.
|
|
|
|
Args:
|
|
order_id: ID of order to check
|
|
|
|
Returns:
|
|
Order with current status
|
|
"""
|
|
...
|
|
|
|
|
|
class PositionManager(Protocol):
|
|
"""Protocol for position management."""
|
|
|
|
def get_position(self, symbol: str) -> Optional[Dict[str, Any]]:
|
|
"""Get current position for symbol.
|
|
|
|
Args:
|
|
symbol: Trading symbol
|
|
|
|
Returns:
|
|
Position data or None
|
|
"""
|
|
...
|
|
|
|
def update_position(
|
|
self,
|
|
symbol: str,
|
|
quantity: Decimal,
|
|
avg_price: Decimal,
|
|
) -> None:
|
|
"""Update position after fill.
|
|
|
|
Args:
|
|
symbol: Trading symbol
|
|
quantity: Filled quantity
|
|
avg_price: Average fill price
|
|
"""
|
|
...
|
|
|
|
|
|
# ============================================================================
|
|
# Data Classes
|
|
# ============================================================================
|
|
|
|
@dataclass
|
|
class RetryConfig:
|
|
"""Configuration for retry behavior.
|
|
|
|
Attributes:
|
|
policy: Retry policy to use
|
|
max_retries: Maximum retry attempts
|
|
initial_delay_ms: Initial delay in milliseconds
|
|
max_delay_ms: Maximum delay in milliseconds
|
|
backoff_multiplier: Multiplier for exponential backoff
|
|
"""
|
|
policy: RetryPolicy = RetryPolicy.EXPONENTIAL
|
|
max_retries: int = 3
|
|
initial_delay_ms: int = 100
|
|
max_delay_ms: int = 5000
|
|
backoff_multiplier: float = 2.0
|
|
|
|
|
|
@dataclass
|
|
class MonitoringConfig:
|
|
"""Configuration for execution monitoring.
|
|
|
|
Attributes:
|
|
log_all_events: Log all execution events
|
|
log_level: Default logging level
|
|
track_latency: Track order latency
|
|
track_fills: Track fill quality
|
|
alert_on_failure: Alert on execution failure
|
|
metrics_interval_seconds: Interval for metrics collection
|
|
"""
|
|
log_all_events: bool = True
|
|
log_level: str = "INFO"
|
|
track_latency: bool = True
|
|
track_fills: bool = True
|
|
alert_on_failure: bool = True
|
|
metrics_interval_seconds: int = 60
|
|
|
|
|
|
@dataclass
|
|
class ExecutorConfig:
|
|
"""Configuration for strategy executor.
|
|
|
|
Attributes:
|
|
conversion_config: Signal to order conversion config
|
|
retry_config: Retry behavior config
|
|
monitoring_config: Monitoring and logging config
|
|
max_concurrent_orders: Maximum concurrent orders
|
|
order_timeout_seconds: Order fill timeout
|
|
enable_stop_orders: Submit stop loss orders
|
|
enable_take_profit: Submit take profit orders
|
|
dry_run: Simulate without actual execution
|
|
"""
|
|
conversion_config: ConversionConfig = field(default_factory=ConversionConfig)
|
|
retry_config: RetryConfig = field(default_factory=RetryConfig)
|
|
monitoring_config: MonitoringConfig = field(default_factory=MonitoringConfig)
|
|
max_concurrent_orders: int = 10
|
|
order_timeout_seconds: int = 300
|
|
enable_stop_orders: bool = True
|
|
enable_take_profit: bool = True
|
|
dry_run: bool = False
|
|
|
|
|
|
@dataclass
|
|
class ExecutionEvent_:
|
|
"""An event during strategy execution.
|
|
|
|
Attributes:
|
|
event_id: Unique event identifier
|
|
event_type: Type of event
|
|
timestamp: When event occurred
|
|
signal_id: Associated signal ID
|
|
order_id: Associated order ID
|
|
symbol: Trading symbol
|
|
message: Event message
|
|
data: Additional event data
|
|
"""
|
|
event_id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
event_type: ExecutionEvent = ExecutionEvent.STARTED
|
|
timestamp: datetime = field(default_factory=datetime.now)
|
|
signal_id: Optional[str] = None
|
|
order_id: Optional[str] = None
|
|
symbol: str = ""
|
|
message: str = ""
|
|
data: Dict[str, Any] = field(default_factory=dict)
|
|
|
|
|
|
@dataclass
|
|
class OrderExecution:
|
|
"""Record of a single order execution.
|
|
|
|
Attributes:
|
|
execution_id: Unique execution identifier
|
|
signal: Original signal
|
|
conversion_result: Signal conversion result
|
|
order_request: Generated order request
|
|
submitted_order: Order after submission
|
|
final_status: Final order status
|
|
fill_price: Average fill price
|
|
fill_quantity: Filled quantity
|
|
submit_time: When order was submitted
|
|
fill_time: When order was filled
|
|
latency_ms: Total latency in milliseconds
|
|
retries: Number of retries used
|
|
error_message: Error if failed
|
|
"""
|
|
execution_id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
signal: Optional[TradingSignal] = None
|
|
conversion_result: Optional[ConversionResult] = None
|
|
order_request: Optional[OrderRequest] = None
|
|
submitted_order: Optional[Order] = None
|
|
final_status: OrderStatus = OrderStatus.PENDING_NEW
|
|
fill_price: Optional[Decimal] = None
|
|
fill_quantity: Optional[Decimal] = None
|
|
submit_time: Optional[datetime] = None
|
|
fill_time: Optional[datetime] = None
|
|
latency_ms: Optional[int] = None
|
|
retries: int = 0
|
|
error_message: str = ""
|
|
|
|
|
|
@dataclass
|
|
class ExecutionResult:
|
|
"""Result of strategy execution.
|
|
|
|
Attributes:
|
|
result_id: Unique result identifier
|
|
status: Final execution status
|
|
start_time: Execution start time
|
|
end_time: Execution end time
|
|
total_signals: Number of signals processed
|
|
orders_submitted: Number of orders submitted
|
|
orders_filled: Number of orders filled
|
|
orders_rejected: Number of orders rejected
|
|
orders_cancelled: Number of orders cancelled
|
|
total_value: Total value executed
|
|
executions: Individual order executions
|
|
events: Execution events log
|
|
errors: Error messages
|
|
metrics: Execution metrics
|
|
"""
|
|
result_id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
status: ExecutionStatus = ExecutionStatus.PENDING
|
|
start_time: datetime = field(default_factory=datetime.now)
|
|
end_time: Optional[datetime] = None
|
|
total_signals: int = 0
|
|
orders_submitted: int = 0
|
|
orders_filled: int = 0
|
|
orders_rejected: int = 0
|
|
orders_cancelled: int = 0
|
|
total_value: Decimal = Decimal("0")
|
|
executions: List[OrderExecution] = field(default_factory=list)
|
|
events: List[ExecutionEvent_] = field(default_factory=list)
|
|
errors: List[str] = field(default_factory=list)
|
|
metrics: Dict[str, Any] = field(default_factory=dict)
|
|
|
|
|
|
# ============================================================================
|
|
# StrategyExecutor Class
|
|
# ============================================================================
|
|
|
|
class StrategyExecutor:
|
|
"""Orchestrates end-to-end strategy execution.
|
|
|
|
Handles the complete trade lifecycle:
|
|
1. Receive signals from signal provider
|
|
2. Convert signals to orders
|
|
3. Submit orders for execution
|
|
4. Monitor order status
|
|
5. Manage positions
|
|
6. Handle errors and retries
|
|
|
|
Attributes:
|
|
config: Executor configuration
|
|
signal_converter: Signal to order converter
|
|
order_executor: Order execution handler
|
|
position_manager: Position management
|
|
event_handlers: Event callback handlers
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
config: Optional[ExecutorConfig] = None,
|
|
order_executor: Optional[OrderExecutor] = None,
|
|
position_manager: Optional[PositionManager] = None,
|
|
portfolio_value: Decimal = Decimal("100000"),
|
|
current_prices: Optional[Dict[str, Decimal]] = None,
|
|
):
|
|
"""Initialize strategy executor.
|
|
|
|
Args:
|
|
config: Executor configuration
|
|
order_executor: Order execution handler
|
|
position_manager: Position management handler
|
|
portfolio_value: Current portfolio value
|
|
current_prices: Current market prices
|
|
"""
|
|
self.config = config or ExecutorConfig()
|
|
self.order_executor = order_executor
|
|
self.position_manager = position_manager
|
|
|
|
# Initialize signal converter
|
|
self.signal_converter = SignalToOrderConverter(
|
|
config=self.config.conversion_config,
|
|
portfolio_value=portfolio_value,
|
|
current_prices=current_prices or {},
|
|
)
|
|
|
|
# Event handlers
|
|
self.event_handlers: Dict[ExecutionEvent, List[Callable]] = {
|
|
event: [] for event in ExecutionEvent
|
|
}
|
|
|
|
# Execution state
|
|
self._current_result: Optional[ExecutionResult] = None
|
|
self._pending_orders: Dict[str, OrderExecution] = {}
|
|
self._is_running = False
|
|
|
|
def register_event_handler(
|
|
self,
|
|
event_type: ExecutionEvent,
|
|
handler: Callable[[ExecutionEvent_], None],
|
|
) -> None:
|
|
"""Register a handler for execution events.
|
|
|
|
Args:
|
|
event_type: Type of event to handle
|
|
handler: Callback function
|
|
"""
|
|
self.event_handlers[event_type].append(handler)
|
|
|
|
def _emit_event(
|
|
self,
|
|
event_type: ExecutionEvent,
|
|
signal_id: Optional[str] = None,
|
|
order_id: Optional[str] = None,
|
|
symbol: str = "",
|
|
message: str = "",
|
|
data: Optional[Dict[str, Any]] = None,
|
|
) -> ExecutionEvent_:
|
|
"""Emit an execution event.
|
|
|
|
Args:
|
|
event_type: Type of event
|
|
signal_id: Associated signal ID
|
|
order_id: Associated order ID
|
|
symbol: Trading symbol
|
|
message: Event message
|
|
data: Additional data
|
|
|
|
Returns:
|
|
The emitted event
|
|
"""
|
|
event = ExecutionEvent_(
|
|
event_type=event_type,
|
|
signal_id=signal_id,
|
|
order_id=order_id,
|
|
symbol=symbol,
|
|
message=message,
|
|
data=data or {},
|
|
)
|
|
|
|
# Log event
|
|
if self.config.monitoring_config.log_all_events:
|
|
logger.log(
|
|
getattr(logging, self.config.monitoring_config.log_level),
|
|
f"[{event_type.value}] {symbol}: {message}",
|
|
)
|
|
|
|
# Record event
|
|
if self._current_result:
|
|
self._current_result.events.append(event)
|
|
|
|
# Call handlers
|
|
for handler in self.event_handlers[event_type]:
|
|
try:
|
|
handler(event)
|
|
except Exception as e:
|
|
logger.error(f"Event handler error: {e}")
|
|
|
|
return event
|
|
|
|
def execute_signals(
|
|
self,
|
|
signals: List[TradingSignal],
|
|
) -> ExecutionResult:
|
|
"""Execute a list of trading signals synchronously.
|
|
|
|
Args:
|
|
signals: List of signals to execute
|
|
|
|
Returns:
|
|
ExecutionResult with all execution details
|
|
"""
|
|
# Initialize result
|
|
result = ExecutionResult(
|
|
status=ExecutionStatus.PENDING,
|
|
start_time=datetime.now(),
|
|
total_signals=len(signals),
|
|
)
|
|
self._current_result = result
|
|
|
|
self._emit_event(
|
|
ExecutionEvent.STARTED,
|
|
message=f"Starting execution of {len(signals)} signals",
|
|
)
|
|
|
|
try:
|
|
result.status = ExecutionStatus.CONVERTING
|
|
|
|
# Convert signals to orders
|
|
for signal in signals:
|
|
if signal.signal_type == SignalType.HOLD:
|
|
continue
|
|
|
|
self._emit_event(
|
|
ExecutionEvent.SIGNAL_RECEIVED,
|
|
signal_id=signal.signal_id,
|
|
symbol=signal.symbol,
|
|
message=f"Received {signal.signal_type.value} signal",
|
|
)
|
|
|
|
execution = self._process_signal(signal)
|
|
result.executions.append(execution)
|
|
|
|
if execution.order_request:
|
|
result.orders_submitted += 1
|
|
self._emit_event(
|
|
ExecutionEvent.ORDER_CREATED,
|
|
signal_id=signal.signal_id,
|
|
order_id=execution.order_request.client_order_id,
|
|
symbol=signal.symbol,
|
|
message="Order created from signal",
|
|
)
|
|
|
|
# In dry run mode, mark all as filled
|
|
if self.config.dry_run:
|
|
for execution in result.executions:
|
|
if execution.order_request:
|
|
execution.final_status = OrderStatus.FILLED
|
|
execution.fill_price = (
|
|
execution.order_request.limit_price or
|
|
self.signal_converter.current_prices.get(
|
|
execution.order_request.symbol,
|
|
Decimal("0")
|
|
)
|
|
)
|
|
execution.fill_quantity = execution.order_request.quantity
|
|
result.orders_filled += 1
|
|
result.total_value += (
|
|
execution.fill_price * execution.fill_quantity
|
|
)
|
|
|
|
# Update final counts
|
|
for execution in result.executions:
|
|
if execution.final_status == OrderStatus.REJECTED:
|
|
result.orders_rejected += 1
|
|
elif execution.final_status == OrderStatus.CANCELLED:
|
|
result.orders_cancelled += 1
|
|
|
|
result.status = ExecutionStatus.COMPLETED
|
|
result.end_time = datetime.now()
|
|
|
|
self._emit_event(
|
|
ExecutionEvent.COMPLETED,
|
|
message=f"Execution complete: {result.orders_filled} filled, "
|
|
f"{result.orders_rejected} rejected",
|
|
)
|
|
|
|
except Exception as e:
|
|
result.status = ExecutionStatus.FAILED
|
|
result.end_time = datetime.now()
|
|
result.errors.append(str(e))
|
|
|
|
self._emit_event(
|
|
ExecutionEvent.FAILED,
|
|
message=f"Execution failed: {str(e)}",
|
|
)
|
|
|
|
# Calculate metrics
|
|
result.metrics = self._calculate_metrics(result)
|
|
self._current_result = None
|
|
|
|
return result
|
|
|
|
async def execute_signals_async(
|
|
self,
|
|
signals: List[TradingSignal],
|
|
) -> ExecutionResult:
|
|
"""Execute signals asynchronously with real order submission.
|
|
|
|
Args:
|
|
signals: List of signals to execute
|
|
|
|
Returns:
|
|
ExecutionResult with all execution details
|
|
"""
|
|
if self.order_executor is None:
|
|
raise ValueError("Order executor required for async execution")
|
|
|
|
result = ExecutionResult(
|
|
status=ExecutionStatus.PENDING,
|
|
start_time=datetime.now(),
|
|
total_signals=len(signals),
|
|
)
|
|
self._current_result = result
|
|
self._is_running = True
|
|
|
|
self._emit_event(
|
|
ExecutionEvent.STARTED,
|
|
message=f"Starting async execution of {len(signals)} signals",
|
|
)
|
|
|
|
try:
|
|
result.status = ExecutionStatus.CONVERTING
|
|
|
|
# Convert and submit orders concurrently
|
|
tasks = []
|
|
for signal in signals:
|
|
if signal.signal_type == SignalType.HOLD:
|
|
continue
|
|
|
|
self._emit_event(
|
|
ExecutionEvent.SIGNAL_RECEIVED,
|
|
signal_id=signal.signal_id,
|
|
symbol=signal.symbol,
|
|
message=f"Received {signal.signal_type.value} signal",
|
|
)
|
|
|
|
execution = self._process_signal(signal)
|
|
result.executions.append(execution)
|
|
|
|
if execution.order_request:
|
|
# Submit order asynchronously
|
|
task = self._submit_order_with_retry(execution)
|
|
tasks.append(task)
|
|
|
|
result.status = ExecutionStatus.SUBMITTING
|
|
|
|
# Wait for all orders to be submitted
|
|
if tasks:
|
|
submitted = await asyncio.gather(*tasks, return_exceptions=True)
|
|
|
|
for i, sub_result in enumerate(submitted):
|
|
if isinstance(sub_result, Exception):
|
|
result.errors.append(str(sub_result))
|
|
else:
|
|
result.orders_submitted += 1
|
|
|
|
result.status = ExecutionStatus.MONITORING
|
|
|
|
# Monitor orders until filled or timeout
|
|
await self._monitor_orders(result)
|
|
|
|
# Update final counts
|
|
for execution in result.executions:
|
|
if execution.final_status == OrderStatus.FILLED:
|
|
result.orders_filled += 1
|
|
if execution.fill_price and execution.fill_quantity:
|
|
result.total_value += (
|
|
execution.fill_price * execution.fill_quantity
|
|
)
|
|
elif execution.final_status == OrderStatus.REJECTED:
|
|
result.orders_rejected += 1
|
|
elif execution.final_status == OrderStatus.CANCELLED:
|
|
result.orders_cancelled += 1
|
|
|
|
result.status = ExecutionStatus.COMPLETED
|
|
result.end_time = datetime.now()
|
|
|
|
self._emit_event(
|
|
ExecutionEvent.COMPLETED,
|
|
message=f"Execution complete: {result.orders_filled} filled",
|
|
)
|
|
|
|
except Exception as e:
|
|
result.status = ExecutionStatus.FAILED
|
|
result.end_time = datetime.now()
|
|
result.errors.append(str(e))
|
|
|
|
self._emit_event(
|
|
ExecutionEvent.FAILED,
|
|
message=f"Execution failed: {str(e)}",
|
|
)
|
|
|
|
finally:
|
|
self._is_running = False
|
|
|
|
result.metrics = self._calculate_metrics(result)
|
|
self._current_result = None
|
|
|
|
return result
|
|
|
|
def _process_signal(self, signal: TradingSignal) -> OrderExecution:
|
|
"""Process a single signal into an order execution.
|
|
|
|
Args:
|
|
signal: Trading signal to process
|
|
|
|
Returns:
|
|
OrderExecution record
|
|
"""
|
|
execution = OrderExecution(signal=signal)
|
|
|
|
# Convert signal to order
|
|
conversion = self.signal_converter.convert(signal)
|
|
execution.conversion_result = conversion
|
|
|
|
if conversion.success and conversion.order_request:
|
|
execution.order_request = conversion.order_request
|
|
else:
|
|
execution.error_message = conversion.error_message
|
|
self._emit_event(
|
|
ExecutionEvent.ERROR,
|
|
signal_id=signal.signal_id,
|
|
symbol=signal.symbol,
|
|
message=f"Signal conversion failed: {conversion.error_message}",
|
|
)
|
|
|
|
return execution
|
|
|
|
async def _submit_order_with_retry(
|
|
self,
|
|
execution: OrderExecution,
|
|
) -> bool:
|
|
"""Submit order with retry logic.
|
|
|
|
Args:
|
|
execution: Order execution record
|
|
|
|
Returns:
|
|
True if submitted successfully
|
|
"""
|
|
retry_config = self.config.retry_config
|
|
delay_ms = retry_config.initial_delay_ms
|
|
|
|
for attempt in range(retry_config.max_retries + 1):
|
|
try:
|
|
execution.submit_time = datetime.now()
|
|
|
|
if self.order_executor:
|
|
order = await self.order_executor.submit_order(
|
|
execution.order_request
|
|
)
|
|
execution.submitted_order = order
|
|
|
|
self._emit_event(
|
|
ExecutionEvent.ORDER_SUBMITTED,
|
|
signal_id=execution.signal.signal_id if execution.signal else None,
|
|
order_id=order.broker_order_id,
|
|
symbol=execution.order_request.symbol,
|
|
message="Order submitted successfully",
|
|
)
|
|
|
|
# Track pending order
|
|
self._pending_orders[order.broker_order_id] = execution
|
|
return True
|
|
|
|
except Exception as e:
|
|
execution.retries = attempt + 1
|
|
error_msg = f"Order submission failed (attempt {attempt + 1}): {e}"
|
|
|
|
if attempt < retry_config.max_retries:
|
|
self._emit_event(
|
|
ExecutionEvent.RETRY,
|
|
signal_id=execution.signal.signal_id if execution.signal else None,
|
|
symbol=execution.order_request.symbol if execution.order_request else "",
|
|
message=error_msg,
|
|
)
|
|
|
|
# Apply delay
|
|
if retry_config.policy == RetryPolicy.EXPONENTIAL:
|
|
await asyncio.sleep(delay_ms / 1000)
|
|
delay_ms = min(
|
|
delay_ms * retry_config.backoff_multiplier,
|
|
retry_config.max_delay_ms,
|
|
)
|
|
elif retry_config.policy == RetryPolicy.FIXED:
|
|
await asyncio.sleep(retry_config.initial_delay_ms / 1000)
|
|
else:
|
|
execution.error_message = str(e)
|
|
self._emit_event(
|
|
ExecutionEvent.ERROR,
|
|
signal_id=execution.signal.signal_id if execution.signal else None,
|
|
symbol=execution.order_request.symbol if execution.order_request else "",
|
|
message=f"Order submission failed after {attempt + 1} attempts: {e}",
|
|
)
|
|
return False
|
|
|
|
return False
|
|
|
|
async def _monitor_orders(self, result: ExecutionResult) -> None:
|
|
"""Monitor pending orders until completion.
|
|
|
|
Args:
|
|
result: Execution result to update
|
|
"""
|
|
timeout = self.config.order_timeout_seconds
|
|
start_time = time.time()
|
|
|
|
while self._pending_orders and self._is_running:
|
|
if time.time() - start_time > timeout:
|
|
# Timeout - cancel remaining orders
|
|
for order_id, execution in list(self._pending_orders.items()):
|
|
execution.final_status = OrderStatus.CANCELLED
|
|
execution.error_message = "Order timeout"
|
|
|
|
if self.order_executor:
|
|
try:
|
|
await self.order_executor.cancel_order(order_id)
|
|
except Exception:
|
|
pass
|
|
|
|
self._emit_event(
|
|
ExecutionEvent.ORDER_CANCELLED,
|
|
order_id=order_id,
|
|
message="Order cancelled due to timeout",
|
|
)
|
|
|
|
self._pending_orders.clear()
|
|
break
|
|
|
|
# Check each pending order
|
|
for order_id, execution in list(self._pending_orders.items()):
|
|
if self.order_executor:
|
|
try:
|
|
order = await self.order_executor.get_order_status(order_id)
|
|
execution.submitted_order = order
|
|
|
|
if order.status == OrderStatus.FILLED:
|
|
execution.final_status = OrderStatus.FILLED
|
|
execution.fill_price = order.filled_avg_price
|
|
execution.fill_quantity = order.filled_quantity
|
|
execution.fill_time = datetime.now()
|
|
|
|
if execution.submit_time:
|
|
execution.latency_ms = int(
|
|
(execution.fill_time - execution.submit_time).total_seconds() * 1000
|
|
)
|
|
|
|
self._emit_event(
|
|
ExecutionEvent.ORDER_FILLED,
|
|
order_id=order_id,
|
|
symbol=order.symbol,
|
|
message=f"Order filled at {order.filled_avg_price}",
|
|
)
|
|
|
|
del self._pending_orders[order_id]
|
|
|
|
# Update position
|
|
if self.position_manager:
|
|
self.position_manager.update_position(
|
|
order.symbol,
|
|
order.filled_quantity,
|
|
order.filled_avg_price,
|
|
)
|
|
|
|
elif order.status == OrderStatus.REJECTED:
|
|
execution.final_status = OrderStatus.REJECTED
|
|
|
|
self._emit_event(
|
|
ExecutionEvent.ORDER_REJECTED,
|
|
order_id=order_id,
|
|
symbol=order.symbol,
|
|
message="Order rejected",
|
|
)
|
|
|
|
del self._pending_orders[order_id]
|
|
|
|
elif order.status == OrderStatus.CANCELLED:
|
|
execution.final_status = OrderStatus.CANCELLED
|
|
|
|
self._emit_event(
|
|
ExecutionEvent.ORDER_CANCELLED,
|
|
order_id=order_id,
|
|
symbol=order.symbol,
|
|
message="Order cancelled",
|
|
)
|
|
|
|
del self._pending_orders[order_id]
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error checking order {order_id}: {e}")
|
|
|
|
# Small delay between checks
|
|
await asyncio.sleep(0.1)
|
|
|
|
def _calculate_metrics(self, result: ExecutionResult) -> Dict[str, Any]:
|
|
"""Calculate execution metrics.
|
|
|
|
Args:
|
|
result: Execution result
|
|
|
|
Returns:
|
|
Dict of metrics
|
|
"""
|
|
metrics = {
|
|
"total_signals": result.total_signals,
|
|
"orders_submitted": result.orders_submitted,
|
|
"orders_filled": result.orders_filled,
|
|
"fill_rate": (
|
|
result.orders_filled / result.orders_submitted
|
|
if result.orders_submitted > 0 else 0
|
|
),
|
|
"orders_rejected": result.orders_rejected,
|
|
"orders_cancelled": result.orders_cancelled,
|
|
"total_value": str(result.total_value),
|
|
"total_errors": len(result.errors),
|
|
}
|
|
|
|
# Calculate latency metrics
|
|
latencies = [
|
|
e.latency_ms for e in result.executions
|
|
if e.latency_ms is not None
|
|
]
|
|
if latencies:
|
|
metrics["avg_latency_ms"] = sum(latencies) / len(latencies)
|
|
metrics["min_latency_ms"] = min(latencies)
|
|
metrics["max_latency_ms"] = max(latencies)
|
|
|
|
# Calculate retry metrics
|
|
total_retries = sum(e.retries for e in result.executions)
|
|
metrics["total_retries"] = total_retries
|
|
|
|
# Calculate duration
|
|
if result.end_time:
|
|
duration = (result.end_time - result.start_time).total_seconds()
|
|
metrics["duration_seconds"] = duration
|
|
|
|
return metrics
|
|
|
|
def cancel_execution(self) -> None:
|
|
"""Cancel ongoing execution."""
|
|
self._is_running = False
|
|
|
|
if self._current_result:
|
|
self._current_result.status = ExecutionStatus.CANCELLED
|
|
|
|
self._emit_event(
|
|
ExecutionEvent.FAILED,
|
|
message="Execution cancelled by user",
|
|
)
|
|
|
|
def update_prices(self, prices: Dict[str, Decimal]) -> None:
|
|
"""Update current market prices.
|
|
|
|
Args:
|
|
prices: Dict of symbol to price
|
|
"""
|
|
for symbol, price in prices.items():
|
|
self.signal_converter.update_price(symbol, price)
|
|
|
|
def update_portfolio_value(self, value: Decimal) -> None:
|
|
"""Update portfolio value.
|
|
|
|
Args:
|
|
value: New portfolio value
|
|
"""
|
|
self.signal_converter.update_portfolio_value(value)
|
|
|
|
def get_execution_summary(self, result: ExecutionResult) -> str:
|
|
"""Generate a summary report of execution.
|
|
|
|
Args:
|
|
result: Execution result
|
|
|
|
Returns:
|
|
Formatted summary string
|
|
"""
|
|
lines = [
|
|
"# Execution Summary",
|
|
f"**Status**: {result.status.value}",
|
|
f"**Start**: {result.start_time}",
|
|
f"**End**: {result.end_time}",
|
|
"",
|
|
"## Order Statistics",
|
|
f"- Signals processed: {result.total_signals}",
|
|
f"- Orders submitted: {result.orders_submitted}",
|
|
f"- Orders filled: {result.orders_filled}",
|
|
f"- Orders rejected: {result.orders_rejected}",
|
|
f"- Orders cancelled: {result.orders_cancelled}",
|
|
f"- Total value: ${result.total_value:,.2f}",
|
|
"",
|
|
]
|
|
|
|
if result.metrics:
|
|
lines.extend([
|
|
"## Metrics",
|
|
f"- Fill rate: {result.metrics.get('fill_rate', 0):.1%}",
|
|
f"- Avg latency: {result.metrics.get('avg_latency_ms', 0):.0f}ms",
|
|
f"- Total retries: {result.metrics.get('total_retries', 0)}",
|
|
f"- Duration: {result.metrics.get('duration_seconds', 0):.1f}s",
|
|
"",
|
|
])
|
|
|
|
if result.errors:
|
|
lines.extend([
|
|
"## Errors",
|
|
])
|
|
for error in result.errors[:5]:
|
|
lines.append(f"- {error}")
|
|
|
|
return "\n".join(lines)
|