Building an Order Execution Engine: Simulating EVM-Based Trading
Building on our basic order book, we'll now create an advanced order execution engine that simulates trading on an EVM-based blockchain. This implementation handles order matching, tracks execution details like fill prices, gas costs, and slippage—critical metrics for understanding trading performance.
Overview
This execution engine:
- Matches incoming orders against the order book
- Tracks partial fills across multiple price levels
- Calculates gas costs and slippage
- Simulates blockchain confirmation delays
- Maintains execution history for analysis
Project Structure
Required Headers
#include <iostream>
#include <string>
#include <map>
#include <vector>
#include <iomanip>
#include <queue>
#include <chrono>
#include <thread>
queue: Managing pending orders waiting for block confirmationchrono: Time-related operationsthread: Simulating block confirmation delays
Data Structures
Order Struct with Remaining Quantity
struct Order {
int id;
double price;
int quantity;
bool is_buy;
int remainingQuantity;
Order(int i, double p, int q, bool buy)
: id(i), price(p), quantity(q), is_buy(buy), remainingQuantity(q) {}
};
Key addition: remainingQuantity tracks unfilled quantity after partial executions.
Fill Struct
struct Fill {
int order_id;
double fill_price;
int fill_quantity;
double gas_cost;
double slippage;
Fill(int oid, double fp, int fq, double gc, double slp)
: order_id(oid), fill_price(fp), fill_quantity(fq),
gas_cost(gc), slippage(slp) {}
};
Represents a single execution event with all relevant metrics.
OrderExecutionEngine Class
Private Members
class OrderExecutionEngine {
private:
std::map<double, int, std::greater<double>> bids;
std::map<double, int> asks;
std::queue<Order> pending_orders;
std::vector<Fill> execution_history;
double gas_price_per_unit = 0.001;
int block_time_ms = 12000; // 12 seconds (Ethereum-like)
Key parameters:
gas_price_per_unit: Transaction cost per unit tradedblock_time_ms: Simulated block confirmation time
Helper Methods
Gas Cost Calculation
double calculateGasCost(int quantity) {
return gas_price_per_unit * quantity;
}
Larger orders incur higher transaction costs.
Slippage Calculation
double calculateSlippage(double expected_price, double actual_price, bool is_buy) {
if (is_buy) {
return actual_price - expected_price; // Positive = worse (paid more)
} else {
return expected_price - actual_price; // Positive = worse (received less)
}
}
Slippage measures how much worse the execution price was compared to expected:
- Buy orders: Positive slippage = paid more than expected
- Sell orders: Positive slippage = received less than expected
Order Matching Logic
std::vector<Fill> matchOrder(const Order& order) {
std::vector<Fill> fills;
int remaining = order.remainingQuantity;
if (order.is_buy) {
// Match against asks (sell orders)
auto it = asks.begin();
while (remaining > 0 && it != asks.end() && it->first <= order.price) {
int available = it->second;
int fill_qty = (remaining < available) ? remaining : available;
double fill_price = it->first;
// Update order book
it->second -= fill_qty;
if (it->second <= 0) {
it = asks.erase(it);
} else {
++it;
}
// Calculate metrics
double gas_cost = calculateGasCost(fill_qty);
double slippage = calculateSlippage(order.price, fill_price, true);
fills.push_back(Fill(order.id, fill_price, fill_qty, gas_cost, slippage));
remaining -= fill_qty;
}
} else {
// Match against bids (buy orders) - similar logic but reversed
auto it = bids.begin();
while (remaining > 0 && it != bids.end() && it->first >= order.price) {
// ... similar matching logic
}
}
// Add remaining quantity to order book if not fully filled
if (remaining > 0) {
if (order.is_buy) {
bids[order.price] += remaining;
} else {
asks[order.price] += remaining;
}
}
return fills;
}
Matching logic:
- For buy orders: Match against asks (sell orders) at prices ≤ limit price
- For sell orders: Match against bids (buy orders) at prices ≥ limit price
- Fill at multiple price levels if needed
- Add unfilled quantity to order book
Submitting Orders
void submitOrder(const Order& order) {
std::cout << "Submitting " << (order.is_buy ? "BUY" : "SELL")
<< " order: " << order.quantity << " @ $" << order.price << "\n";
// Simulate block confirmation delay
std::this_thread::sleep_for(std::chrono::milliseconds(block_time_ms / 4));
// Match the order
std::vector<Fill> fills = matchOrder(order);
// Process fills
int total_filled = 0;
for (const Fill& fill : fills) {
execution_history.push_back(fill);
std::cout << " Fill: " << fill.fill_quantity << " @ $"
<< std::fixed << std::setprecision(2) << fill.fill_price
<< " (Gas: $" << fill.gas_cost
<< ", Slippage: $" << fill.slippage << ")\n";
total_filled += fill.fill_quantity;
}
if (total_filled < order.quantity) {
std::cout << " Partial fill: " << (order.quantity - total_filled)
<< " remaining in order book\n";
} else {
std::cout << " Full fill completed\n";
}
}
Adding Liquidity
void addLiquidity(double price, int quantity, bool is_buy) {
if (is_buy) {
bids[price] += quantity;
} else {
asks[price] += quantity;
}
}
Market makers can add liquidity to specific price levels.
Execution Statistics
void printExecutionStats() const {
std::cout << "\n=== EXECUTION STATISTICS ===\n";
std::cout << "Total fills: " << execution_history.size() << "\n";
double total_gas = 0.0;
double total_slippage = 0.0;
double total_volume = 0.0;
for (const Fill& fill : execution_history) {
total_gas += fill.gas_cost;
total_slippage += fill.slippage;
total_volume += fill.fill_price * fill.fill_quantity;
}
std::cout << std::fixed << std::setprecision(2);
std::cout << "Total gas costs: $" << total_gas << "\n";
std::cout << "Total slippage: $" << total_slippage << "\n";
std::cout << "Total volume: $" << total_volume << "\n";
}
Complete Example
int main() {
OrderExecutionEngine engine;
// Add initial liquidity (market makers)
engine.addLiquidity(50000.00, 10, false); // Sell @ $50k
engine.addLiquidity(50010.00, 15, false); // Sell @ $50,010
engine.addLiquidity(50020.00, 20, false); // Sell @ $50,020
engine.addLiquidity(49990.00, 12, true); // Buy @ $49,990
engine.addLiquidity(49980.00, 18, true); // Buy @ $49,980
engine.addLiquidity(49970.00, 25, true); // Buy @ $49,970
engine.printOrderBook();
// Execute trades
Order buy1(1, 50015.00, 30, true);
engine.submitOrder(buy1);
engine.printOrderBook();
Order sell1(2, 49985.00, 5, false);
engine.submitOrder(sell1);
engine.printOrderBook();
Order buy2(3, 50025.00, 8, true);
engine.submitOrder(buy2);
engine.printOrderBook();
engine.printExecutionStats();
return 0;
}
Understanding the Output
Submitting BUY order: 30 @ $50015.00
Fill: 10 @ $50000.00 (Gas: $0.01, Slippage: $-15.00)
Fill: 15 @ $50010.00 (Gas: $0.02, Slippage: $-5.00)
Partial fill: 5 remaining in order book
=== EXECUTION STATISTICS ===
Total fills: 2
Total gas costs: $0.03
Total slippage: $-20.00
Total volume: $1,251,500.00
Key insights:
- Negative slippage = better than expected (got better prices)
- Partial fills are common in real trading
- Gas costs accumulate with each fill
Key Concepts
Price-Time Priority
Orders are matched at the best available prices first, then by time. Our implementation prioritizes price levels correctly.
Partial Fills
Large orders often fill across multiple price levels:
- Fill at best price first
- Continue to next price level
- Remaining quantity stays in order book
Gas Costs
On EVM chains, every transaction costs gas. Our simulation:
- Charges per unit traded
- Accumulates across multiple fills
- Shows total cost in statistics
Slippage
Slippage occurs when:
- Large orders move the market
- Limited liquidity at best price
- Order book depth is insufficient
Negative slippage (getting better prices) is possible when:
- Market moves in your favor
- You're providing liquidity
- You get price improvement
Extending the Implementation
Market Orders
void submitMarketOrder(const Order& order) {
Order market_order = order;
market_order.price = (order.is_buy) ?
std::numeric_limits<double>::max() : // Buy at any price
0.0; // Sell at any price
submitOrder(market_order);
}
Stop Orders
void submitStopOrder(const Order& order, double stop_price) {
// Trigger when price crosses stop_price
}
Order Cancellation
void cancelOrder(int order_id) {
// Remove from order book
}
Performance Considerations
- Order book updates: Efficient map operations for price level updates
- Fill tracking: Vector for O(1) append, but consider deque for large histories
- Thread safety: Add mutexes for concurrent access in production
Common Pitfalls
- Slippage calculation: Ensure correct sign for buy vs. sell
- Remaining quantity: Always update
remainingQuantitycorrectly - Order book state: Keep bids/asks in sync with fills
- Gas estimation: Real gas costs are more complex—this is simplified
Conclusion
This execution engine demonstrates:
- Order matching across multiple price levels
- Partial fill handling for large orders
- Performance metrics (gas, slippage)
- Blockchain simulation with confirmation delays
From here, you can extend it with:
- Advanced order types (stop, limit, market)
- Risk management (position limits, margin checks)
- Real blockchain integration
- Performance optimizations
The foundation is solid for building production trading systems that handle real-world complexity while maintaining performance and accuracy.