Multithreading in Rust for Algorithmic Trading

Search for a command to run...

Good stuff! I've been looking for key insights like this with Rust 🦀
Often when we hear algorithmic trading, we think about alpha-seeking algorithms i.e. generating profits by predicting market movements or identifying mispricings or trends and executing trades automatically. However, for a long time, algorithmic trad...
Iterators are a fundamental concept in programming that enable you to process a sequence of items, such as elements in a collection, one at a time, without revealing the collection's internal structure. They form the backbone of operations like loopi...

A closure is an anonymous function that can capture variables from its environment. At a high level, Rust closures allow you to write small, concise functions inline, without having to formally define a new fn with a name. They’re particularly handy ...

Immutability is a cool concept in programming that C++ questions with "why?" It means that once you create a variable or object, you can't change its state. This idea somewhat opposes object-oriented concepts, yet it's crucial for writing clean, main...

Object-oriented programming, also called OOP, is a programming style that is dependent on the concept of objects. It is very popular and established. It is like designing and organizing your code by thinking of parts of your program as real-world obj...

Traits are the most important topic to understand the design patterns in Rust. Traits are fundamental feature in Rust that provide a way for shared behavior, abstraction and polymorphish. You can think of traits as interfaces in other programming la...

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.
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.
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.
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.
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);
}
}

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.
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!