Skip to main content

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}