Welcome to this tutorial on building a stock market engine using Rust! In this tutorial, we’ll walk through the process of writing the engine from scratch and understand how things actually work.
We’ll break down complex concepts into simple terms, making it easy for everyone to follow along. You can expect a step-by-step approach, with clean and working code by the end of each tutorial (expecting 3 at max).
Throughout the tutorial, we’ll use screenshots from the Angel-One mobile app to make things clear and understand the stock market. So, buckle up, and let’s get started on this exciting journey!
Disclaimer #
For those unfamiliar with what does an engine means, let me clarify in simpler terms. An engine is essentially a library containing functions and methods. While these components aren’t visible in live action, they constitute the core logic of a system. In Rust, such libraries are referred to as crates, and this engine serves as one such library crate.
The engine’s application can take the form of various implementations. For instance, you might opt to utilize the actix-web crate to construct a web-based backend, with the engine providing the underlying core logic.
If you find the code’s flow uncertain or get confused about where specific segments belong, don’t hesitate to visit the repository of this project.
There, you’ll find the same code and can refer to the tutorial-1 commit for guidance.
Repo Link : https://github.com/Harshil-Jani/stock_engine_rs
Please consider leaving a GitHub star. Your support is greatly appreciated.
Introduction to stock exchange engine structure #
Take a look from inside out in order to understand the structure clearly. Let’s start from the smallest box Order and come to what an entire engine comprises of.
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.
This is the high-level overview of what we will be building for this project. At the end you will be able to handle the companies listed with a particular price and with the change just like in screenshot below.
Defining Orders #
Let’s start from the brick and then build an empire out of it. We will start this tutorial with coding from a smallest of the things available to our clear minds. As you are now already aware that the order is made up of a price and a quantity. And each order can be of two types : either a Buy order or a Sell order.
Example of Order Booking from Angel One Application
Take a look at the above order snippet from app. On the very top you can see the company name or stock name. It has a specific price point (market price) and a percent change(with respect to one previous day). On right you have the stock exchange which here is BSE (Bombay Stock Exchange) and then what you have is type of order B/S. B stands for Buy and S for Sell. Below you have a Product Type. We will implement Delivery at the moment and discuss intraday later on in our engine. But just to make you aware here is the explaination of both order type.
Intraday : The shares should be bought and sold on the same day. And those can be leveraged with multiples of quantity.
Delivery : Normal order which you can hold for any period of time you wish to hold for.
Next you have a quantity and price and then you have an option to choose between a limit order or a market order. The very basic difference is that in market order you buy/sell at the current market price. But in limit order you can set the price by yourself. For above example you can let’s say choose to buy when the price falls to 625 or sell when price goes high to 630. But in market order, buying and selling will be forced at the market rate which is 627.90
Let’s start by implementing the order.
Start with a fresh new rust project
$ cargo new stock\_engine --lib
$ cd stock\_engine
$ cd src
$ mkdir core\_engine
$ cd core\_engine
$ touch engine.rs
$ touch order.rs
$ touch orderbook.rs
$ touch mod.rs
Before doing the code, We will need a crate called rust_decimal and rust_decimal_macros.
$ cargo add rust\_decimal
$ cargo add rust\_decimal\_macros
By the time I am writing this tutorial, The crates are at version v1.34
. In case if you use this in future and things tend to break then you can set the same version in your Cargo.toml files. The final project structure looks like below
├── Cargo.lock
├── Cargo.toml
├── src
│ ├── core\_engine
│ │ ├── engine.rs
│ │ ├── mod.rs
│ │ ├── order.rs
│ │ └── orderbook.rs
│ └── lib.rs
Your lib.rs
file should look like below
pub mod core\_engine;
In the new file core_engine/order.rs
we will define the structure of how the order looks like and what type is it (Buy or a 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,
}
}
}
Well an interesting fact is that in Indian Stock Market the quantity is absolute whole number and is never in fraction or decimal. But In future, I have plans to expand the engine for different countries market or even include the crypto in it. In case you want it specific to a single market you can decide on whether to put the quantity type as decimal or unsigned integer.
Defining the OrderBook #
Let’s move to core_engine/orderbook.rs
and define the orderbook. From the architecture, It would be clear that Orderbook has different orders. Let’s say you have two orders at same price then the best thing could be that you can append the orders at a price and add the quantities. For example,
1st Order : BUY , 10 Quantity, 100.00 Price
2nd Order : BUY, 20 Quantity, 100.00 Price
Then you can comibine both under one Price Point and include that order inside of orderbook.
Orderbook : [ (BUY, 30 Quantity, 100.00 Price)]
Take a look at this orderbook in angel one application.
On the green side you have all bid or buy orders arranged at a particular price and on red you have ask or sell orders arranged at a particular price.
Let’s now define the structure of orderbook. We can use a HashMap or BTreeMap since it involves looking up at a particular price point and then adding all the order requests of that price in a lot.
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()
}
}
}
Now, We would need a method using which we can add our order to the orderbook. Let’s write add_order_to_orderbook
method into the 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\],
);
}
}
}
}
}
}
Alright, So now we can create an order and add it into the orderbook which is awesome. Let us write some test cases to ensure that the entire functionality works as expected.
Sample Test Case for creation of orderbook
Write the test cases in lib.rs
file as below
#\[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() {
// Initialze 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 two assert statements ensure that the total unique price points of the orderbook are 2 for both buy orders and the sell order.
Next two assert statements ensure that the number of buy orders at price 690 are 2 and at price 685 is 1.
Similarly, for last two assert statements the number of sell orders at 700 are 2 and at 705 is 1.
One thing to observe is that the orderbook from the screenshot attached above of Angel-One application has all the buy orders in descending manner and the sell orders in increasing or ascending manner. This is an important thing to consider because the market price is decided on the basis of these closest values and the last made transaction of order.
In our case, We are using a HashMap of Prices and Orders so I think we don’t need to have a sorting of Buy and Sell orders. Those could be well for showing on the frontend but for the engine what we need is the best buy price and the best sell price. If you observe back in the screenshot, we have at the bottom total buy volume and the sell volume giving us an idea of what is the trend and if there are more buyers or 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)
}
}
Alright, This is exactly what we wanted. Now that we can track the buy volume and the sell volume of the stock. Let’s add a test case to our test module
#\[test\]
fn test\_prices\_and\_volumes(){
// Initialze 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));
}
Perfect. We will complete this up in next tutorial since it is going to be the base on which we will write tons of features. As I have already promised, You will have an end to end working and clean code by the end of each tutorial so I will move to the main engine part.
Writing the Core Engine #
The engine holds the list of all the public companies and each company has their own orderbook. Each orderbook has series of Buy or Sell orders. This is the most outer step of our stock engine.
The company that is listed has following parameters : Name, Symbol, Sector, exchange in which it has been listed. Let’s build some enums in core_engine/engine.rs
to track what we would need for a company.
pub enum Market {
IndianMarket(IndianExchange),
USMarket(USExchange),
CryptoMarket(CryptoExchange),
}
pub enum IndianExchange {
NSE,
BSE,
}
pub enum USExchange {
NASDAQ,
NYSE,
}
pub enum CryptoExchange {
WazirX,
CoinDCX,
Binance,
Coinbase,
}
pub enum Sector {
Technology,
Finance,
Banking,
Healthcare,
Energy,
ConsumerDiscretionary,
ConsumerStaples,
Industrials,
Materials,
RealEstate,
CommunicationServices,
Utilities
}
Now that we have multiple enums which builds up entire market of which our company needs to be listed, We need to write the structure for the MatchingEngine and the Company and it should have it’s own implemented OrderBook.
use std::collections::HashMap;
use super::orderbook::OrderBook;
pub struct Company {
name : String,
symbol : String,
sector : Sector,
market : Market,
}
impl Company {
pub fn new (
name : String,
symbol : String,
sector : Sector,
market: Market
) -> Company {
Company {
name,
symbol,
sector,
market
}
}
}
pub struct MatchingEngine {
pub orderbooks : HashMap<Company, OrderBook>
}
impl MatchingEngine {
pub fn new() -> MatchingEngine {
MatchingEngine {
orderbooks : HashMap::new()
}
}
}
Now, We will write methods to list any new company or get the orderbook of existing company. In order to do that, We need to add some attributes to the custom enums and the structs. Please add the derive attributes of Hash, PartialEq, Eq and Clone
on top all the above enums and structs i.e Market, Sector, IndianExchange, USExchange, CryptoExchange and Company.
#\[derive(Hash,PartialEq,Eq,Clone)\]
The methods for listing any company or viewing the orderbook is as described below
impl MatchingEngine {
pub fn list\_new\_company(&mut self, company : Company) {
let orderbook = OrderBook::new();
self.orderbooks.insert(company, orderbook);
}
pub fn get\_company\_orderbook(&mut self, company : &Company) -> Option<&mut OrderBook> {
self.orderbooks.get\_mut(company)
}
}
Let’s test the company listing and perform basic orderbook operations using the above method.
use self::core\_engine::engine::{ Company, IndianExchange, Market, MatchingEngine, Sector };
#\[test\]
fn test\_company\_listing() {
let mut engine = MatchingEngine::new();
let company = Company::new(
"Nactore".to\_string(),
"NACT".to\_string(),
Sector::Technology,
Market::IndianMarket(IndianExchange::BSE)
);
engine.list\_new\_company(company.clone());
assert\_eq!(engine.orderbooks.len(), 1);
match engine.get\_company\_orderbook(&company) {
Some(order\_book) => {
// 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);
}
None => panic!("Company not found"),
};
assert\_eq!(engine.get\_company\_orderbook(&company).unwrap().buy\_volume().unwrap(), dec!(70));
assert\_eq!(engine.get\_company\_orderbook(&company).unwrap().sell\_volume().unwrap(), dec!(65));
}
Tada ! Our engine has now evolved to a point where we can list companies, maintain their order books, and execute buy or sell orders seamlessly.
In our upcoming tutorial, we’ll shift our focus towards the execution of orders. Currently, orders merely exist within the order book without any matching mechanism. The essence of our engine lies in its ability to match buyers with sellers and vice versa.
If you enjoyed this project, that’s awesome! I’m trying something different by mixing US, Indian, and crypto markets together. It’s tricky because crypto is always active, but Indian and US markets have set hours. Right now, we’re focused on the Indian market.
If you found this tutorial helpful, please give it a like on GitHub. I’m open to feedback and learning. Thanks for being a part of this experiment!
If you found this tutorial helpful, please give it a like on GitHub. I’m open to feedback and learning. Thanks for being a part of this experiment!