This article is translated from Medium
Original: Building Stock Market Engine from scratch in Rust
https://github.com/Harshil-Jani/stock_engine_rs
Medium
This article is relatively basic, but it is very helpful for beginners to understand a simple trading system prototype.
From the perspective of the exchange matching engine, the main structure is shown in the figure below:

The four main entities in the figure above are explained as follows, read the original for details:
-
Order: Request to Buy or Sell at a particular price and quantity.
-
OrderBook: List of all the orders for a particular company.
-
Company: A publicly listed company which offers shares to people and has a particular price for it at a single moment.
-
Engine: List of companies that are available on the exchange.
The final market data is displayed in the user terminal form as shown in the figure below:

The proposed project structure by the author is as follows:

Order
-
An order consists of a price and a quantity.
-
Each order can be one of two types: either a buy order or a sell order.
The structure of an order and its type (buy or sell):
use rust_decimal::Decimal;
pub enum BuyOrSell {
Buy,
Sell,
}
pub struct Order {
pub quantity: Decimal,
pub price: Decimal,
pub order_type: BuyOrSell,
}
impl Order {
pub fn new(
quantity: Decimal,
price: Decimal,
order_type: BuyOrSell,
) -> Order {
Order {
quantity,
price,
order_type,
}
}
}
OrderBook
From an architectural perspective, the order book has different orders. For example, if there are two orders at the same price, the best practice may be to accumulate the quantity of orders by price. For example:
Order 1: Buy, Quantity 10, Price 100.00
Order 2: Buy, Quantity 20, Price 100.00
These can be combined into one price point and included in the order book.
Order Book: [(Buy, Quantity 30, Price 100.00)]
In the order book shown in the figure:

