Building a Crypto Trading Bot from Scratch - 11: Building a Trade Executor
From Signal to Order
In the last article, we built a Risk Manager that validates signals and calculates trade parameters. But validation doesn’t execute trades—it just tells you if and how to trade.
Now comes the moment of truth: actually placing orders on the exchange. This is where signals become real trades, and where things can go wrong in fascinating ways.
The Trade Executor is the bridge between your bot’s decisions and the market. It handles order placement, retries, failures, partial fills, and all the messy reality of actually interacting with exchange APIs.
What Can Go Wrong?
Before diving into the implementation, let me share what I learned the hard way about order execution:
Network failures: API request times out mid-flight. Was the order placed or not?
Insufficient balance: You think you have $1,000 available, but fees or another order consumed it.
Order rejected: Price moved while you were calculating. Your limit order is now too far from the market.
Partial fills: You wanted 0.1 BTC but only got 0.06. Now what?
Exchange errors: “System overloaded, try again later.” How long do you wait?
Rate limiting: Make too many requests, get temporarily banned.
The Trade Executor’s job is to handle all of this gracefully, with retries, logging, and clear error states.
The Trade Executor Architecture
Here’s the structure:
class TradeExecutor:
"""
Trade Executor handles order placement and management.
Responsibilities:
- Execute validated trading signals
- Place orders via exchange adapter
- Handle order lifecycle (pending → filled/cancelled)
- Support market and limit orders
- Retry logic with exponential backoff
- Order reconciliation
- Track pending orders
"""
def __init__(
self,
exchange: ExchangeBase,
data_store: DataStore,
execution_config: Dict[str, Any],
logger=None
):
self.exchange = exchange
self.data_store = data_store
self.logger = logger or logging.getLogger(self.__class__.__name__)
# Load config
self.order_type = OrderType(execution_config.get('order_type', 'LIMIT'))
self.slippage_tolerance_pct = execution_config.get('slippage_tolerance_pct', 0.1)
self.order_timeout_seconds = execution_config.get('order_timeout_seconds', 30)
self.max_retries = execution_config.get('max_retries', 3)
self.retry_delay_seconds = execution_config.get('retry_delay_seconds', 1)
# Track pending orders
self.pending_orders: Dict[str, Trade] = {}
Key design decisions:
Market vs Limit Orders: Configurable. Market orders fill immediately but can have slippage. Limit orders control price but might not fill.
Retry Logic: If order placement fails, retry with exponential backoff. Don’t give up after one failure.
Order Tracking: Keep track of pending orders so we can reconcile them later.
Executing a Signal
The main entry point is execute_signal():



