Jump to content

Building a High-Performance Orderbook Aggregator in Rust

From JOHNWICK

In the fast-paced world of cryptocurrency trading, having access to accurate and consolidated orderbook data is crucial for making informed trading decisions. In this article, we will explore the architecture and implementation of a high-performance orderbook aggregator built in Rust. This system fetches, processes, and merges orderbook data from multiple cryptocurrency exchanges, providing a unified view of the market. Architecture Overview The orderbook aggregator follows a modular architecture with these key components: 1. Data Fetcher: Handles HTTP requests to exchange APIs
2. Rate Limiter: Manages API call frequency to respect exchange rate limits
3. Data Models: Defines the structure for orderbook data
4. Orderbook Merger: Combines orderbooks from different exchanges
5. Price Calculator: Computes execution prices for given quantities Core Components 1. Rate Limiter The RateLimiter is a critical component that ensures we don’t exceed exchange API rate limits. It implements a token bucket algorithm with these key features: pub struct RateLimiter {

state: Arc<Mutex<RateLimiterState>>,

}

struct RateLimiterState {

tokens: Decimal,
capacity: Decimal,
tokens_per_second: Decimal,
last_update: Instant,

}

    • Key Implementation Details:**
- Uses `Arc<Mutex<…>>` for thread-safe shared state
- Implements both non-blocking (`try_acquire`) and blocking (`acquire`) methods
- Supports both token-based and interval-based rate limiting
- Uses precise timing with `std::time::Instant` for accurate rate calculation

The rate limiter is particularly important when dealing with exchange APIs that have strict rate limits, preventing our application from being banned for excessive requests.

      1. 2. Data Models and Deserialization

The system uses strongly-typed models for each exchange’s orderbook data. Here’s how the Coinbase order model is defined: ```rust
pub struct CoinbaseOrder {
pub price: Decimal,
pub size: Decimal,
pub num_orders: u64,
}
```

    • Notable Implementation Details:**
- Uses `rust_decimal` for precise decimal arithmetic
- Implements custom deserialization for handling exchange-specific JSON formats
- Uses Rust’s type system to ensure data consistency

The deserialization is particularly interesting as it handles different numeric formats from various exchanges: ```rust
fn from_str_to_decimal<’de, D>(d: D) -> Result<Decimal, D::Error>
where
D: Deserializer<’de>,
{
let s = String::deserialize(d)?;
Decimal::from_str(&s).map_err(Error::custom)
}
```

      1. 3. Orderbook Merging

The orderbook merger combines orderbooks from different exchanges while maintaining proper ordering. The implementation handles both asks (ascending) and bids (descending) orders: ```rust
pub fn merge_sorted_asks(coinbase_asks: Vec<CoinbaseOrder>, gemini_asks: Vec<GeminiOrder>) -> Vec<OrderBook> {
let mut merged: Vec<OrderBook> = Vec::with_capacity(coinbase_asks.len() + gemini_asks.len());
// … merging logic …
}
```

    • Key Features:**
- Efficient merging using iterators
- Pre-allocates the result vector for better performance
- Maintains proper ordering during merge
- Handles edge cases like empty orderbooks
      1. 4. Price Calculation

The price calculator determines the average price for executing a trade of a given size: ```rust
pub fn calculate_entity_price(
entity: &[OrderBook],
quantity: Decimal,
is_ascending: bool,
order_type: &str
) -> Result<Decimal, String> {
// … calculation logic …
}
```

    • Features:**
- Handles partial order fills
- Validates orderbook sorting
- Tracks liquidity across multiple exchanges
- Provides detailed logging for debugging
    1. Performance Optimizations

1. **Efficient Memory Usage**:
— Uses `&str` instead of `String` where possible to avoid allocations
— Pre-allocates vectors with known capacity
— Implements efficient iteration patterns 2. **Concurrent Data Fetching**:
```rust
let (result_coinbase, result_gemini) = tokio::join!(
async {
coinbase_rl.acquire().await;
get_data(&client, &coinbase_api).await
},
async {
gemini_rl.acquire().await;
get_data(&client, &gemini_api).await
}
);
``` 3. **Precise Decimal Arithmetic**:
— Uses `rust_decimal` instead of floating-point for financial calculations
— Avoids floating-point precision errors in price calculations

    1. Key Learnings
      1. 1. Using Decimal for Financial Precision

The implementation uses `rust_decimal` to avoid floating-point precision issues common in financial calculations. This ensures that: - Prices are represented accurately
- Calculations maintain precision across operations
- Avoids rounding errors that could lead to financial discrepancies

      1. 2. Efficient String Handling

The code demonstrates several string handling optimizations: - Uses string slices (`&str`) instead of owned `String` where possible
- Implements custom deserialization to avoid intermediate string allocations
- Uses `String::with_capacity` when building strings to minimize reallocations

      1. 3. Asynchronous Programming with Tokio

The system leverages Tokio for efficient I/O operations: - Concurrent API requests to multiple exchanges
- Non-blocking rate limiting
- Efficient task scheduling

      1. 4. Error Handling and Logging

- Uses `anyhow` for ergonomic error handling
- Implements detailed logging for debugging and monitoring
- Handles API failures gracefully with fallback mechanisms

    1. Scaling to Other Exchanges

The architecture is designed to be extensible to additional exchanges: 1. **Adding a New Exchange**:
— Implement the exchange-specific data model
— Add API client configuration
— Implement the orderbook transformation logic
— Add to the merging pipeline 2. **Distributed Processing**:
— The system can be scaled horizontally by sharding by trading pair
— Each instance can handle a subset of currency pairs
— Redis or similar can be used for shared state and rate limiting 3. **Caching Layer**:
— Implement caching of orderbook snapshots
— Use WebSocket connections for real-time updates
— Add diff-based updates to minimize data transfer

    1. Conclusion

This orderbook aggregator demonstrates how to build a high-performance, reliable system for consolidating cryptocurrency market data. By leveraging Rust’s type system, efficient memory management, and asynchronous programming, we’ve created a solution that’s both fast and maintainable. The modular design allows for easy extension to additional exchanges and features, making it a solid foundation for building more complex trading systems. Whether you’re building a trading bot, market analysis tool, or just need consolidated market data, this implementation provides a robust starting point.