mirror of
https://github.com/0xShay/rust-orderbook.git
synced 2026-01-10 20:53:24 +00:00
initial commit
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/target
|
||||
|
||||
/.vs
|
||||
7
Cargo.lock
generated
Normal file
7
Cargo.lock
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "rust_orderbook"
|
||||
version = "0.1.0"
|
||||
6
Cargo.toml
Normal file
6
Cargo.toml
Normal file
@@ -0,0 +1,6 @@
|
||||
[package]
|
||||
name = "rust_orderbook"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Shay Patel
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
22
README.md
Normal file
22
README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# rust-orderbook
|
||||
|
||||
This repository contains the source code for a basic orderbook implementation in Rust, exposing functionality to place both BUY and SELL limit and market orders.
|
||||
|
||||
## Motivation
|
||||
The primary motivation for writing this code was (and is) to learn the basics of Rust syntax and it's unique features, as well as expand and apply my understanding of fundamentally key trading and market dynamics.
|
||||
|
||||
If you spot any issues or errors within the code, or think I could've done something differently, please raise an issue or feel free to send in a PR - I'd love to learn how I could've approached things from a different perspective, and I can take these considerations into account for future projects!
|
||||
|
||||
## Building and running
|
||||
You can run the code by cloning the repository, `cd`ing into the cloned folder, and simply running `cargo run`. This will both build and run the project in a single step.
|
||||
|
||||
## Usage
|
||||
Once the program is running, it will populate the order book with some dummy BUY and SELL orders (`populate_orderbook`), and then print out the highest and lowest 5 BUY and SELL orders, whilst also displaying the spread between the two.
|
||||
|
||||
You can run one of three commands:
|
||||
- `BUY [quantity] (price)` - buy `quantity` at market rate. Optional `price` parameter will place a limit BUY order.
|
||||
- `SELL [quantity] (price)` - sell `quantity` at market rate. Optional `price` parameter will place a limit SELL order.
|
||||
- `EXIT` -> exits the program
|
||||
|
||||
## License
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
336
src/main.rs
Normal file
336
src/main.rs
Normal file
@@ -0,0 +1,336 @@
|
||||
use std::io;
|
||||
use std::collections::BTreeMap;
|
||||
use std::str::SplitWhitespace;
|
||||
|
||||
struct Orderbook {
|
||||
// both of these maps map a price to a quantity
|
||||
bids: BTreeMap<i32, i32>,
|
||||
asks: BTreeMap<i32, i32>,
|
||||
}
|
||||
|
||||
fn read_in_quantity_and_price(split: &mut SplitWhitespace<'_>, quantity: &mut i32, price: &mut Option<i32>) {
|
||||
let quantity_opt_str: Option<&str> = split.next();
|
||||
let price_opt_str: Option<&str> = split.next();
|
||||
|
||||
// ensure quantity_str is a valid integer
|
||||
*quantity = quantity_opt_str.expect("No quantity provided.").trim().parse::<i32>().expect("Invalid quantity provided.");
|
||||
|
||||
// ensure quantity is positive
|
||||
assert!(*quantity > 0, "Quantity must be positive.");
|
||||
|
||||
// see if a price has been supplied
|
||||
match price_opt_str {
|
||||
None => {},
|
||||
Some(price_str) => {
|
||||
// ensure price_str is a valid number
|
||||
let price_num = price_str.trim().parse::<i32>().expect("Invalid price provided.");
|
||||
|
||||
// ensure price_num is positive
|
||||
assert!(price_num > 0, "Price must be positive.");
|
||||
|
||||
*price = Some(price_num);
|
||||
}
|
||||
};
|
||||
|
||||
()
|
||||
}
|
||||
|
||||
fn gen_hashtag_loop(n: usize) -> String {
|
||||
let hashtags: String = std::iter::repeat('#').take(n).collect();
|
||||
return hashtags;
|
||||
}
|
||||
|
||||
fn list_orders(ob: &Orderbook) {
|
||||
println!("===============================");
|
||||
println!("{:<5} {:>13}", "PRICE", "QUANTITY");
|
||||
|
||||
println!();
|
||||
|
||||
if ob.asks.len() < 5 {
|
||||
for _ in 0..(5 - ob.asks.len()) { println!(); };
|
||||
};
|
||||
|
||||
for ask in ob.asks.iter().take(5).rev() {
|
||||
println!("${:<4.2} {:>4} {}", ask.0, ask.1, gen_hashtag_loop((*ask.1).try_into().expect("Failed to format i32 as usize.")));
|
||||
};
|
||||
println!("-------------------------------");
|
||||
|
||||
println!("{:.2}bps", (100.0 * ((*ob.asks.first_key_value().expect("One-sided order book.").0 as f32 / *ob.bids.last_key_value().expect("One-sided order book.").0 as f32) - 1.0)));
|
||||
|
||||
println!("-------------------------------");
|
||||
for bid in ob.bids.iter().rev().take(5) {
|
||||
println!("${:<4.2} {:>4} {}", bid.0, bid.1, gen_hashtag_loop((*bid.1).try_into().expect("Failed to format i32 as usize.")));
|
||||
};
|
||||
|
||||
if ob.bids.len() < 5 {
|
||||
for _ in 0..(5 - ob.bids.len()) { println!(); };
|
||||
};
|
||||
|
||||
println!();
|
||||
}
|
||||
|
||||
fn market_buy(ob: &mut Orderbook, quantity: i32) {
|
||||
println!("MARKET BUY placed: {} @ market price", quantity);
|
||||
let mut left_to_buy: i32 = quantity;
|
||||
let mut total_value: i32 = 0;
|
||||
while left_to_buy > 0 {
|
||||
let (p, q) = ob.asks.pop_first().expect("Insufficient sell volume.");
|
||||
if q > left_to_buy {
|
||||
// push back sell order with reduced quantity
|
||||
ob.asks.insert(p, q-left_to_buy);
|
||||
|
||||
// increase total_value
|
||||
total_value += left_to_buy * p;
|
||||
|
||||
// reduce left_to_buy
|
||||
left_to_buy = 0;
|
||||
} else {
|
||||
// increase total_value
|
||||
total_value += q*p;
|
||||
|
||||
// reduce left_to_buy
|
||||
left_to_buy -= q;
|
||||
}
|
||||
}
|
||||
println!("Bought {} at an average price of ${:.2}", quantity, ((total_value as f32) / (quantity as f32)));
|
||||
}
|
||||
|
||||
fn market_sell(ob: &mut Orderbook, quantity: i32) {
|
||||
println!("MARKET SELL placed: {} @ market price", quantity);
|
||||
let mut left_to_sell: i32 = quantity;
|
||||
let mut total_value: i32 = 0;
|
||||
while left_to_sell > 0 {
|
||||
let (p, q) = ob.bids.pop_last().expect("Insufficient buy volume.");
|
||||
if q > left_to_sell {
|
||||
// push back buy order with reduced quantity
|
||||
ob.bids.insert(p, q-left_to_sell);
|
||||
|
||||
// increase total_value
|
||||
total_value += left_to_sell * p;
|
||||
|
||||
// reduce left_to_sell
|
||||
left_to_sell = 0;
|
||||
} else {
|
||||
// increase total_value
|
||||
total_value += q*p;
|
||||
|
||||
// reduce left_to_sell
|
||||
left_to_sell -= q;
|
||||
}
|
||||
}
|
||||
println!("Sold {} at an average price of ${:.2}", quantity, ((total_value as f32) / (quantity as f32)));
|
||||
}
|
||||
|
||||
fn create_buy_order(ob: &mut Orderbook, quantity: i32, price: i32) {
|
||||
println!("Placed BUY order ({} @ ${})", quantity, price);
|
||||
match ob.bids.get(&price) {
|
||||
Some(prev_quantity) => {
|
||||
// add to existing order
|
||||
ob.bids.insert(price, quantity + prev_quantity);
|
||||
},
|
||||
None => {
|
||||
// create new order
|
||||
ob.bids.insert(price, quantity);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn create_sell_order(ob: &mut Orderbook, quantity: i32, price: i32) {
|
||||
println!("Placed SELL order ({} @ ${})", quantity, price);
|
||||
match ob.asks.get(&price) {
|
||||
Some(prev_quantity) => {
|
||||
// add to existing order
|
||||
ob.asks.insert(price, quantity + prev_quantity);
|
||||
},
|
||||
None => {
|
||||
// create new order
|
||||
ob.asks.insert(price, quantity);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn limit_buy(ob: &mut Orderbook, quantity: i32, price: i32) {
|
||||
println!("LIMIT BUY placed: {} @ ${}", quantity, price);
|
||||
let mut left_to_buy: i32 = quantity;
|
||||
let mut total_value: i32 = 0;
|
||||
let mut total_quantity: i32 = 0;
|
||||
|
||||
// check the cheapest sell order - if it doesn't exist, or it does but the price is too high, create a new buy order
|
||||
while left_to_buy > 0 {
|
||||
match ob.asks.pop_first() {
|
||||
None => {
|
||||
// there are no sell orders, so create a new buy order
|
||||
create_buy_order(ob, left_to_buy, price);
|
||||
left_to_buy = 0;
|
||||
},
|
||||
Some((p, q)) => {
|
||||
// check if this sell order is cheap enough
|
||||
if p <= price {
|
||||
// cheap enough, fill order as much as possible
|
||||
if q > left_to_buy {
|
||||
// push back sell order with reduced quantity
|
||||
ob.asks.insert(p, q-left_to_buy);
|
||||
|
||||
// increase total_value
|
||||
total_value += left_to_buy * p;
|
||||
total_quantity += left_to_buy;
|
||||
|
||||
// reduce left_to_buy
|
||||
left_to_buy = 0;
|
||||
} else {
|
||||
// increase total_value
|
||||
total_value += q*p;
|
||||
total_quantity += q;
|
||||
|
||||
// reduce left_to_buy
|
||||
left_to_buy -= q;
|
||||
}
|
||||
} else {
|
||||
ob.asks.insert(p, q);
|
||||
create_buy_order(ob, left_to_buy, price);
|
||||
left_to_buy = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("Bought {} at an average price of ${:.2}", total_quantity, ((total_value as f32) / (total_quantity as f32)));
|
||||
}
|
||||
|
||||
fn limit_sell(ob: &mut Orderbook, quantity: i32, price: i32) {
|
||||
println!("LIMIT SELL placed: {} @ ${}", quantity, price);
|
||||
let mut left_to_sell: i32 = quantity;
|
||||
let mut total_value: i32 = 0;
|
||||
let mut total_quantity: i32 = 0;
|
||||
|
||||
// check the most appealing buy order - if it doesn't exist, or it does but the price is too low, create a new sell order
|
||||
while left_to_sell > 0 {
|
||||
match ob.bids.pop_last() {
|
||||
None => {
|
||||
// there are no buy orders, so create a new sell order
|
||||
create_sell_order(ob, left_to_sell, price);
|
||||
left_to_sell = 0;
|
||||
},
|
||||
Some((p, q)) => {
|
||||
// check if this buy order is high enough
|
||||
if p >= price {
|
||||
// fill order as much as possible
|
||||
if q > left_to_sell {
|
||||
// push back buy order with reduced quantity
|
||||
ob.bids.insert(p, q-left_to_sell);
|
||||
|
||||
// increase total_value
|
||||
total_value += left_to_sell * p;
|
||||
total_quantity += left_to_sell;
|
||||
|
||||
// reduce left_to_sell
|
||||
left_to_sell = 0;
|
||||
} else {
|
||||
// increase total_value
|
||||
total_value += q*p;
|
||||
total_quantity += q;
|
||||
|
||||
// reduce left_to_sell
|
||||
left_to_sell -= q;
|
||||
}
|
||||
} else {
|
||||
ob.bids.insert(p, q);
|
||||
create_sell_order(ob, left_to_sell, price);
|
||||
left_to_sell = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("Sold {} at an average price of ${:.2}", total_quantity, ((total_value as f32) / (total_quantity as f32)));
|
||||
}
|
||||
|
||||
fn populate_orderbook(ob: &mut Orderbook) {
|
||||
limit_buy(ob, 5, 8);
|
||||
limit_buy(ob, 4, 7);
|
||||
limit_buy(ob, 2, 3);
|
||||
limit_buy(ob, 6, 8);
|
||||
limit_buy(ob, 5, 15);
|
||||
limit_buy(ob, 10, 10);
|
||||
limit_buy(ob, 8, 9);
|
||||
limit_buy(ob, 1, 2);
|
||||
limit_buy(ob, 12, 14);
|
||||
limit_buy(ob, 7, 5);
|
||||
|
||||
limit_sell(ob, 2, 15);
|
||||
limit_sell(ob, 3, 16);
|
||||
limit_sell(ob, 3, 17);
|
||||
limit_sell(ob, 4, 18);
|
||||
limit_sell(ob, 5, 20);
|
||||
limit_sell(ob, 6, 18);
|
||||
limit_sell(ob, 9, 21);
|
||||
limit_sell(ob, 15, 25);
|
||||
limit_sell(ob, 2, 19);
|
||||
limit_sell(ob, 11, 22);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut input_string = String::new();
|
||||
|
||||
let mut ob = Orderbook {
|
||||
bids: BTreeMap::new(),
|
||||
asks: BTreeMap::new(),
|
||||
};
|
||||
|
||||
populate_orderbook(&mut ob);
|
||||
list_orders(&ob);
|
||||
|
||||
while input_string.trim() != "EXIT" {
|
||||
input_string.clear();
|
||||
|
||||
println!("Enter a command:");
|
||||
io::stdin().read_line(&mut input_string).unwrap();
|
||||
println!();
|
||||
|
||||
let mut split = input_string.split_whitespace();
|
||||
|
||||
let command: Option<&str> = split.next();
|
||||
|
||||
match command.expect("No command specified.") {
|
||||
"EXIT" => {},
|
||||
"BUY" => {
|
||||
let mut quantity: i32 = 0;
|
||||
let mut price_opt: Option<i32> = None;
|
||||
read_in_quantity_and_price(&mut split.clone(), &mut quantity, &mut price_opt);
|
||||
|
||||
match price_opt {
|
||||
None => {
|
||||
market_buy(&mut ob, quantity);
|
||||
},
|
||||
Some(price) => {
|
||||
limit_buy(&mut ob, quantity, price);
|
||||
}
|
||||
};
|
||||
},
|
||||
"SELL" => {
|
||||
let mut quantity: i32 = 0;
|
||||
let mut price_opt: Option<i32> = None;
|
||||
read_in_quantity_and_price(&mut split.clone(), &mut quantity, &mut price_opt);
|
||||
|
||||
match price_opt {
|
||||
None => {
|
||||
market_sell(&mut ob, quantity);
|
||||
},
|
||||
Some(price) => {
|
||||
limit_sell(&mut ob, quantity, price);
|
||||
}
|
||||
};
|
||||
},
|
||||
_ => {
|
||||
panic!("Unknown command.");
|
||||
}
|
||||
};
|
||||
|
||||
println!();
|
||||
list_orders(&ob);
|
||||
}
|
||||
|
||||
println!("Program terminating.");
|
||||
}
|
||||
Reference in New Issue
Block a user