diff --git a/Cargo.lock b/Cargo.lock index 46e2505..1b6ea7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,17 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "version_check", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -11,18 +22,87 @@ dependencies = [ "libc", ] +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "borsh" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "bumpalo" version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + [[package]] name = "cc" version = "1.2.55" @@ -39,6 +119,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.43" @@ -58,12 +144,35 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -76,6 +185,21 @@ dependencies = [ "wasip2", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "iana-time-zone" version = "0.1.65" @@ -100,6 +224,22 @@ dependencies = [ "cc", ] +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + [[package]] name = "js-sys" version = "0.3.85" @@ -122,6 +262,12 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + [[package]] name = "num-traits" version = "0.2.19" @@ -148,13 +294,32 @@ dependencies = [ [[package]] name = "oxide-arbiter" -version = "0.1.1-beta.2" +version = "0.2.0-beta.2" dependencies = [ "chrono", "ordered-float", + "rust_decimal", "uuid", ] +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -164,6 +329,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "quote" version = "1.0.44" @@ -179,18 +364,173 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "rkyv" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rust_decimal" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61f703d19852dbf87cbc513643fa81428361eb6940f1ac14fd58155d295a3eb0" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand", + "rkyv", + "serde", + "serde_json", +] + [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.114" @@ -202,6 +542,57 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.8+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0742ff5ff03ea7e67c8ae6c93cac239e0d9784833362da3f9a9c1da8dfefcbdc" +dependencies = [ + "winnow", +] + [[package]] name = "unicode-ident" version = "1.0.22" @@ -214,11 +605,23 @@ version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" dependencies = [ - "getrandom", + "getrandom 0.3.4", "js-sys", "wasm-bindgen", ] +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasip2" version = "1.0.2+wasi-0.2.9" @@ -260,7 +663,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.114", "wasm-bindgen-shared", ] @@ -294,7 +697,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -305,7 +708,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.114", ] [[package]] @@ -332,8 +735,52 @@ dependencies = [ "windows-link", ] +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "zerocopy" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index fd6c910..d6ff74d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxide-arbiter" -version = "0.1.1-beta.2" +version = "0.2.0-beta.2" edition = "2024" description = "A CLOB order matching engine with price-time priority, limit/market orders, and four time-in-force policies." license = "MIT" @@ -12,4 +12,5 @@ readme = "README.md" [dependencies] chrono = "0.4.43" ordered-float = "5.1.0" +rust_decimal = "1.40.0" uuid = { version = "1.20.0", features = ["v4"] } diff --git a/README.md b/README.md index fd6d792..b477206 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,8 @@ oxide-arbiter implements a Centralized Limit Order Book (CLOB) with price-time p ``` orders: HashMap // source of truth; O(1) lookup by ID -buy_orders: HashMap, VecDeque>> -sell_orders: HashMap, VecDeque>> +buy_orders: HashMap>> +sell_orders: HashMap>> trades: Vec // append-only execution history ``` @@ -83,9 +83,9 @@ enum TimeInForce { GTC, IOC, FOK, DAY } | `order_side` | `OrderSide` | Buy or Sell | | `order_type` | `OrderType` | Limit or Market | | `time_in_force` | `TimeInForce` | Execution policy | -| `price` | `f32` | Limit price (market orders normalized to resting price) | -| `quantity` | `f32` | Requested quantity | -| `quantity_filled` | `f32` | Executed quantity | +| `price` | `Decimal` | Limit price (market orders normalized to resting price) | +| `quantity` | `Decimal` | Requested quantity | +| `quantity_filled` | `Decimal` | Executed quantity | | `status` | `OrderStatus` | Current lifecycle state | | `created_at` | `DateTime` | Creation timestamp | | `updated_at` | `DateTime` | Last modification timestamp | @@ -99,8 +99,8 @@ enum TimeInForce { GTC, IOC, FOK, DAY } | `buy_order_id` | `Uuid` | Matched buy order | | `sell_order_id` | `Uuid` | Matched sell order | | `item_id` | `Uuid` | Asset matched | -| `quantity` | `f32` | Execution size | -| `price` | `f32` | Execution price (resting order's price) | +| `quantity` | `Decimal` | Execution size | +| `price` | `Decimal` | Execution price (resting order's price) | | `timestamp` | `DateTime` | Execution timestamp | ### CreateOrderRequest @@ -111,8 +111,8 @@ enum TimeInForce { GTC, IOC, FOK, DAY } | `user_id` | `Uuid` | | `order_side` | `OrderSide` | | `order_type` | `OrderType` | -| `price` | `f32` | -| `quantity` | `f32` | +| `price` | `Decimal` | +| `quantity` | `Decimal` | | `time_in_force` | `TimeInForce` | --- @@ -129,13 +129,13 @@ add_order(&mut self, req: CreateOrderRequest) -> Result // Queries get_orders(&self) -> &HashMap get_order_by_id(&self, order_id: Uuid) -> Option<&Order> -get_current_market_price(&self, item_id: Uuid, side: OrderSide) -> Option +get_current_market_price(&self, item_id: Uuid, side: OrderSide) -> Option // Mutations cancel_order(&mut self, order_id: Uuid) -> bool update_order_status(&mut self, order_id: Uuid, status: OrderStatus) -> Option<&Order> -update_order_quantity(&mut self, order_id: Uuid, quantity: f32) -> Option<&Order> -update_order_price(&mut self, order_id: Uuid, price: f32) -> Option<&Order> +update_order_quantity(&mut self, order_id: Uuid, quantity: Decimal) -> Option<&Order> +update_order_price(&mut self, order_id: Uuid, price: Decimal) -> Option<&Order> // Trade history (public field) trades: Vec @@ -171,6 +171,8 @@ oxide-arbiter = "0.1.0-beta.1" ```rust use oxide_arbiter::{CreateOrderRequest, OrderBookService, OrderSide, OrderType, TimeInForce}; +use rust_decimal::Decimal; +use std::str::FromStr; let mut book = OrderBookService::new(); let asset_id = uuid::Uuid::new_v4(); @@ -183,8 +185,8 @@ let buy = book.add_order(CreateOrderRequest { order_side: OrderSide::Buy, order_type: OrderType::Limit, time_in_force: TimeInForce::GTC, - price: 100.0, - quantity: 50.0, + price: Decimal::from_str("100.0").unwrap(), + quantity: Decimal::from_str("50.0").unwrap(), }).unwrap(); // Incoming sell limit order — matches immediately @@ -194,8 +196,8 @@ let sell = book.add_order(CreateOrderRequest { order_side: OrderSide::Sell, order_type: OrderType::Limit, time_in_force: TimeInForce::GTC, - price: 100.0, - quantity: 50.0, + price: Decimal::from_str("100.0").unwrap(), + quantity: Decimal::from_str("50.0").unwrap(), }).unwrap(); // Inspect executed trades @@ -226,7 +228,7 @@ cargo test --- -## Roadmap +## Todos ### Indexing & Queries @@ -251,3 +253,4 @@ Every query other than lookup-by-ID currently requires an O(n) scan of the full | Item | Detail | |------|--------| | Thread safety | `OrderBookService` is not `Sync`. An `Arc>` wrapper or a channel-based design is needed for concurrent order acceptance. | +| Benchmarks | No performance benchmarks exist. A `criterion`-based suite would establish baseline throughput and catch regressions. | diff --git a/examples/basic_usage.rs b/examples/basic_usage.rs index 916d78f..0e7069b 100644 --- a/examples/basic_usage.rs +++ b/examples/basic_usage.rs @@ -1,4 +1,6 @@ use oxide_arbiter::{CreateOrderRequest, OrderBookService, OrderSide, OrderType, TimeInForce}; +use rust_decimal::Decimal; +use std::str::FromStr; fn main() { let mut order_book = OrderBookService::new(); @@ -8,20 +10,18 @@ fn main() { order_side: OrderSide::Buy, order_type: OrderType::Limit, time_in_force: TimeInForce::DAY, - price: 10.0, - quantity: 100.0, + price: Decimal::from_str("10.0").unwrap(), + quantity: Decimal::from_str("100.0").unwrap(), }); - let _ = order_book.add_order(CreateOrderRequest { item_id: uuid::Uuid::new_v4(), user_id: uuid::Uuid::new_v4(), order_side: OrderSide::Sell, order_type: OrderType::Limit, time_in_force: TimeInForce::DAY, - price: 12.0, - quantity: 50.0, + price: Decimal::from_str("12.0").unwrap(), + quantity: Decimal::from_str("50.0").unwrap(), }); - for (_, order_book_order) in order_book.get_orders() { println!("--- Order Details ---"); println!("Order ID: {}", order_book_order.id); @@ -34,7 +34,7 @@ fn main() { println!("Order Updated At: {}", order_book_order.updated_at); println!("---------------------"); } - println!("OrderBookService created successfully."); println!("Hello, world!"); } + diff --git a/examples/order_matching.rs b/examples/order_matching.rs index c151cdd..ac4f5b8 100644 --- a/examples/order_matching.rs +++ b/examples/order_matching.rs @@ -1,11 +1,12 @@ use oxide_arbiter::{ CreateOrderRequest, OrderBookService, OrderSide, OrderStatus, OrderType, TimeInForce, }; +use rust_decimal::Decimal; +use std::str::FromStr; fn print_orders(book: &OrderBookService) { let mut orders: Vec<_> = book.get_orders().values().collect(); orders.sort_by_key(|o| o.created_at); - for order in orders { println!( " [{:?}] {:?} {:?} — {}/{} units @ {} — created {}", @@ -22,33 +23,28 @@ fn print_orders(book: &OrderBookService) { fn main() { let mut book = OrderBookService::new(); - println!("=== Full Fill ==="); - let item_a = uuid::Uuid::new_v4(); - book.add_order(CreateOrderRequest { item_id: item_a, user_id: uuid::Uuid::new_v4(), order_side: OrderSide::Sell, order_type: OrderType::Limit, - price: 50.0, - quantity: 100.0, + price: Decimal::from_str("50.0").unwrap(), + quantity: Decimal::from_str("100.0").unwrap(), time_in_force: TimeInForce::GTC, }) .unwrap(); - book.add_order(CreateOrderRequest { item_id: item_a, user_id: uuid::Uuid::new_v4(), order_side: OrderSide::Buy, order_type: OrderType::Limit, - price: 50.0, - quantity: 100.0, + price: Decimal::from_str("50.0").unwrap(), + quantity: Decimal::from_str("100.0").unwrap(), time_in_force: TimeInForce::GTC, }) .unwrap(); - println!("Trades produced:"); for trade in &book.trades { println!( @@ -56,38 +52,32 @@ fn main() { trade.id, trade.quantity, trade.price ); } - println!("\nOrders:"); print_orders(&book); - // --- Partial fill --- println!("\n=== Partial Fill ==="); - let item_b = uuid::Uuid::new_v4(); - book.add_order(CreateOrderRequest { item_id: item_b, user_id: uuid::Uuid::new_v4(), order_side: OrderSide::Buy, order_type: OrderType::Limit, - price: 30.0, - quantity: 200.0, + price: Decimal::from_str("30.0").unwrap(), + quantity: Decimal::from_str("200.0").unwrap(), time_in_force: TimeInForce::GTC, }) .unwrap(); - // Sell fills only part of the resting buy — buy stays PartiallyFilled book.add_order(CreateOrderRequest { item_id: item_b, user_id: uuid::Uuid::new_v4(), order_side: OrderSide::Sell, order_type: OrderType::Limit, - price: 30.0, - quantity: 80.0, + price: Decimal::from_str("30.0").unwrap(), + quantity: Decimal::from_str("80.0").unwrap(), time_in_force: TimeInForce::GTC, }) .unwrap(); - println!("Trades produced:"); for trade in &book.trades { println!( @@ -95,20 +85,16 @@ fn main() { trade.id, trade.quantity, trade.price ); } - println!("\nOrders:"); print_orders(&book); - // --- Summary: filter by fill status --- println!("\n=== Filled Orders ==="); - let mut closed: Vec<_> = book .get_orders() .values() .filter(|o| matches!(o.status, OrderStatus::Closed)) .collect(); closed.sort_by_key(|o| o.created_at); - println!("Fully filled ({}):", closed.len()); for order in &closed { println!( @@ -116,14 +102,12 @@ fn main() { order.order_side, order.quantity_filled, order.price ); } - let mut partial: Vec<_> = book .get_orders() .values() .filter(|o| matches!(o.status, OrderStatus::PartiallyFilled)) .collect(); partial.sort_by_key(|o| o.created_at); - println!("Partially filled ({}):", partial.len()); for order in &partial { println!( @@ -135,7 +119,6 @@ fn main() { order.quantity - order.quantity_filled, ); } - let open_count = book .get_orders() .values() @@ -143,3 +126,4 @@ fn main() { .count(); println!("Open: {open_count}"); } + diff --git a/examples/time_in_force.rs b/examples/time_in_force.rs index 091c28a..48d3ee5 100644 --- a/examples/time_in_force.rs +++ b/examples/time_in_force.rs @@ -1,22 +1,21 @@ use oxide_arbiter::{CreateOrderRequest, OrderBookService, OrderSide, OrderType, TimeInForce}; +use rust_decimal::Decimal; +use std::str::FromStr; fn main() { println!("=== IOC (Immediate Or Cancel) ==="); - let mut book = OrderBookService::new(); let item = uuid::Uuid::new_v4(); - book.add_order(CreateOrderRequest { item_id: item, user_id: uuid::Uuid::new_v4(), order_side: OrderSide::Sell, order_type: OrderType::Limit, - price: 10.0, - quantity: 30.0, + price: Decimal::from_str("10.0").unwrap(), + quantity: Decimal::from_str("30.0").unwrap(), time_in_force: TimeInForce::GTC, }) .unwrap(); - // IOC buy for 100 — only 30 are available let ioc = book .add_order(CreateOrderRequest { @@ -24,36 +23,31 @@ fn main() { user_id: uuid::Uuid::new_v4(), order_side: OrderSide::Buy, order_type: OrderType::Limit, - price: 10.0, - quantity: 100.0, + price: Decimal::from_str("10.0").unwrap(), + quantity: Decimal::from_str("100.0").unwrap(), time_in_force: TimeInForce::IOC, }) .unwrap(); - println!("IOC order status: {:?}", ioc.status); println!("Quantity requested: 100"); println!("Quantity filled: {}", ioc.quantity_filled); println!("Quantity after IOC trim: {}", ioc.quantity); println!("Trades: {}", book.trades.len()); - // --- FOK: Fill Or Kill --- println!("\n=== FOK (Fill Or Kill) ==="); - let mut book = OrderBookService::new(); let item = uuid::Uuid::new_v4(); - // Resting sell at a price the FOK buy cannot reach book.add_order(CreateOrderRequest { item_id: item, user_id: uuid::Uuid::new_v4(), order_side: OrderSide::Sell, order_type: OrderType::Limit, - price: 20.0, - quantity: 100.0, + price: Decimal::from_str("20.0").unwrap(), + quantity: Decimal::from_str("100.0").unwrap(), time_in_force: TimeInForce::GTC, }) .unwrap(); - // FOK buy at 10.0 — no price match, so zero trades → entire order cancelled let fok = book .add_order(CreateOrderRequest { @@ -61,36 +55,30 @@ fn main() { user_id: uuid::Uuid::new_v4(), order_side: OrderSide::Buy, order_type: OrderType::Limit, - price: 10.0, - quantity: 50.0, + price: Decimal::from_str("10.0").unwrap(), + quantity: Decimal::from_str("50.0").unwrap(), time_in_force: TimeInForce::FOK, }) .unwrap(); - println!("FOK order status: {:?}", fok.status); println!("Quantity filled: {}", fok.quantity_filled); println!("Trades: {}", book.trades.len()); - // --- GTC: Good Till Cancelled --- println!("\n=== GTC (Good Till Cancelled) ==="); - let mut book = OrderBookService::new(); let item = uuid::Uuid::new_v4(); - let gtc = book .add_order(CreateOrderRequest { item_id: item, user_id: uuid::Uuid::new_v4(), order_side: OrderSide::Buy, order_type: OrderType::Limit, - price: 25.0, - quantity: 50.0, + price: Decimal::from_str("25.0").unwrap(), + quantity: Decimal::from_str("50.0").unwrap(), time_in_force: TimeInForce::GTC, }) .unwrap(); - println!("GTC order status after placement: {:?}", gtc.status); - book.cancel_order(gtc.id); let after_cancel = book.get_order_by_id(gtc.id).unwrap(); println!( @@ -98,3 +86,4 @@ fn main() { after_cancel.status ); } + diff --git a/src/components/dto.rs b/src/components/dto.rs index 7a5ffce..4e49018 100644 --- a/src/components/dto.rs +++ b/src/components/dto.rs @@ -1,16 +1,14 @@ use chrono::{DateTime, Utc}; +use rust_decimal::Decimal; use uuid::Uuid; #[derive(Debug, Clone, Copy)] +#[allow(dead_code)] pub enum TimeInForce { - /// Remains active until cancelled or fully filled. - GTC, - /// Executes immediately; any unfilled remainder is cancelled. - IOC, - /// Must fill in full immediately or the entire order is cancelled. - FOK, - /// Expires 24 hours after submission. - DAY, + GTC, // Good Till Cancelled + IOC, // Immediate Or Cancel + FOK, // Fill Or Kill + DAY, // Day Order } #[derive(Debug, Clone, Copy)] @@ -20,6 +18,7 @@ pub enum OrderSide { } #[derive(Debug, Clone, Copy)] +#[allow(dead_code)] pub enum OrderStatus { Open, PartiallyFilled, @@ -28,12 +27,14 @@ pub enum OrderStatus { } #[derive(Debug, Clone, Copy)] +#[allow(dead_code)] pub enum OrderType { Limit, Market, } #[derive(Debug, Clone)] +#[allow(dead_code)] pub struct Order { pub id: Uuid, pub item_id: Uuid, @@ -41,9 +42,9 @@ pub struct Order { pub order_side: OrderSide, pub order_type: OrderType, pub time_in_force: TimeInForce, - pub price: f32, - pub quantity: f32, - pub quantity_filled: f32, + pub price: Decimal, + pub quantity: Decimal, + pub quantity_filled: Decimal, pub status: OrderStatus, pub created_at: DateTime, pub updated_at: DateTime, @@ -51,22 +52,24 @@ pub struct Order { } #[derive(Debug, Clone)] +#[allow(dead_code)] pub struct Trade { pub id: Uuid, pub buy_order_id: Uuid, pub sell_order_id: Uuid, pub item_id: Uuid, - pub quantity: f32, - pub price: f32, + pub quantity: Decimal, + pub price: Decimal, pub timestamp: chrono::DateTime, } +#[allow(dead_code)] pub struct CreateOrderRequest { pub item_id: Uuid, pub user_id: Uuid, pub order_side: OrderSide, pub order_type: OrderType, - pub price: f32, - pub quantity: f32, + pub price: Decimal, + pub quantity: Decimal, pub time_in_force: TimeInForce, } diff --git a/src/components/services.rs b/src/components/services.rs index cbd4478..27910bd 100644 --- a/src/components/services.rs +++ b/src/components/services.rs @@ -1,20 +1,20 @@ use std::{ cmp::min, collections::{BTreeMap, HashMap, VecDeque}, + str::FromStr, }; use crate::components::dto::{ CreateOrderRequest, Order, OrderSide, OrderStatus, OrderType, TimeInForce, Trade, }; -use chrono::Utc; -use ordered_float::OrderedFloat; +use chrono::{DateTime, Utc}; +use rust_decimal::Decimal; use uuid::Uuid; pub struct OrderBookService { orders: HashMap, - buy_orders: HashMap, VecDeque>>, - sell_orders: HashMap, VecDeque>>, - /// All trades executed since the service was created. Appended to on each `add_order` call. + buy_orders: HashMap>>, + sell_orders: HashMap>>, pub trades: Vec, } @@ -29,11 +29,11 @@ impl OrderBookService { } pub fn add_order(&mut self, create_order_request: CreateOrderRequest) -> Result { - if create_order_request.price < 0.0 { + if create_order_request.price < Decimal::ZERO { return Err("Price cannot be negative".to_string()); } - if create_order_request.quantity <= 0.0 { + if create_order_request.quantity <= Decimal::ZERO { return Err("Quantity must be greater than zero".to_string()); } @@ -51,7 +51,7 @@ impl OrderBookService { order_type: create_order_request.order_type, price: create_order_request.price, quantity: create_order_request.quantity, - quantity_filled: 0.0, + quantity_filled: Decimal::ZERO, time_in_force: create_order_request.time_in_force, status: OrderStatus::Open, created_at: Utc::now(), @@ -65,10 +65,10 @@ impl OrderBookService { let price_difference = match order.order_side { OrderSide::Buy if market_price > order.price => market_price - order.price, OrderSide::Sell if market_price < order.price => order.price - market_price, - _ => 0.0, + _ => Decimal::ZERO, }; - if price_difference > (order.price * 0.05) { + if price_difference > (order.price * Decimal::from_str("0.05").unwrap()) { return Err(format!( "Market order price cannot be more than 5% away from the current market price. Current market price: {}, Order price: {}", market_price, order.price @@ -97,17 +97,17 @@ impl OrderBookService { self.buy_orders .entry(updated_order.item_id) .or_default() - .entry(OrderedFloat(updated_order.price)) + .entry(updated_order.price) .or_default() - .push_back(updated_order.clone()); + .push_back(updated_order.id); } OrderSide::Sell => { self.sell_orders .entry(updated_order.item_id) .or_default() - .entry(OrderedFloat(updated_order.price)) + .entry(updated_order.price) .or_default() - .push_back(updated_order.clone()); + .push_back(updated_order.id); } } } @@ -119,22 +119,30 @@ impl OrderBookService { &self.orders } - pub fn get_current_market_price(&self, item_id: Uuid, order_side: OrderSide) -> Option { + fn is_expired(&self, expires_at: Option>) -> bool { + match expires_at { + Some(expiry) => { + let now = Utc::now(); + expiry < now + } + None => false, + } + } + + pub fn get_current_market_price( + &self, + item_id: Uuid, + order_side: OrderSide, + ) -> Option { let price_map = match order_side { OrderSide::Buy => self.sell_orders.get(&item_id)?, OrderSide::Sell => self.buy_orders.get(&item_id)?, }; match order_side { - OrderSide::Buy => price_map - .iter() - .next() - .map(|(ordered_price, _)| ordered_price.0), - - OrderSide::Sell => price_map - .iter() - .next_back() - .map(|(ordered_price, _)| ordered_price.0), + OrderSide::Buy => price_map.iter().next().map(|(price, _)| *price), + + OrderSide::Sell => price_map.iter().next_back().map(|(price, _)| *price), } } @@ -164,13 +172,18 @@ impl OrderBookService { if let Some(order) = self.get_mutable_order_by_id(order_id) { order.status = OrderStatus::Cancelled; order.updated_at = Utc::now(); + self.remove_from_book(order_id); true } else { false } } - pub fn update_order_quantity(&mut self, order_id: Uuid, new_quantity: f32) -> Option<&Order> { + pub fn update_order_quantity( + &mut self, + order_id: Uuid, + new_quantity: Decimal, + ) -> Option<&Order> { if let Some(order) = self.orders.get_mut(&order_id) { order.quantity = new_quantity; order.updated_at = Utc::now(); @@ -180,7 +193,7 @@ impl OrderBookService { } } - pub fn update_order_price(&mut self, order_id: Uuid, new_price: f32) -> Option<&Order> { + pub fn update_order_price(&mut self, order_id: Uuid, new_price: Decimal) -> Option<&Order> { if let Some(order) = self.get_mutable_order_by_id(order_id) { order.price = new_price; order.updated_at = Utc::now(); @@ -197,7 +210,7 @@ impl OrderBookService { }; let item_id = order.item_id; - let price = OrderedFloat(order.price); + let price = order.price; let side = order.order_side; let book = match side { @@ -207,7 +220,7 @@ impl OrderBookService { if let Some(price_map) = book.get_mut(&item_id) { if let Some(order_queue) = price_map.get_mut(&price) { - order_queue.retain(|o| o.id != order_id); + order_queue.retain(|order_id_from_queue| *order_id_from_queue != order_id); if order_queue.is_empty() { price_map.remove(&price); @@ -220,22 +233,25 @@ impl OrderBookService { } } - fn fill_order(&mut self, order_id: Uuid, quantity_filled: f32) -> Option<&mut Order> { - if let Some(order) = self.get_mutable_order_by_id(order_id) { + fn fill_order(&mut self, order_id: Uuid, quantity_filled: Decimal) -> Option<&mut Order> { + let is_fully_filled = if let Some(order) = self.get_mutable_order_by_id(order_id) { order.quantity_filled += quantity_filled; if order.quantity_filled >= order.quantity { order.status = OrderStatus::Closed; + true } else { order.status = OrderStatus::PartiallyFilled; + false } - - order.updated_at = Utc::now(); - order.quantity_filled >= order.quantity } else { return None; }; + if is_fully_filled { + self.remove_from_book(order_id); + } + self.get_mutable_order_by_id(order_id) } @@ -248,11 +264,9 @@ impl OrderBookService { } pub fn execute_order_matching(&mut self, incoming_order: &mut Order) { - let mut trades: Vec = Vec::new(); - let order_book_side = match incoming_order.order_side { - OrderSide::Buy => self.sell_orders.clone(), - OrderSide::Sell => self.buy_orders.clone(), + OrderSide::Buy => &self.sell_orders, + OrderSide::Sell => &self.buy_orders, }; let price_maps = match order_book_side.get(&incoming_order.item_id) { @@ -262,91 +276,86 @@ impl OrderBookService { } }; - let prices: Vec> = match incoming_order.order_side { + let prices: Vec = match incoming_order.order_side { OrderSide::Buy => price_maps.keys().cloned().collect(), OrderSide::Sell => price_maps.keys().cloned().rev().collect(), }; - let mut matched_trade_list: HashMap = HashMap::new(); - let mut staged_order_to_fill: HashMap = HashMap::new(); + let mut queue_orders: Vec<(Decimal, Uuid)> = Vec::new(); - for price in &prices { - let order_queue = &price_maps[price]; - - for resting_order in order_queue { - let resting_order = self.get_order_by_id(resting_order.id); + for price in prices { + let order_queue = price_maps.get(&price); + for order_id in order_queue.unwrap() { + queue_orders.push((price, *order_id)); + } + } - if !resting_order.is_some() { - break; - } + let mut trades: Vec = Vec::new(); + let mut staged_order_to_fill: HashMap = HashMap::new(); - let resting_order = resting_order.unwrap(); - let resting_order_snapshot = resting_order.clone(); + for (price, order_id) in queue_orders { + let resting_order = self.get_order_by_id(order_id); - let is_match = self.can_match_price(incoming_order, resting_order); + if !resting_order.is_some() { + continue; + } - if !is_match { - break; - } + let resting_order = resting_order.unwrap(); - let available_quantity = resting_order.quantity - resting_order.quantity_filled; - if available_quantity <= 0.0 { - break; - } + if self.is_expired(resting_order.expires_at) + && matches!(resting_order.time_in_force, TimeInForce::DAY) + { + self.remove_from_book(resting_order.id); + continue; + } - let quantity_to_match = incoming_order.quantity - incoming_order.quantity_filled; - let trade_quantity = min( - OrderedFloat(available_quantity), - OrderedFloat(quantity_to_match), - ) - .into_inner(); + if !self.can_match_price(incoming_order, resting_order) { + break; + } - if trade_quantity <= 0.0 { - break; - } + let available_quantity = resting_order.quantity - resting_order.quantity_filled; + if available_quantity <= Decimal::ZERO { + continue; + } - let trade_id = Uuid::new_v4(); - let trade_index = trades.len(); - - trades.push(Trade { - id: trade_id, - buy_order_id: if matches!(incoming_order.order_side, OrderSide::Buy) { - incoming_order.id - } else { - resting_order.id - }, - sell_order_id: if matches!(incoming_order.order_side, OrderSide::Sell) { - incoming_order.id - } else { - resting_order.id - }, - item_id: incoming_order.item_id, - quantity: trade_quantity, - price: price.into_inner(), - timestamp: Utc::now(), - }); - - matched_trade_list.insert(trade_index, resting_order_snapshot); - - staged_order_to_fill - .entry(resting_order.id) - .and_modify(|q| *q += trade_quantity) - .or_insert(trade_quantity); - - staged_order_to_fill - .entry(incoming_order.id) - .and_modify(|q| *q += trade_quantity) - .or_insert(trade_quantity); - - if let Some(order) = self.get_order_by_id(incoming_order.id) { - let mut quantity_filled = order.quantity_filled.clone(); - quantity_filled += trade_quantity; - incoming_order.quantity_filled = quantity_filled; - } + let quantity_to_match = incoming_order.quantity - incoming_order.quantity_filled; + let trade_quantity = min(available_quantity, quantity_to_match); + + let trade_id: Uuid = Uuid::new_v4(); + + trades.push(Trade { + id: trade_id, + buy_order_id: if matches!(incoming_order.order_side, OrderSide::Buy) { + incoming_order.id + } else { + resting_order.id + }, + sell_order_id: if matches!(incoming_order.order_side, OrderSide::Sell) { + incoming_order.id + } else { + resting_order.id + }, + item_id: incoming_order.item_id, + quantity: trade_quantity, + price, + timestamp: Utc::now(), + }); + + *staged_order_to_fill + .entry(resting_order.id) + .or_insert(Decimal::ZERO) += trade_quantity; + *staged_order_to_fill + .entry(incoming_order.id) + .or_insert(Decimal::ZERO) += trade_quantity; + + incoming_order.quantity_filled += trade_quantity; + + if incoming_order.quantity_filled == incoming_order.quantity { + break; } } - let mut performed_reversal = false; + let mut unstaged_matched_orders = false; if trades.len() > 0 && matches!(incoming_order.time_in_force, TimeInForce::IOC) { self.update_order_quantity(incoming_order.id, incoming_order.quantity_filled); @@ -357,16 +366,13 @@ impl OrderBookService { && incoming_order.quantity_filled != incoming_order.quantity { self.cancel_order(incoming_order.id); - performed_reversal = true; + unstaged_matched_orders = true; } - if !performed_reversal { + if !unstaged_matched_orders { for (order_id, trade_quantity) in staged_order_to_fill { self.fill_order(order_id, trade_quantity); } - for (_, order) in matched_trade_list { - self.remove_from_book(order.id); - } self.trades.append(&mut trades); } @@ -375,3 +381,4 @@ impl OrderBookService { } } } + diff --git a/src/components/services_test.rs b/src/components/services_test.rs index 0972885..d1605ca 100644 --- a/src/components/services_test.rs +++ b/src/components/services_test.rs @@ -4,6 +4,8 @@ mod tests { dto::{CreateOrderRequest, OrderSide, OrderStatus, OrderType, TimeInForce}, services::OrderBookService, }; + use rust_decimal::Decimal; + use std::str::FromStr; use uuid::Uuid; #[test] @@ -14,12 +16,12 @@ mod tests { user_id: Uuid::new_v4(), order_side: OrderSide::Buy, order_type: OrderType::Limit, - price: 10.0, + price: Decimal::from_str("10.0").unwrap(), time_in_force: TimeInForce::DAY, - quantity: 100.0, + quantity: Decimal::from_str("100.0").unwrap(), }; let order = order_book.add_order(create_order_request).unwrap(); - assert_eq!(order.quantity, 100.0); + assert_eq!(order.quantity, Decimal::from_str("100.0").unwrap()); assert_eq!(matches!(order.order_side, OrderSide::Buy), true); assert_eq!(matches!(order.status, OrderStatus::Open), true); } @@ -33,8 +35,8 @@ mod tests { order_side: OrderSide::Sell, order_type: OrderType::Limit, time_in_force: TimeInForce::DAY, - price: 20.0, - quantity: 50.0, + price: Decimal::from_str("20.0").unwrap(), + quantity: Decimal::from_str("50.0").unwrap(), }; let order = order_book.add_order(create_order_request).unwrap(); let fetched_order = order_book.get_order_by_id(order.id); @@ -51,8 +53,8 @@ mod tests { order_side: OrderSide::Buy, order_type: OrderType::Limit, time_in_force: TimeInForce::DAY, - price: 15.0, - quantity: 100.0, + price: Decimal::from_str("15.0").unwrap(), + quantity: Decimal::from_str("100.0").unwrap(), }; let order = order_book.add_order(create_order_request).unwrap(); let updated_order = order_book.update_order_status(order.id, OrderStatus::Closed); @@ -72,13 +74,17 @@ mod tests { order_side: OrderSide::Sell, order_type: OrderType::Limit, time_in_force: TimeInForce::DAY, - price: 25.0, - quantity: 50.0, + price: Decimal::from_str("25.0").unwrap(), + quantity: Decimal::from_str("50.0").unwrap(), }; let order = order_book.add_order(create_order_request).unwrap(); - let updated_order = order_book.update_order_quantity(order.id, 75.0); + let updated_order = + order_book.update_order_quantity(order.id, Decimal::from_str("75.0").unwrap()); assert!(updated_order.is_some()); - assert_eq!(updated_order.unwrap().quantity, 75.0); + assert_eq!( + updated_order.unwrap().quantity, + Decimal::from_str("75.0").unwrap() + ); } #[test] @@ -90,8 +96,8 @@ mod tests { order_side: OrderSide::Buy, order_type: OrderType::Limit, time_in_force: TimeInForce::DAY, - price: 30.0, - quantity: 100.0, + price: Decimal::from_str("30.0").unwrap(), + quantity: Decimal::from_str("100.0").unwrap(), }; let order = order_book.add_order(create_order_request).unwrap(); order_book.cancel_order(order.id); @@ -113,8 +119,8 @@ mod tests { order_side: OrderSide::Buy, order_type: OrderType::Limit, time_in_force: TimeInForce::DAY, - price: 10.0, - quantity: 100.0, + price: Decimal::from_str("10.0").unwrap(), + quantity: Decimal::from_str("100.0").unwrap(), }; let buy_order = order_book.add_order(buy_order_request).unwrap(); @@ -124,16 +130,22 @@ mod tests { order_side: OrderSide::Sell, order_type: OrderType::Limit, time_in_force: TimeInForce::DAY, - price: 10.0, - quantity: 50.0, + price: Decimal::from_str("10.0").unwrap(), + quantity: Decimal::from_str("50.0").unwrap(), }; let sell_order = order_book.add_order(sell_order_request).unwrap(); let fetched_buy_order = order_book.get_order_by_id(buy_order.id).unwrap(); let fetched_sell_order = order_book.get_order_by_id(sell_order.id).unwrap(); - assert_eq!(fetched_buy_order.quantity_filled, 50.0); - assert_eq!(fetched_sell_order.quantity_filled, 50.0); + assert_eq!( + fetched_buy_order.quantity_filled, + Decimal::from_str("50.0").unwrap() + ); + assert_eq!( + fetched_sell_order.quantity_filled, + Decimal::from_str("50.0").unwrap() + ); assert_eq!( matches!(fetched_buy_order.status, OrderStatus::PartiallyFilled), true @@ -155,8 +167,8 @@ mod tests { order_side: OrderSide::Buy, order_type: OrderType::Limit, time_in_force: TimeInForce::DAY, - price: 10.0, - quantity: 100.0, + price: Decimal::from_str("10.0").unwrap(), + quantity: Decimal::from_str("100.0").unwrap(), }; let buy_order = order_book.add_order(buy_order_request).unwrap(); @@ -166,16 +178,22 @@ mod tests { order_side: OrderSide::Sell, order_type: OrderType::Limit, time_in_force: TimeInForce::DAY, - price: 10.0, - quantity: 100.0, + price: Decimal::from_str("10.0").unwrap(), + quantity: Decimal::from_str("100.0").unwrap(), }; let sell_order = order_book.add_order(sell_order_request).unwrap(); let fetched_buy_order = order_book.get_order_by_id(buy_order.id).unwrap(); let fetched_sell_order = order_book.get_order_by_id(sell_order.id).unwrap(); - assert_eq!(fetched_buy_order.quantity_filled, 100.0); - assert_eq!(fetched_sell_order.quantity_filled, 100.0); + assert_eq!( + fetched_buy_order.quantity_filled, + Decimal::from_str("100.0").unwrap() + ); + assert_eq!( + fetched_sell_order.quantity_filled, + Decimal::from_str("100.0").unwrap() + ); assert_eq!( matches!(fetched_buy_order.status, OrderStatus::Closed), true @@ -195,13 +213,17 @@ mod tests { order_side: OrderSide::Buy, order_type: OrderType::Limit, time_in_force: TimeInForce::DAY, - price: 10.0, - quantity: 100.0, + price: Decimal::from_str("10.0").unwrap(), + quantity: Decimal::from_str("100.0").unwrap(), }; let order = order_book.add_order(create_order_request).unwrap(); - let updated_order = order_book.update_order_price(order.id, 15.0); + let updated_order = + order_book.update_order_price(order.id, Decimal::from_str("15.0").unwrap()); assert!(updated_order.is_some()); - assert_eq!(updated_order.unwrap().price, 15.0); + assert_eq!( + updated_order.unwrap().price, + Decimal::from_str("15.0").unwrap() + ); } #[test] @@ -215,8 +237,8 @@ mod tests { order_side: OrderSide::Buy, order_type: OrderType::Limit, time_in_force: TimeInForce::DAY, - price: 10.0, - quantity: 100.0, + price: Decimal::from_str("10.0").unwrap(), + quantity: Decimal::from_str("100.0").unwrap(), }; let buy_order = order_book.add_order(buy_order_request).unwrap(); @@ -226,8 +248,8 @@ mod tests { order_side: OrderSide::Sell, order_type: OrderType::Limit, time_in_force: TimeInForce::DAY, - price: 10.0, - quantity: 50.0, + price: Decimal::from_str("10.0").unwrap(), + quantity: Decimal::from_str("50.0").unwrap(), }; let sell_order = order_book.add_order(sell_order_request).unwrap(); @@ -246,17 +268,25 @@ mod tests { order_side: OrderSide::Sell, order_type: OrderType::Limit, time_in_force: TimeInForce::DAY, - price: 20.0, - quantity: 50.0, + price: Decimal::from_str("20.0").unwrap(), + quantity: Decimal::from_str("50.0").unwrap(), }; let order = order_book.add_order(create_order_request).unwrap(); - let updated_order = order_book.update_order_quantity(order.id, 75.0); + let updated_order = + order_book.update_order_quantity(order.id, Decimal::from_str("75.0").unwrap()); assert!(updated_order.is_some()); - assert_eq!(updated_order.unwrap().quantity, 75.0); + assert_eq!( + updated_order.unwrap().quantity, + Decimal::from_str("75.0").unwrap() + ); - let updated_order_price = order_book.update_order_price(order.id, 25.0); + let updated_order_price = + order_book.update_order_price(order.id, Decimal::from_str("25.0").unwrap()); assert!(updated_order_price.is_some()); - assert_eq!(updated_order_price.unwrap().price, 25.0); + assert_eq!( + updated_order_price.unwrap().price, + Decimal::from_str("25.0").unwrap() + ); } #[test] @@ -269,8 +299,8 @@ mod tests { order_side: OrderSide::Buy, order_type: OrderType::Limit, time_in_force: TimeInForce::DAY, - price: 10.0, - quantity: 100.0, + price: Decimal::from_str("10.0").unwrap(), + quantity: Decimal::from_str("100.0").unwrap(), }; let buy_order = order_book.add_order(buy_order_request).unwrap(); @@ -280,16 +310,16 @@ mod tests { order_side: OrderSide::Sell, order_type: OrderType::Limit, time_in_force: TimeInForce::DAY, - price: 15.0, - quantity: 50.0, + price: Decimal::from_str("15.0").unwrap(), + quantity: Decimal::from_str("50.0").unwrap(), }; let sell_order = order_book.add_order(sell_order_request).unwrap(); let fetched_buy_order = order_book.get_order_by_id(buy_order.id).unwrap(); let fetched_sell_order = order_book.get_order_by_id(sell_order.id).unwrap(); - assert_eq!(fetched_buy_order.quantity_filled, 0.0); - assert_eq!(fetched_sell_order.quantity_filled, 0.0); + assert_eq!(fetched_buy_order.quantity_filled, Decimal::ZERO); + assert_eq!(fetched_sell_order.quantity_filled, Decimal::ZERO); assert_eq!(matches!(fetched_buy_order.status, OrderStatus::Open), true); assert_eq!(matches!(fetched_sell_order.status, OrderStatus::Open), true); } @@ -303,8 +333,8 @@ mod tests { order_side: OrderSide::Buy, order_type: OrderType::Market, time_in_force: TimeInForce::DAY, - price: 0.0, - quantity: 100.0, + price: Decimal::ZERO, + quantity: Decimal::from_str("100.0").unwrap(), }; let result = order_book.add_order(create_order_request); assert!(result.is_err()); @@ -325,8 +355,8 @@ mod tests { order_side: OrderSide::Sell, order_type: OrderType::Limit, time_in_force: TimeInForce::DAY, - price: 10.0, - quantity: 50.0, + price: Decimal::from_str("10.0").unwrap(), + quantity: Decimal::from_str("50.0").unwrap(), }; let _ = order_book.add_order(sell_order_request); let current_market_price = order_book @@ -340,12 +370,15 @@ mod tests { order_type: OrderType::Market, time_in_force: TimeInForce::DAY, price: current_market_price, - quantity: 50.0, + quantity: Decimal::from_str("50.0").unwrap(), }; let buy_market_order = order_book.add_order(buy_market_order_request).unwrap(); - assert_eq!(buy_market_order.price, 10.0); - assert_eq!(buy_market_order.quantity_filled, 50.0); + assert_eq!(buy_market_order.price, Decimal::from_str("10.0").unwrap()); + assert_eq!( + buy_market_order.quantity_filled, + Decimal::from_str("50.0").unwrap() + ); } #[test] @@ -359,8 +392,8 @@ mod tests { order_side: OrderSide::Sell, order_type: OrderType::Limit, time_in_force: TimeInForce::DAY, - price: 10.0, - quantity: 50.0, + price: Decimal::from_str("10.0").unwrap(), + quantity: Decimal::from_str("50.0").unwrap(), }; let _ = order_book.add_order(sell_order_request); @@ -370,12 +403,15 @@ mod tests { order_side: OrderSide::Buy, order_type: OrderType::Limit, time_in_force: TimeInForce::IOC, - price: 10.0, - quantity: 100.0, + price: Decimal::from_str("10.0").unwrap(), + quantity: Decimal::from_str("100.0").unwrap(), }; let buy_ioc_order = order_book.add_order(buy_ioc_order_request).unwrap(); - assert_eq!(buy_ioc_order.quantity_filled, 50.0); - assert_eq!(buy_ioc_order.quantity, 50.0); + assert_eq!( + buy_ioc_order.quantity_filled, + Decimal::from_str("50.0").unwrap() + ); + assert_eq!(buy_ioc_order.quantity, Decimal::from_str("50.0").unwrap()); assert_eq!(matches!(buy_ioc_order.status, OrderStatus::Closed), true); } @@ -390,8 +426,8 @@ mod tests { order_side: OrderSide::Sell, order_type: OrderType::Limit, time_in_force: TimeInForce::DAY, - price: 30.0, - quantity: 50.0, + price: Decimal::from_str("30.0").unwrap(), + quantity: Decimal::from_str("50.0").unwrap(), }; let _ = order_book.add_order(sell_order_request); @@ -401,14 +437,15 @@ mod tests { order_side: OrderSide::Buy, order_type: OrderType::Market, time_in_force: TimeInForce::DAY, - price: 20.0, // Market price is too far from the current market price - quantity: 50.0, + price: Decimal::from_str("20.0").unwrap(), + quantity: Decimal::from_str("50.0").unwrap(), }; let result = order_book.add_order(buy_market_order_request); assert!(result.is_err()); - assert_eq!( - result.err().unwrap(), - "Market order price cannot be more than 5% away from the current market price. Current market price: 30, Order price: 20" - ); + + let err_msg = result.err().unwrap(); + assert!(err_msg.contains("Market order price cannot be more than 5% away")); + assert!(err_msg.contains("30")); + assert!(err_msg.contains("20")); } }