#order #order-book #book #engine #rules #interface

silhouette-mvp-orderbook

A price-time priority order book matching engine

1 unstable release

new 0.1.0 Apr 10, 2025

#757 in Magic Beans

Download history 125/week @ 2025-04-07

125 downloads per month

MIT license

125KB
2K SLoC

MVP Order Book

A price-time priority order book matching engine.

Library Interface

order_received(order: Order)

Adds an order to the order book. Takes in an Order struct (in src/types.rs):

pub struct Order {
  /// Unique identifier for the order
  pub id: alloy_primitives::U64,
  /// Address that receives tokens after order settlement
  pub receiver: alloy_primitives::Address,
  /// Token pair being traded (e.g., "ETH/USDC")
  pub token_pair: String,
  /// Whether this is a buy or sell order
  pub side: OrderSide,
  /// Amount of base token to trade
  pub amount: alloy_primitives::U64,
  /// Limit price for the order
  pub price: alloy_primitives::U64,
  /// Timestamp after which the order expires
  pub expiry: alloy_primitives::U64,
  /// Timestamp when the order was received
  pub timestamp: alloy_primitives::U64,
}
  • Match: Represents a successful trade with:

cancel_order(order_id: U64) -> CancellationResult

Cancels an order by its ID. Returns a CancellationResult with details about the cancelled order, or an error if the order doesn't exist.

pub struct CancellationResult {
    /// ID of the cancelled order
    pub order_id: U64,
    /// Address that would have received tokens after order settlement
    pub receiver: Address,
    /// Remaining amount of base token that was cancelled
    pub amount: U64,
    /// Token pair being traded (e.g., "ETH/USDC")
    pub token_pair: String,
    /// Limit price for the order
    pub limit_price: U64,
    /// Whether this was a buy or sell order
    pub side: OrderSide,
}

settle_batch(current_time: U64, exchange_price: U64) -> BatchResult

Matches all orders in the order book. Can be called every time a new order is received. Orders are matched based on price-time priority, with order ID as a tiebreaker for orders with the same price and time. For two compatible orders (each within the other's limit price), the trade executes at the sell order's price. For example, these two orders will be matched at price 1000:

  • Buy order with limit price 1100 and amount 100
  • Sell order with limit price 1000 and amount 100

If an order has not been completely filled and settle_batch() is called after that order's expiry, the remaining amount will be settled on the L1. exchange_price is provided but no longer used for compatibility checks - all expired orders are included in the remainders vector in BatchResult.

settle_batch() returns a BatchResult struct (in src/types.rs):

pub struct BatchResult {
    /// Matches that occurred during this batch
    pub matches: Vec<Match>,
    /// Orders that expired with unfilled amounts
    pub remainders: Vec<Remainder>,
    /// Total remainder amount
    pub total_remainder: U64,
}

Order Types

The system supports both limit and market orders:

Limit Orders

Standard orders with a specified price limit:

  • Buy limit orders execute at or below the specified price
  • Sell limit orders execute at or above the specified price

Market Orders

Orders that execute immediately at the best available price:

  • Market buy orders match with the lowest-priced sell orders
  • Market sell orders match with the highest-priced buy orders
  • Market orders are processed immediately upon receipt
  • Partially filled market orders remain in the book until fully matched

Order Book (src/orderbook.rs)

Main matching engine implementation:

  • Orders are stored in a BTreeMap for price-time priority ordering with order ID as a tiebreaker
  • Separate books for buy and sell orders
  • Core functions:
    • order_received(): Processes new orders
    • cancel_order(): Cancels an existing order
    • settle_batch(): Returns and clears matched orders
    • try_match(): Internal matching logic

OrderBuilder (src/builder.rs)

A more intuitive API for creating orders:

// Create a buy order
let buy_order = OrderBuilder::buy("ETH")
    .using("USDC")
    .id(U64::from(1))
    .receiver(Address::zero())
    .amount(dec!(1.0)) // Use dec! macro for Decimal input (e.g., 1 ETH)
    .price(dec!(2000)) // Use dec! macro for Decimal input (e.g., $2000)
    .expiry(U64::from(u64::MAX))
    .timestamp(U64::from(1))
    .build()
    .unwrap();

// Create a sell order
let sell_order = OrderBuilder::sell("ETH")
    .using("USDC")
    .id(U64::from(2))
    .receiver(Address::repeat_byte(1))
    .amount(dec!(1.0)) // Use dec! macro
    .price(dec!(1900)) // Use dec! macro
    .expiry(U64::from(u64::MAX))
    .timestamp(U64::from(2))
    .build()
    .unwrap();

// Create a market buy order
let market_buy_order = OrderBuilder::market_buy("ETH")
    .using("USDC")
    .id(U64::from(3))
    .receiver(Address::zero())
    .amount(dec!(1.0)) // Use dec! macro (price is not needed for market orders)
    .expiry(U64::from(u64::MAX))
    .timestamp(U64::from(3))
    .build()
    .unwrap();

// Create a market sell order
let market_sell_order = OrderBuilder::market_sell("ETH")
    .using("USDC")
    .id(U64::from(4))
    .receiver(Address::zero())
    .amount(dec!(1.5)) // Use dec! macro
    .expiry(U64::from(u64::MAX))
    .timestamp(U64::from(4))
    .build()
    .unwrap();

This maintains compatibility with the standard base/quote convention used internally.

Price Convention

For any trading pair "BASE/QUOTE" (e.g., "ETH/USDC"):

  • Price is always expressed as QUOTE per unit of BASE
  • Examples:
    • ETH/USDC pair: price of 1500 means 1500 USDC per ETH
    • BTC/USD pair: price of 45000 means 45000 USD per BTC
    • BTC/ETH pair: price of 16.67 means 16.67 ETH per BTC
  • Amount is always in units of the base currency (the first currency in the pair)

The OrderBuilder handles this convention internally, so users can specify which token they're buying or selling without knowing which token is the base/quote

Price and Size Validation Rules

Both Price (px) and Size (sz) have a maximum number of decimals that are accepted.

Price Rules

  • Prices can have up to 5 significant figures
  • No more than MAX_DECIMALS - szDecimals decimal places, where MAX_DECIMALS is 6
  • Integer prices are always allowed, regardless of the number of significant figures
  • Example: 123456.0 is a valid price even though 12345.6 is not
  • Example: 0.0001234 is valid if szDecimals is 0 or 1, but not if szDecimals is greater than 2 (more than 6-2 decimal places)

Size Rules

  • Sizes are rounded to the szDecimals of that asset
  • Example: if szDecimals = 3 then 1.001 is a valid size but 1.0001 is rounded down to 1.000
  • szDecimals for an asset is found in the meta response to the info endpoint

Dependencies

~5.5MB
~102K SLoC