Multithreading in Rust for Algorithmic Trading
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!
Subscribe to my newsletter
Read articles from Siddharth directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Siddharth
Siddharth
I am Quantitative Developer, Rust enthusiast and passionate about Algo Trading.