Skip to main content

rustrade_data/subscription/
quote.rs

1use super::SubscriptionKind;
2use rust_decimal::Decimal;
3use rustrade_macro::{DeSubKind, SerSubKind};
4use serde::{Deserialize, Serialize};
5
6/// 10,000 as a Decimal constant for basis point calculations.
7const BPS_FACTOR: Decimal = Decimal::from_parts(10_000, 0, 0, false, 0);
8
9/// Barter [`Subscription`](super::Subscription) [`SubscriptionKind`] that yields [`Quote`]
10/// [`MarketEvent<T>`](crate::event::MarketEvent) events.
11///
12/// Represents real-time best bid/ask quotes (NBBO for equities, top-of-book for crypto).
13#[derive(
14    Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, DeSubKind, SerSubKind,
15)]
16pub struct Quotes;
17
18impl SubscriptionKind for Quotes {
19    type Event = Quote;
20
21    fn as_str(&self) -> &'static str {
22        "quotes"
23    }
24}
25
26impl std::fmt::Display for Quotes {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        write!(f, "{}", self.as_str())
29    }
30}
31
32/// Normalised Barter [`Quote`] model representing best bid/ask.
33#[derive(Copy, Clone, PartialEq, PartialOrd, Debug, Deserialize, Serialize)]
34pub struct Quote {
35    pub bid_price: Decimal,
36    pub bid_amount: Decimal,
37    pub ask_price: Decimal,
38    pub ask_amount: Decimal,
39}
40
41impl Quote {
42    /// Calculate the mid-price as the average of bid and ask prices.
43    pub fn mid_price(&self) -> Decimal {
44        (self.bid_price + self.ask_price) / Decimal::TWO
45    }
46
47    /// Calculate the spread (ask - bid).
48    pub fn spread(&self) -> Decimal {
49        self.ask_price - self.bid_price
50    }
51
52    /// Calculate the spread in basis points (1 bps = 0.01%) relative to the mid-price.
53    /// Returns zero if mid-price is zero.
54    pub fn spread_bps(&self) -> Decimal {
55        let mid = self.mid_price();
56        if mid.is_zero() {
57            Decimal::ZERO
58        } else {
59            (self.spread() / mid) * BPS_FACTOR
60        }
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67    use rust_decimal_macros::dec;
68
69    #[test]
70    fn test_quote_mid_price() {
71        let quote = Quote {
72            bid_price: dec!(100),
73            bid_amount: dec!(10),
74            ask_price: dec!(102),
75            ask_amount: dec!(5),
76        };
77        assert_eq!(quote.mid_price(), dec!(101));
78    }
79
80    #[test]
81    fn test_quote_spread() {
82        let quote = Quote {
83            bid_price: dec!(100),
84            bid_amount: dec!(10),
85            ask_price: dec!(102),
86            ask_amount: dec!(5),
87        };
88        assert_eq!(quote.spread(), dec!(2));
89    }
90
91    #[test]
92    fn test_quote_spread_bps() {
93        let quote = Quote {
94            bid_price: dec!(100),
95            bid_amount: dec!(10),
96            ask_price: dec!(101),
97            ask_amount: dec!(5),
98        };
99        // Spread = 1, mid = 100.5, spread_bps = 1/100.5 * 10000 ≈ 99.50248...
100        let bps = quote.spread_bps();
101        assert!(bps > dec!(99) && bps < dec!(100), "spread_bps={bps}");
102    }
103}