Multithreading in Rust for Algorithmic Trading

Multithreading in Rust for Algorithmic Trading

·

4 min read

In algorithmic trading, speed, correctness and efficiency are paramount, and utilizing multithreading can significantly enhance the performance of trading systems. Rust, with its focus on safety, concurrency, and performance, provides a powerful language for building high-performance trading systems. Rust's ownership and borrowing system helps enforce thread safety at compile time. In this article, we will see multithreading in Rust in the context of an algorithmic trading application. Multiple threads can process market data concurrently, enabling faster signal generation and ultimately reducing the latency and increasing the overall throughput of the trading system. Trading systems can be extremely complex however on a high level, it has three components.

  • Market data feed: Simulated or real-time market data stream.

  • Signal generation: Analysis of market data to generate trading signals.

  • Order execution: Placement and execution of trading orders.

In a simple example of this system, we need at least three separate threads to handle simulating market data, signal generation, and order execution, allowing the trading system to react swiftly to market conditions.

Market Data Simulation

For the sake of simplicity for this blog, let's just simulate a single stock. A common way to represent an order book is by using a struct. Here's an example of a basic struct for an order book:

#[derive(Debug)]
struct Quote {
    symbol: String,
    bids: (f64, u32),
    asks: (f64, u32),
}
#[derive(Debug)]
struct Order {
    symbol: String,
    quantity: i32,
    price: f64,
    order_type: String,
}

In this example, the Quote struct has fields: bids and asks. Each field holds tuples representing the best price levels and quantities. You can further enhance the Quote to hold the vector for bids and asks e.g. bids: Vec<(f64, u32)>.

fn simulate_market_data() -> Quote {
    let normal = rand_distr::Normal::new(0.0, 1.0).unwrap();
    let z = normal.sample(&mut rand::thread_rng());
    let st = 100.0;
    let risk_free_rate = 0.05;
    let volatility = 0.2;
    let dt = 1.0/252.0;
    let bidprice = st*exp(((risk_free_rate - 0.5 * volatility.powi(2)) * dt)+volatility * dt.sqrt()*z);
    let askprice = bidprice + 0.05;
    // Generate random quantity
    let mut rng = rand::thread_rng();
    let bid_quantity = rng.gen_range(100..1000);
    let ask_quantity = rng.gen_range(100..1000);
    let quote = OrderBook {
        symbol: "SPY".to_string(),
        bids: (bidprice, bid_quantity),
        asks: (askprice, ask_quantity),
    };
    return quote
}

In this example, the simulate_market_data function generates random market data and returns Quote.

Channels for inter-thread communication

Channel provides a built-in communication mechanism for inter-thread data exchange. It handles synchronization internally, reducing the likelihood of synchronization errors.

fn market_data(tx: mpsc::Sender<Quote>) {
    loop {
        // Generate market data
        let order_data = simulate_market_data();
        // Send market data to the main thread
        tx.send(order_data).expect("Failed to send market data");
        // Pause the execution for one millisecond
        thread::sleep(Duration::from_millis(1));
    }
}

We will run the market_data function in a separate thread and repeatedly generates market data using simulate_market_data. It sends the generated market data to the main thread via a channel (tx) and wait for one millisecond to send another order data.

Spawn threads

fn main() {
    // Create channels for inter-thread communication
    let (tx, rx) = mpsc::channel();
    let (tx_order, rx_order) = mpsc::channel();
    // Spawn a thread for market data simulation
    thread::spawn(move || {
        market_data(tx);
    });
    // Receive and process market data in the separate thread
    thread::spawn(move || {
        signal_generation(rx,tx_order);
    });
    thread::spawn(move || {
        order_management(rx_order);
    });
    loop{}
}

In the main function, two channels are created for inter-thread communication using mpsc::channel(). Then, three separate threads are spawned using thread::spawn, where the market_data, signal_generation and order_management functions are executed. The main thread continuously receives market data from the channel using rx.recv(). Upon receiving market data, it can process it according to the requirements.

Signal generation and order management

fn signal_generation(rx: mpsc::Receiver<Quote>,tx_order: mpsc::Sender<Order>){
    while let Ok(data) = rx.recv() {
        // Process market data
        println!("Received market data: {:?}", data);
        let bid_price = data.bids.0;
        let ask_price = data.asks.0;
        if bid_price<100.0{
            println!("Place BUY Order");
            let order = Order {
                symbol: data.symbol,
                quantity: 1,
                price: bid_price,
                order_type: "LMT".to_string(),
            };
            tx_order.send(order).expect("Failed to send order");
        }
        else if ask_price>100.0 {
            println!("Place SELL Order");
            let order = Order {
                symbol: data.symbol,
                quantity: 1,
                price: ask_price,
                order_type: "LMT".to_string(),
            };
            tx_order.send(order).expect("Failed to send order");
        };
    }
}

Here we are using two channels one for receiving Quotes and another for sending orders.

fn order_management(rx:mpsc::Receiver<Order>){
    while let Ok(data) = rx.recv() {
        // Process Orders
        println!("Received order: {:?}", data);
    }
}

Other considerations

You can use Mutex as well. Ideally, use mutexes to protect shared data and coordinate access to critical sections. You may have multi-step operations that involve multiple threads collaborating to complete a task then Mutexes can be used to coordinate the execution of these operations, ensuring that threads wait for their turn and follow a specific order or synchronization protocol.

Additionally, other synchronization primitives like RwLock, Atomic types, or even higher-level abstractions like message queues or publish-subscribe systems are useful depending on the specific requirements.

Resources

Here is an excellent video by Jon Gjengset to learn more about channels.

And here is another great resource to learn multithreading in Rust, probably the BEST Book on concurrency by Mara Bos. Excellent Read. Thank you, Mara.

There are a few Rust-specific tools like flamegraph that can help identify bottlenecks and areas for optimization while using multithreading. Flamegraphs are used to visualize where time is being spent.

Hope this is helpful!

Did you find this article valuable?

Support Siddharth by becoming a sponsor. Any amount is appreciated!