How the Nautilus Trader ExecutionEngine Handles Order Submission, Fill Processing, and Venue Connectivity
The ExecutionEngine orchestrates order routing, validates venue matching, reconciles incoming fill reports, and manages execution client connectivity through a centralized Rust-based pipeline that ensures deterministic state transitions across multiple trading venues.
The ExecutionEngine serves as the central nervous system for order execution in the Nautilus Trader open-source trading platform. Written in Rust and located in the nautechsystems/nautilus_trader repository, this component manages the complete lifecycle of orders—from initial submission through fill reconciliation—while maintaining robust connectivity to diverse trading venues. Understanding how the ExecutionEngine handles order submission, fill processing, and venue connectivity is essential for developers building reliable algorithmic trading systems.
Order Submission Routing and Validation
When a TradingCommand arrives on the message bus, the ExecutionEngine first resolves which execution client should receive the command. This routing logic is implemented in crates/execution/src/engine/mod.rs at lines 927-945:
match command {
TradingCommand::SubmitOrder(cmd) => self.handle_submit_order(client, cmd),
TradingCommand::SubmitOrderList(cmd) => self.handle_submit_order_list(client, cmd),
// … other commands
}
The engine selects the client from a three-tier hierarchy:
- Explicit
client_idspecified on the command itself - Venue-based routing map (
self.routing_map) that maps venues to client IDs - Default client as a fallback mechanism
If no client can be resolved, the engine logs an error and drops the command, preventing orders from being sent to invalid destinations.
Processing Single Orders and Order Lists
Single Order Submission
The handle_submit_order method in crates/execution/src/engine/mod.rs (lines 959-1033) implements the complete validation and submission pipeline:
fn handle_submit_order(&self, client: &dyn ExecutionClient, cmd: &SubmitOrder) {
// 1️⃣ Pull the order from the cache
let mut order = match self.cache.borrow().order(&cmd.client_order_id) {
Some(o) => o.clone(),
None => { log::error!("Cannot handle submit order: order not found in cache …"); return; }
};
// 2️⃣ Verify the order’s venue matches the client’s venue
if order.instrument_id().venue != client.venue() {
self.deny_order(&order, &format!("Order venue … does not match client venue …"));
return;
}
// 3️⃣ (Optional) Snapshot the order state
if self.config.snapshot_orders { self.create_order_state_snapshot(&order); }
// 4️⃣ Retrieve the instrument (needed for quote‑quantity conversion)
let instrument = match self.cache.borrow().instrument(&order.instrument_id()) {
Some(i) => i.clone(),
None => { log::error!("Cannot handle submit order: no instrument found …"); return; }
};
// 5️⃣ Quote‑quantity conversion (deprecated flag)
if self.config.convert_quote_qty_to_base && !instrument.is_inverse() && order.is_quote_quantity() {
// … conversion logic
}
// 6️⃣ Own‑order‑book handling (if enabled)
if self.config.manage_own_order_books && should_handle_own_book_order(&order) {
let mut own_book = self.get_or_init_own_order_book(&order.instrument_id());
own_book.add(order.to_own_book_order());
}
// 7️⃣ Forward the command to the client
if let Err(e) = client.submit_order(cmd) {
self.deny_order(&order, &format!("failed‑to‑submit‑order‑to‑client: {e}"));
}
}
Key validation steps include:
- Cache lookup: Validates the order exists in the shared
Cachebefore processing - Venue matching: Ensures
order.instrument_id().venueequalsclient.venue()to prevent misrouting - State snapshots: Optional persistence via
snapshot_ordersconfiguration for audit trails - Quote quantity conversion: Legacy support for converting quote-denominated quantities to base units
- Own order book management: Internal order book tracking when
manage_own_order_booksis enabled
Batch Order List Submission
The handle_submit_order_list method (lines 1035-1115 in the same file) follows an analogous pattern but operates on vectors of orders. It performs batch venue validation, optional snapshots for all orders, and bulk quote-quantity conversion before invoking client.submit_order_list.
Fill Report Reconciliation and Position Management
Receiving Fill Reports
When an ExecutionReport::Fill arrives from a venue, the engine forwards it to the reconciliation logic at lines 647-658 in crates/execution/src/engine/mod.rs:
ExecutionReport::Fill(fill_report) => {
self.reconcile_fill_report(fill_report);
}
Reconciliation Logic
The reconcile_fill_report method (lines 702-735) implements the core reconciliation pipeline:
pub fn reconcile_fill_report(&mut self, report: &FillReport) {
// 1️⃣ Find the associated order (by venue/client order IDs)
// 2️⃣ Validate the fill (duplicate, over‑fill, missing instrument/account, etc.)
// 3️⃣ If valid, generate an OrderFilled event for the order
// (handled by `apply_fill_to_order` → `apply_order_event`)
}
The engine delegates validation to the reconcile_fill helper from nautilus_execution::reconciliation, which respects the allow_overfills configuration flag to determine whether over-fills are tolerated or rejected.
Applying Fills to Orders
The apply_fill_to_order method (lines 660-675) handles the state transition:
fn apply_fill_to_order(&self, order: &mut OrderAny, fill: OrderFilled) -> anyhow::Result<()> {
if order.is_duplicate_fill(&fill) { … }
self.check_overfill(order, &fill)?;
let event = OrderEventAny::Filled(fill);
self.apply_order_event(order, event)
}
Duplicate fills are detected early and ignored with a warning. Over-fills are either logged as warnings (if allow_overfills is true) or rejected with an error via check_overfill (lines 27-40).
Position Updates and OMS Handling
Once the fill event is applied, handle_order_fill (lines 1457-1478) updates portfolio positions:
fn handle_order_fill(&mut self, order: &OrderAny, fill: OrderFilled, oms_type: OmsType) {
// 1️⃣ Ensure instrument & account exist
// 2️⃣ Determine the correct OMS (hedging vs. netting) → `determine_oms_type`
// 3️⃣ Determine (or generate) a PositionId → `determine_position_id`
// 4️⃣ Apply the fill to the position (open, update, flip, close)
// 5️⃣ Publish `OrderFilled` and `Position*` events via the msgbus
}
OMS Determination (lines 723-746):
fn determine_oms_type(&self, fill: &OrderFilled) -> OmsType {
// Strategy‑level override → self.oms_overrides
// Else native client OMS (via routing_map lookup)
// Else default client OMS
// Fallback: Netting
}
Position ID Generation:
- Hedging: Attempts to reuse existing positions for spawned orders or generates new IDs via
self.pos_id_generator.generate - Netting: Deterministic ID derived from
instrument_idandstrategy_id
The resulting events are published on the message bus (msgbus::publish_order_event, msgbus::publish_position_event). If snapshot_positions is enabled, state snapshots are persisted for recovery.
Venue Connectivity and Client Registration
Registering Execution Clients
The engine maintains a registry of execution clients through the register_client method in crates/execution/src/engine/mod.rs (lines 722-735):
pub fn register_client(&mut self, client: Box<dyn ExecutionClient>) -> anyhow::Result<()> {
let client_id = client.client_id();
let venue = client.venue();
// … duplicate‑client check …
let adapter = ExecutionClientAdapter::new(client);
self.routing_map.insert(venue, client_id);
self.clients.insert(client_id, adapter);
Ok(())
}
The routing_map creates a direct link between a venue and its client ID, enabling O(1) lookup during command routing.
Default Client Fallback
For commands that lack explicit venue mappings, the engine supports a default client (lines 943-951):
pub fn register_default_client(&mut self, client: Box<dyn ExecutionClient>) {
let client_id = client.client_id();
self.default_client = Some(ExecutionClientAdapter::new(client));
log::debug!("Registered default client {client_id}");
}
This fallback ensures that order submission commands without specific client assignments still reach an execution venue rather than being dropped.
Connection Status Monitoring
The engine exposes several helper methods to monitor venue health:
| Method | Purpose | Code Location |
|---|---|---|
check_connected |
Returns true only if all registered clients and the default client are connected | Lines 11-19 |
check_disconnected |
Returns true only if all are disconnected | Lines 21-29 |
client_connection_status |
Returns vector of (ClientId, bool) tuples indicating individual client connectivity |
Lines 32-45 |
get_external_client_ids / external_order_claims |
Support external execution streams not handled by the engine (e.g., separate order-matching services) | Lines 55-70 |
These helpers read the is_connected() state from each ExecutionClientAdapter, which delegates to the underlying ExecutionClient implementation. Higher-level components like the Trader or Kernel invoke exec_engine.check_connected() before starting live trading, ensuring no orders are lost or sent to unavailable venues.
Practical Implementation Example
The following pattern demonstrates how to instantiate and configure the ExecutionEngine for live trading:
use nautilus_execution::engine::{ExecutionEngine, ExecutionEngineConfig};
use std::rc::Rc;
use std::cell::RefCell;
use nautilus_common::{clock::Clock, cache::Cache};
// 1️⃣ Build core services
let clock: Rc<RefCell<dyn Clock>> = // ... initialize clock
let cache: Rc<RefCell<Cache>> = // ... initialize cache
// 2️⃣ Create the engine with default configuration
let mut engine = ExecutionEngine::new(clock.clone(), cache.clone(), None);
// 3️⃣ Register a concrete execution client (e.g., a mock exchange)
let client: Box<dyn ExecutionClient> = Box::new(MyMockExchange::new());
engine.register_client(client)?; // Maps venue to client_id
// 4️⃣ (Optional) Register a default fallback client
engine.register_default_client(Box::new(FallbackClient::new()));
// 5️⃣ Verify connectivity before live trading
assert!(engine.check_connected(), "One or more venues are offline");
// 6️⃣ Submit an order (order must already exist in cache)
let submit_cmd = SubmitOrder::new(/* ... */);
engine.execute(TradingCommand::SubmitOrder(submit_cmd));
This initialization pattern mirrors the system kernel's construction logic in crates/system/src/kernel.rs (lines 139-150), ensuring the engine is fully wired into the message bus and ready for deterministic order execution.
Summary
- Order submission requires cache validation, venue matching against the execution client, optional state snapshots, and forwarding to the correct
ExecutionClientvia the routing map—errors generate denied-order events rather than silent failures. - Fill processing reconciles incoming
FillReportevents against cached orders, detects duplicates and over-fills (respecting theallow_overfillsconfiguration), applies fills to orders, and updates positions using either hedging or netting OMS logic with deterministicPositionIdgeneration. - Venue connectivity relies on a registration system mapping venues to client IDs, with optional default client fallback and comprehensive connection status helpers (
check_connected,client_connection_status) that prevent trading when venues are unavailable. - Configuration options including
snapshot_orders,manage_own_order_books, andconvert_quote_qty_to_baseprovide fine-grained control over order state persistence and quantity handling.
Frequently Asked Questions
How does the ExecutionEngine prevent orders from being sent to the wrong venue?
The engine validates that order.instrument_id().venue matches client.venue() in the handle_submit_order method (lines 959-1033 of crates/execution/src/engine/mod.rs). If the venues do not match, the engine immediately calls deny_order and returns without forwarding the command to the client, preventing misrouted orders from reaching external venues.
What happens when a fill report arrives for an order that is already filled?
The apply_fill_to_order method (lines 660-675) checks for duplicate fills using order.is_duplicate_fill(&fill). If a duplicate is detected, the engine logs a warning and ignores the fill. For over-fills (fills exceeding the order quantity), the engine checks the allow_overfills configuration flag—if false, it rejects the fill with an error; if true, it logs a warning and processes the fill.
How does the ExecutionEngine support both hedging and netting position management?
The engine determines the Order Management System (OMS) type via determine_oms_type (lines 723-746), which checks strategy-level overrides, native client OMS settings from the routing map, or falls back to netting. For hedging, the engine attempts to reuse existing positions or generates new PositionIds via self.pos_id_generator.generate. For netting, it creates deterministic IDs derived from instrument_id and strategy_id, ensuring consistent position aggregation.
Can the ExecutionEngine operate with multiple trading venues simultaneously?
Yes, the engine supports multi-venue trading through its client registration system. The register_client method (lines 722-735) inserts entries into self.routing_map, mapping each venue to its corresponding client ID. Commands are routed based on the order's venue or explicit client ID. Additionally, register_default_client (lines 943-951) provides a fallback for commands without specific venue mappings, enabling complex multi-venue strategies while maintaining clean separation between venue connections.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →