diff --git a/Cargo.lock b/Cargo.lock index c4c09ef..dcb0a1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,18 +2,162 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "autocfg" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "either" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "indexmap" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + [[package]] name = "num-traits" version = "0.2.19" @@ -29,6 +173,7 @@ version = "0.2.0" dependencies = [ "either", "ordered-float", + "rstest", "rustc-hash", ] @@ -41,8 +186,179 @@ dependencies = [ "num-traits", ] +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "proc-macro-crate" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + +[[package]] +name = "rstest" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b423f0e62bdd61734b67cd21ff50871dfaeb9cc74f869dcd6af974fbcb19936" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version", +] + +[[package]] +name = "rstest_macros" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e1711e7d14f74b12a58411c542185ef7fb7f2e7f8ee6e2940a883628522b42" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn", + "unicode-ident", +] + [[package]] name = "rustc-hash" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "syn" +version = "2.0.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" + +[[package]] +name = "toml_edit" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] diff --git a/Cargo.toml b/Cargo.toml index eb560d5..28e337e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,3 +9,6 @@ rust-version = "1.70" either = "1.13.0" ordered-float = "4.2.2" rustc-hash = "2.0.0" + +[dev-dependencies] +rstest = "0.22.0" diff --git a/src/book.rs b/src/book.rs index a8a9abb..0667c8f 100644 --- a/src/book.rs +++ b/src/book.rs @@ -25,11 +25,11 @@ pub struct Client { } impl Client { - pub fn new() -> Client { + pub fn new() -> Rc { CLIENT_ID.with(|thread_id| { let id = thread_id.get(); thread_id.set(id + 1); - Self { id } + Rc::new(Self { id }) }) } } @@ -354,15 +354,134 @@ fn is_deeper(a: f64, b: f64, side: &Side) -> bool { #[cfg(test)] mod test { use super::*; + use rstest::*; - #[test] - fn test_empty_order_book() { - let ob = OrderBook::new(); - assert!(ob.get_ladder(&Side::Bid).is_empty()); - assert!(ob.get_ladder(&Side::Ask).is_empty()); + #[fixture] + fn ob() -> OrderBook { + OrderBook::new() + } + + #[rstest] + #[case(Side::Bid)] + #[case(Side::Ask)] + fn test_empty_order_book(#[by_ref] ob: &OrderBook, #[case] side: Side) { + assert!(ob.get_ladder(&side).is_empty()); + } + + #[rstest] + fn test_empty_bid(#[by_ref] ob: &OrderBook) { assert_eq!(ob.best_bid(), None); assert_eq!(ob.best_bid_size(), None); + } + + #[rstest] + fn test_empty_ask(#[by_ref] ob: &OrderBook) { assert_eq!(ob.best_ask(), None); assert_eq!(ob.best_ask_size(), None); } + + #[test] + fn test_client_id() { + let client1 = Client::new(); + let client2 = Client::new(); + assert_ne!(client1.id, client2.id); + } + + #[fixture] + fn client() -> Rc { + Client::new() + } + + #[rstest] + #[case(-1.0, 0)] + #[case(-1.0, 1)] + #[case(0.0, 1)] + fn test_invalid_order( + #[by_ref] ob: &OrderBook, + client: Rc, + #[case] price: f64, + #[case] size: u64, + ) { + let order = Order::new(Side::Bid, price, size, &client); + assert!(ob.validate_order(&order).is_err()); + } + + #[fixture] + fn order(client: Rc) -> Order { + Order::new(Side::Bid, 1.0, 1, &client) + } + + #[rstest] + fn test_valid_order(#[by_ref] ob: &OrderBook, #[by_ref] order: &Order) { + assert!(ob.validate_order(&order).is_ok()); + } + + #[rstest] + fn test_passive_placement(mut ob: OrderBook, order: Order) { + let result = ob.insert(order); + assert!(matches!(result, OrderBookResult::OrderId(_))); + } + + #[rstest] + fn test_cancel_order(mut ob: OrderBook, order: Order) { + let order_id = match ob.insert(order) { + OrderBookResult::OrderId(id) => id, + _ => unreachable!(), + }; + let result = ob.cancel(order_id); + assert!(matches!(result, OrderBookResult::Canceled)); + } + + #[rstest] + fn test_cancel_invalid_order(mut ob: OrderBook, order: Order) { + ob.insert(order); + let result = ob.cancel(18378); + assert!(matches!(result, OrderBookResult::Error(_))); + } + + #[rstest] + fn test_best_bid(mut ob: OrderBook, client: Rc) { + let prices = vec![1.4, 1.5, 1.6, 1.3, 1.8, 1.4]; + let sizes = vec![1, 2, 3, 4, 5, 6]; + + for (price, size) in prices.iter().zip(sizes.iter()) { + let order = Order::new(Side::Bid, *price, *size, &client); + ob.insert(order); + } + + assert_eq!(ob.best_bid(), Some(1.8)); + assert_eq!(ob.best_bid_size(), Some(5)); + } + + #[rstest] + fn test_best_ask(mut ob: OrderBook, client: Rc) { + let prices = vec![1.4, 1.5, 1.6, 1.3, 1.8, 1.4]; + let sizes = vec![1, 2, 3, 4, 5, 6]; + + for (price, size) in prices.iter().zip(sizes.iter()) { + let order = Order::new(Side::Ask, *price, *size, &client); + ob.insert(order); + } + + assert_eq!(ob.best_ask(), Some(1.3)); + assert_eq!(ob.best_ask_size(), Some(4)); + } + + #[rstest] + fn test_partial_fill(mut ob: OrderBook, client: Rc) { + let order1 = Order::new(Side::Bid, 1.5, 1, &client); + let order2 = Order::new(Side::Ask, 1.5, 2, &client); + ob.insert(order1); + + if let OrderBookResult::OrderIdTrades(_, trades) = ob.insert(order2) { + let trade = &trades[0]; + assert_eq!(trade.price, 1.5); + assert_eq!(trade.size, 1); + assert_eq!(trades.len(), 1); + } else { + unreachable!(); + } + + assert_eq!(ob.best_ask_size(), Some(1)); + } } diff --git a/src/main.rs b/src/main.rs index d01dad6..cd5b92c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,9 @@ use orderbook::book::{Client, Order, OrderBook, OrderBookResult, Side}; -use std::rc::Rc; fn main() { let mut ob = OrderBook::new(); - let client1 = Rc::new(Client::new()); - let client2 = Rc::new(Client::new()); + let client1 = Client::new(); + let client2 = Client::new(); // Initial order book {