1 unstable release
new 0.1.0 | Apr 10, 2025 |
---|
#757 in Magic Beans
125 downloads per month
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 orderscancel_order()
: Cancels an existing ordersettle_batch()
: Returns and clears matched orderstry_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