In the green area, you can see all the buy orders arranged by a specific price, while in the red area, you can see the sell orders arranged by a specific price.
The structure of the order book is defined: since it involves looking up specific price points and adding all order requests at that price, a HashMap or BTreeMap can be used.
use std::collections::HashMap;
use rust_decimal::Decimal;
use super::order::Order;
pub struct OrderBook {
// HashMap : [Key : Price, Value : All the orders at that price]
pub buy_orders: HashMap<Decimal, Vec<Order>>,
pub sell_orders: HashMap<Decimal, Vec<Order>>,
}
impl OrderBook {
pub fn new() -> OrderBook {
OrderBook {
buy_orders: HashMap::new(),
sell_orders: HashMap::new()
}
}
}
We need a method to add orders to the order book. The add_order_to_orderbook method is written in impl OrderBook:
use super::order::BuyOrSell;
impl OrderBook {
pub fn add_order_to_orderbook(&mut self, order: Order) {
// Check the order type whether it is a buy or sell order
let order_price = order.price;
match order.order_type {
BuyOrSell::Buy => {
// Check If the price exists in the buy_orders HashMap
match self.buy_orders.get_mut(&order_price) {
Some(orders) => {
// If it exists, add the order to the existing price point
orders.push(order);
}
None => {
// If it does not exist, create a new price point and add the order
self.buy_orders.insert(
order_price,
vec![order],
);
}
}
}
BuyOrSell::Sell => {
// Check If the price exists in the sell_orders HashMap
match self.sell_orders.get_mut(&order_price) {
Some(orders) => {
// If it exists, add the order to the existing price point
orders.push(order);
}
None => {
// If it does not exist, create a new price point and add the order
self.sell_orders.insert(
order_price,
vec![order],
);
}
}
}
}
}
}
Writing Test Cases:
#[cfg(test)]
mod test {
use super::*;
use core_engine::{order::{BuyOrSell, Order}, orderbook::OrderBook};
use rust_decimal_macros::dec;
#[test]
fn test_add_order_to_orderbook() {
// Initialize the new order_book
let mut order_book = OrderBook::new();
// Create some buy orders.
let buy_order_1 = Order::new(dec!(35), dec!(690), BuyOrSell::Buy);
let buy_order_2 = Order::new(dec!(20), dec!(685), BuyOrSell::Buy);
let buy_order_3 = Order::new(dec!(15), dec!(690), BuyOrSell::Buy);
// Create some sell orders.
let sell_order_1 = Order::new(dec!(10), dec!(700), BuyOrSell::Sell);
let sell_order_2 = Order::new(dec!(25), dec!(705), BuyOrSell::Sell);
let sell_order_3 = Order::new(dec!(30), dec!(700), BuyOrSell::Sell);
// Add the orders to the order_book
order_book.add_order_to_orderbook(buy_order_1);
order_book.add_order_to_orderbook(buy_order_2);
order_book.add_order_to_orderbook(buy_order_3);
order_book.add_order_to_orderbook(sell_order_1);
order_book.add_order_to_orderbook(sell_order_2);
order_book.add_order_to_orderbook(sell_order_3);
assert_eq!(order_book.buy_orders.len(), 2);
assert_eq!(order_book.sell_orders.len(), 2);
assert_eq!(order_book.buy_orders.get(&dec!(690)).unwrap().len(), 2);
assert_eq!(order_book.buy_orders.get(&dec!(685)).unwrap().len(), 1);
assert_eq!(order_book.sell_orders.get(&dec!(700)).unwrap().len(), 2);
assert_eq!(order_book.sell_orders.get(&dec!(705)).unwrap().len(), 1);
}
}
The first and second assertions ensure that the total unique price points in the order book for buy and sell orders are both 2.
The next line of assertions ensures that the number of buy orders at price 690 is 2, and at price 685 is 1.
Similarly, for the last two assertion statements, the number of sell orders at 700 is 2, and at 705 is 1.
An important fact observed is that in the order book of the Angel-One application shown in the figure, the buy orders are arranged in descending order, and the sell orders in ascending order. This needs to be considered because the market price is determined based on these closest values and the last executed orders.
As shown, using a price and order HashMap, there is no need to sort the buy and sell orders. This information could be useful for front-end display, but for the engine, the best needed are the buy and sell prices. If you observe the screenshot, at the bottom, there are total buy and sell volumes, which can give a clue about the trends and the situation of buyers and sellers.
impl OrderBook {
pub fn best_buy_price(&self) -> Option<Decimal> {
// Get the maximum price from the buy_orders HashMap
self.buy_orders.keys().max().cloned()
}
pub fn best_sell_price(&self) -> Option<Decimal> {
// Get the minimum price from the sell_orders HashMap
self.sell_orders.keys().min().cloned()
}
pub fn buy_volume(&self) -> Option<Decimal> {
// Calculate the total volume of the buy orders
let buy_volume: Decimal = self.buy_orders.values().flatten().map(|order| order.quantity).sum();
Some(buy_volume)
}
pub fn sell_volume(&self) -> Option<Decimal> {
// Calculate the total volume of the buy orders
let sell_volume: Decimal = self.sell_orders.values().flatten().map(|order| order.quantity).sum();
Some(sell_volume)
}
}
In the test module, add a test case to track the buying and selling volumes of stocks:
#[test]
fn test_prices_and_volumes() {
// Initialize the new order_book
let mut order_book = OrderBook::new();
// Create some buy orders.
let buy_order_1 = Order::new(dec!(35), dec!(690), BuyOrSell::Buy);
let buy_order_2 = Order::new(dec!(20), dec!(685), BuyOrSell::Buy);
let buy_order_3 = Order::new(dec!(15), dec!(690), BuyOrSell::Buy);
// Create some sell orders.
let sell_order_1 = Order::new(dec!(10), dec!(700), BuyOrSell::Sell);
let sell_order_2 = Order::new(dec!(25), dec!(705), BuyOrSell::Sell);
let sell_order_3 = Order::new(dec!(30), dec!(700), BuyOrSell::Sell);
// Add the orders to the order_book
order_book.add_order_to_orderbook(buy_order_1);
order_book.add_order_to_orderbook(buy_order_2);
order_book.add_order_to_orderbook(buy_order_3);
order_book.add_order_to_orderbook(sell_order_1);
order_book.add_order_to_orderbook(sell_order_2);
order_book.add_order_to_orderbook(sell_order_3);
assert_eq!(order_book.best_buy_price().unwrap(), dec!(690));
assert_eq!(order_book.best_sell_price().unwrap(), dec!(700));
// Total Buying Order Quantity = 35+20+15
assert_eq!(order_book.buy_volume().unwrap(), dec!(70));
// Total Selling Order Quantity = 10+25+30
assert_eq!(order_book.sell_volume().unwrap(), dec!(65));
}