Skip to main content

Building an Order Execution Engine: Simulating EVM-Based Trading

You submit a buy order. The price on screen shows $50,000. Your fill comes back at $50,047.

That gap is slippage. It doesn't happen by accident. It's what you get when an execution engine walks the order book, consumes liquidity across price levels, and adds gas costs on the way. Understanding it requires building the engine yourself.

This post extends the basic order book into a full execution engine that simulates EVM-based trading. It covers partial fills across levels, gas cost tracking, slippage measurement, and blockchain confirmation delays.

Repository: cpp-trading

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