Skip to main content

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 confirmation
  • chrono: Time-related operations
  • thread: 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 traded
  • block_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:

  1. For buy orders: Match against asks (sell orders) at prices ≤ limit price
  2. For sell orders: Match against bids (buy orders) at prices ≥ limit price
  3. Fill at multiple price levels if needed
  4. 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:

  1. Fill at best price first
  2. Continue to next price level
  3. 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

  1. Slippage calculation: Ensure correct sign for buy vs. sell
  2. Remaining quantity: Always update remainingQuantity correctly
  3. Order book state: Keep bids/asks in sync with fills
  4. 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.


Read more from Cryptogrammar