clob_sync/order.rs
1use fastnum::D128 as Dec;
2use fastnum::decimal::Context;
3use std::convert::TryFrom;
4use std::fmt::Debug;
5use std::sync::LazyLock;
6
7use crate::error::Error;
8use derive_more::Display;
9use uuid7::uuid7;
10
11use ustr::Ustr;
12
13static DECIMAL_CONTEXT: LazyLock<Context> = LazyLock::new(Context::default);
14
15/// A trading order in the Central Limit Order Book.
16///
17/// # Example
18///
19/// ```
20/// use clob_sync::order::{Order, OrderType, OrderSide, Quantity, Price, Symbol};
21/// use fastnum::D128 as Dec;
22/// use fastnum::decimal::Context;
23/// use std::str::FromStr;
24///
25/// let order = Order::new(
26/// OrderType::Limit(Price::try_from("50000.50").unwrap()),
27/// Quantity::try_from("1.5").unwrap(),
28/// OrderSide::Buy,
29/// Symbol::from("BTC-USD"),
30/// );
31/// ```
32#[derive(PartialEq, Eq, Debug, Clone)]
33pub struct Order {
34 pub id: Option<OrderId>,
35 pub order_type: OrderType,
36 pub quantity: Quantity,
37 pub side: OrderSide,
38 pub symbol: Symbol,
39}
40impl Order {
41 /// Creates a new order without an ID. The order must be assigned an ID
42 /// before it can be executed.
43 ///
44 /// # Arguments
45 ///
46 /// * `order_type` - The type of order (Limit or Market)
47 /// * `quantity` - The quantity to trade
48 /// * `side` - Whether this is a Buy or Sell order
49 /// * `symbol` - The trading symbol (e.g., "BTC-USD")
50 ///
51 /// # Example
52 ///
53 /// ```
54 /// use clob_sync::order::{Order, OrderType, OrderSide, Quantity, Price, Symbol};
55 /// use fastnum::D128 as Dec;
56 /// use fastnum::decimal::Context;
57 /// use std::str::FromStr;
58 ///
59 /// let order = Order::new(
60 /// OrderType::Limit(Price::try_from("50000.00").unwrap()),
61 /// Quantity::from(1u64),
62 /// OrderSide::Buy,
63 /// Symbol::from("BTC-USD"),
64 /// );
65 /// assert!(order.id.is_none());
66 /// ```
67 pub fn new(
68 order_type: OrderType,
69 quantity: Quantity,
70 side: OrderSide,
71 symbol: Symbol,
72 ) -> Order {
73 Order {
74 id: None,
75 order_type,
76 quantity,
77 side,
78 symbol,
79 }
80 }
81
82 /// Assigns a time-based UUIDv7 order ID to this order.
83 ///
84 /// UUIDv7 guarantees monotonic ordering within the same millisecond,
85 /// making it suitable for order priority determination.
86 ///
87 /// # Example
88 ///
89 /// ```
90 /// use clob_sync::order::{Order, OrderType, OrderSide, Quantity, Price, Symbol};
91 /// use fastnum::D128 as Dec;
92 /// use fastnum::decimal::Context;
93 /// use std::str::FromStr;
94 ///
95 /// let mut order = Order::new(
96 /// OrderType::Limit(Price::try_from("50000.00").unwrap()),
97 /// Quantity::from(1u64),
98 /// OrderSide::Buy,
99 /// Symbol::from("BTC-USD"),
100 /// );
101 /// order = order.with_id();
102 /// assert!(order.id.is_some());
103 /// ```
104 pub fn with_id(mut self) -> Self {
105 self.id = Some(OrderId::default());
106 self
107 }
108
109 /// Returns the order ID.
110 ///
111 /// # Panics
112 ///
113 /// Panics if the order has not been assigned an ID via [`with_id`][Order::with_id].
114 ///
115 /// # Example
116 ///
117 /// ```
118 /// use clob_sync::order::{Order, OrderType, OrderSide, Quantity, Price, Symbol};
119 /// use fastnum::D128 as Dec;
120 /// use fastnum::decimal::Context;
121 /// use std::str::FromStr;
122 ///
123 /// let order = Order::new(
124 /// OrderType::Limit(Price::try_from("50000.00").unwrap()),
125 /// Quantity::from(1u64),
126 /// OrderSide::Buy,
127 /// Symbol::from("BTC-USD"),
128 /// ).with_id();
129 ///
130 /// let _id = order.get_id();
131 /// ```
132 pub fn get_id(&self) -> OrderId {
133 self.id
134 .expect("unexpected condition: order not marked with id")
135 }
136
137 /// Creates a new order with the quantity reduced by the specified amount.
138 ///
139 /// This is useful when an order is partially filled and the remaining
140 /// quantity needs to be tracked.
141 ///
142 /// # Arguments
143 ///
144 /// * `quantity` - The quantity to subtract from this order's quantity
145 ///
146 /// # Example
147 ///
148 /// ```
149 /// use clob_sync::order::{Order, OrderType, OrderSide, Quantity, Price, Symbol};
150 /// use fastnum::D128 as Dec;
151 /// use fastnum::decimal::Context;
152 /// use std::str::FromStr;
153 ///
154 /// let order = Order::new(
155 /// OrderType::Limit(Price::try_from("50000.00").unwrap()),
156 /// Quantity::try_from("10.0").unwrap(),
157 /// OrderSide::Buy,
158 /// Symbol::from("BTC-USD"),
159 /// );
160 ///
161 /// let remaining = order.with_reduced_quantity(&Quantity::try_from("3.0").unwrap());
162 /// assert_eq!(remaining.quantity, Quantity::try_from("7.0").unwrap());
163 /// ```
164 pub fn with_reduced_quantity(&self, quantity: &Quantity) -> Order {
165 Order {
166 quantity: self.quantity.reduced_by(quantity),
167 ..self.clone()
168 }
169 }
170}
171
172/// A time-based UUIDv7 order identifier.
173///
174/// UUIDv7 provides monotonic ordering within the same millisecond,
175/// ensuring deterministic order prioritization in the order book.
176/// The internal UUID is sortable and suitable for trading applications.
177#[derive(PartialEq, Eq, PartialOrd, Ord, Display, Clone, Copy)]
178// #[display("{}", 0)]
179pub struct OrderId(
180 // time orderable v7, see https://www.ietf.org/rfc/rfc9562.html#name-uuid-version-7
181 pub uuid7::Uuid,
182);
183impl Default for OrderId {
184 fn default() -> Self {
185 // uuid7 guarantees monotonic ordering within same millisecond (uuid does not)
186 Self(uuid7())
187 }
188}
189impl Debug for OrderId {
190 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191 write!(f, "OrderId({})", self.0)
192 }
193}
194
195/// A type-safe quantity wrapper for financial instruments.
196///
197/// # Example
198///
199/// ```
200/// use clob_sync::order::Quantity;
201///
202/// let qty = Quantity::try_from("100.5").unwrap();
203/// assert!(!qty.is_zero());
204/// ```
205#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Display, Copy, Clone)]
206pub struct Quantity(pub Dec);
207impl Quantity {
208 /// Creates a new Quantity from an f64 value.
209 pub fn new(value: f64) -> Quantity {
210 Quantity(Dec::from(value))
211 }
212
213 /// Returns a new Quantity reduced by the specified amount.
214 ///
215 /// # Example
216 ///
217 /// ```
218 /// use clob_sync::order::Quantity;
219 ///
220 /// let qty = Quantity::try_from("100.0").unwrap();
221 /// let reduced = qty.reduced_by(&Quantity::try_from("30.0").unwrap());
222 /// assert_eq!(reduced, Quantity::try_from("70.0").unwrap());
223 /// ```
224 pub fn reduced_by(&self, quantity: &Quantity) -> Quantity {
225 Quantity(self.0 - quantity.0)
226 }
227
228 /// Reduces this quantity in place by the specified amount.
229 ///
230 /// # Example
231 ///
232 /// ```
233 /// use clob_sync::order::Quantity;
234 ///
235 /// let mut qty = Quantity::try_from("100.0").unwrap();
236 /// qty.reduce_by(&Quantity::try_from("30.0").unwrap());
237 /// assert_eq!(qty, Quantity::try_from("70.0").unwrap());
238 /// ```
239 pub fn reduce_by(&mut self, quantity: &Quantity) {
240 self.0 -= quantity.0
241 }
242
243 /// Sets the quantity to zero.
244 ///
245 /// # Example
246 ///
247 /// ```
248 /// use clob_sync::order::Quantity;
249 ///
250 /// let mut qty = Quantity::new(100.0);
251 /// qty.clear();
252 /// assert!(qty.is_zero());
253 /// ```
254 pub fn clear(&mut self) {
255 self.0 = Dec::from(0i32)
256 }
257
258 /// Returns true if the quantity is zero.
259 pub fn is_zero(&self) -> bool {
260 self.0 == Dec::from(0i32)
261 }
262}
263impl From<u64> for Quantity {
264 fn from(value: u64) -> Self {
265 Quantity(Dec::from(value))
266 }
267}
268impl TryFrom<&str> for Quantity {
269 type Error = Error;
270 fn try_from(value: &str) -> Result<Self, Self::Error> {
271 Ok(Quantity(Dec::from_str(value, *DECIMAL_CONTEXT)?))
272 }
273}
274impl TryFrom<String> for Quantity {
275 type Error = Error;
276 fn try_from(value: String) -> Result<Self, Self::Error> {
277 Quantity::try_from(value.as_str())
278 }
279}
280impl From<f32> for Quantity {
281 fn from(value: f32) -> Self {
282 Quantity(Dec::from(value as f64))
283 }
284}
285impl From<usize> for Quantity {
286 fn from(value: usize) -> Self {
287 Quantity(Dec::from(value))
288 }
289}
290
291macro_rules! impl_quantity_from_integer {
292 ($($t:ty),*) => {
293 $(
294 impl From<$t> for Quantity {
295 fn from(value: $t) -> Self {
296 Quantity(Dec::try_from(value).expect(concat!("cannot convert ", stringify!($t), " to Dec19x19")))
297 }
298 }
299 )*
300 }
301}
302impl_quantity_from_integer!(u8, u16, u32, u128);
303
304/// A trading symbol (e.g., "BTC-USD", "AAPL").
305///
306/// Symbols are interned using [`Ustr`] for efficient comparison and storage.
307///
308/// # Example
309///
310/// ```
311/// use clob_sync::order::Symbol;
312///
313/// let symbol = Symbol::from("BTC-USD");
314/// assert_eq!(symbol, Symbol::from("BTC-USD"));
315/// ```
316#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Copy, Display)]
317pub struct Symbol(pub Ustr);
318impl From<&str> for Symbol {
319 fn from(value: &str) -> Self {
320 Self(Ustr::from(value))
321 }
322}
323
324/// The side of an order - Buy or Sell.
325///
326/// # Example
327///
328/// ```
329/// use clob_sync::order::OrderSide;
330///
331/// assert_eq!(*OrderSide::Buy.opposite(), OrderSide::Sell);
332/// assert_eq!(*OrderSide::Sell.opposite(), OrderSide::Buy);
333/// ```
334#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Copy, Clone, Display)]
335#[repr(u8)] // Explicit discriminants for predictable indexing (0 and 1)
336pub enum OrderSide {
337 /// A buy order (bid)
338 Buy = 0,
339 /// A sell order (ask)
340 Sell = 1,
341}
342
343impl OrderSide {
344 /// Returns the opposite side of this order.
345 ///
346 /// # Example
347 ///
348 /// ```
349 /// use clob_sync::order::OrderSide;
350 ///
351 /// assert_eq!(*OrderSide::Buy.opposite(), OrderSide::Sell);
352 /// ```
353 pub fn opposite(&self) -> &Self {
354 match self {
355 OrderSide::Buy => &OrderSide::Sell,
356 OrderSide::Sell => &Self::Buy,
357 }
358 }
359}
360
361/// A type-safe price wrapper for financial instruments.
362///
363/// Prices use high-precision Dec19x19 decimal representation to avoid
364/// floating-point inaccuracies in financial calculations.
365///
366/// # Example
367///
368/// ```
369/// use clob_sync::order::Price;
370/// use fastnum::D128 as Dec;
371/// use fastnum::decimal::Context;
372/// use std::str::FromStr;
373///
374/// let price = Price::try_from("50000.50").unwrap();
375/// assert_eq!(price.to_string(), "50000.50");
376/// ```
377/// A type-safe price wrapper for financial instruments.
378///
379/// Prices use high-precision Dec19x19 decimal representation to avoid
380/// floating-point inaccuracies in financial calculations.
381///
382/// # Example
383///
384/// ```
385/// use clob_sync::order::Price;
386/// use fastnum::D128 as Dec;
387/// use fastnum::decimal::Context;
388/// use std::str::FromStr;
389///
390/// let price = Price::try_from("50000.50").unwrap();
391/// assert_eq!(price.to_string(), "50000.50");
392/// ```
393#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Copy, Display)]
394pub struct Price(pub Dec);
395impl Price {
396 /// Creates a Price from an f64 value.
397 pub fn new(value: f64) -> Price {
398 Price(Dec::from(value))
399 }
400}
401macro_rules! impl_price_from_integer {
402 ($($t:ty),*) => {
403 $(
404 impl From<$t> for Price {
405 fn from(value: $t) -> Self {
406 Price(Dec::from(value))
407 }
408 }
409 )*
410 }
411}
412impl_price_from_integer!(u8, u16, u32, i8, i16, i32, i64);
413
414macro_rules! impl_price_from_large_integer {
415 ($($t:ty),*) => {
416 $(
417 impl From<$t> for Price {
418 fn from(value: $t) -> Self {
419 Price(Dec::try_from(value).expect(concat!("cannot convert ", stringify!($t), " to Dec19x19")))
420 }
421 }
422 )*
423 }
424}
425impl_price_from_large_integer!(u64, u128, i128);
426
427impl TryFrom<&str> for Price {
428 type Error = Error;
429
430 fn try_from(value: &str) -> Result<Self, Self::Error> {
431 Ok(Price(
432 Dec::from_str(value, *DECIMAL_CONTEXT)
433 .map_err(Error::DecimalParseError)?,
434 ))
435 }
436}
437
438impl TryFrom<String> for Price {
439 type Error = Error;
440
441 fn try_from(value: String) -> Result<Self, Self::Error> {
442 Price::try_from(value.as_str())
443 }
444}
445
446impl From<f32> for Price {
447 fn from(value: f32) -> Self {
448 Price(Dec::from(value as f64))
449 }
450}
451impl From<usize> for Price {
452 fn from(value: usize) -> Self {
453 Price(Dec::from(value))
454 }
455}
456
457/// The type of an order - Limit or Market.
458///
459/// # Example
460///
461/// ```
462/// use clob_sync::order::{OrderType, Price};
463/// use fastnum::D128 as Dec;
464/// use fastnum::decimal::Context;
465/// use std::str::FromStr;
466///
467/// let limit = OrderType::Limit(Price::try_from("50000.00").unwrap());
468/// let market = OrderType::Market;
469/// ```
470#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone)]
471pub enum OrderType {
472 /// A limit order with a specific price
473 Limit(Price),
474 /// A market order that executes immediately at the best available price
475 Market,
476}
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481
482 use insta::assert_debug_snapshot;
483
484 #[test]
485 fn test_price_from_str() {
486 assert_debug_snapshot!(Price::try_from("123.45").unwrap(), @r"
487 Price(
488 D128(12345e-2),
489 )
490 ");
491 assert_debug_snapshot!(Price::try_from(String::from("123.45")).unwrap(), @r"
492 Price(
493 D128(12345e-2),
494 )
495 ");
496 }
497
498 #[test]
499 fn test_price_from_number() {
500 assert_debug_snapshot!(Price::from(123), @r"
501 Price(
502 D128(123e0),
503 )
504 ");
505 assert_debug_snapshot!(Price::from(123_u64), @r"
506 Price(
507 D128(123e0),
508 )
509 ");
510 }
511}