1use crate::core::errors::{Result, RustyQLibError};
18
19#[derive(Debug, Clone, Copy, PartialEq)]
24pub enum Quote {
25 Mid(f64),
28 TopOfBook { bid: f64, ask: f64 },
30 Sized { bid: f64, bid_size: f64, ask: f64, ask_size: f64 },
33}
34
35fn require_uncrossed(bid: f64, ask: f64) -> Result<()> {
36 if !bid.is_finite() || !ask.is_finite() {
37 return Err(RustyQLibError::invalid_input(
38 "quote",
39 format!("bid/ask must be finite, got bid={bid}, ask={ask}"),
40 ));
41 }
42 if bid > ask {
43 return Err(RustyQLibError::invalid_input(
44 "quote",
45 format!("crossed quote: bid {bid} > ask {ask}"),
46 ));
47 }
48 Ok(())
49}
50
51impl Quote {
52 pub fn new(mid: f64) -> Self {
56 Quote::Mid(mid)
57 }
58
59 pub fn from_bid_ask(bid: f64, ask: f64) -> Result<Self> {
63 require_uncrossed(bid, ask)?;
64 Ok(Quote::TopOfBook { bid, ask })
65 }
66
67 pub fn from_bid_ask_sized(bid: f64, bid_size: f64, ask: f64, ask_size: f64) -> Result<Self> {
70 require_uncrossed(bid, ask)?;
71 if !(bid_size.is_finite() && ask_size.is_finite() && bid_size > 0.0 && ask_size > 0.0) {
72 return Err(RustyQLibError::invalid_input(
73 "quote",
74 format!("sizes must be finite and positive, got bid_size={bid_size}, ask_size={ask_size}"),
75 ));
76 }
77 Ok(Quote::Sized { bid, bid_size, ask, ask_size })
78 }
79
80 pub fn mid(&self) -> f64 {
83 match *self {
84 Quote::Mid(value) => value,
85 Quote::TopOfBook { bid, ask } | Quote::Sized { bid, ask, .. } => 0.5 * (bid + ask),
86 }
87 }
88
89 pub fn value(&self) -> f64 {
92 self.mid()
93 }
94
95 pub fn bid(&self) -> Option<f64> {
97 match *self {
98 Quote::Mid(_) => None,
99 Quote::TopOfBook { bid, .. } | Quote::Sized { bid, .. } => Some(bid),
100 }
101 }
102
103 pub fn ask(&self) -> Option<f64> {
105 match *self {
106 Quote::Mid(_) => None,
107 Quote::TopOfBook { ask, .. } | Quote::Sized { ask, .. } => Some(ask),
108 }
109 }
110
111 pub fn spread(&self) -> Option<f64> {
113 match *self {
114 Quote::Mid(_) => None,
115 Quote::TopOfBook { bid, ask } | Quote::Sized { bid, ask, .. } => Some(ask - bid),
116 }
117 }
118
119 pub fn microprice(&self) -> f64 {
124 match *self {
125 Quote::Sized { bid, bid_size, ask, ask_size } => {
126 (bid * ask_size + ask * bid_size) / (bid_size + ask_size)
127 }
128 _ => self.mid(),
129 }
130 }
131
132 pub fn valid_value(&self) -> bool {
134 self.mid() > 0.0
135 }
136
137 pub fn scaled(&self, factor: f64) -> Quote {
142 match *self {
143 Quote::Mid(value) => Quote::Mid(value * factor),
144 Quote::TopOfBook { bid, ask } => {
145 Quote::TopOfBook { bid: bid * factor, ask: ask * factor }
146 }
147 Quote::Sized { bid, bid_size, ask, ask_size } => {
148 Quote::Sized { bid: bid * factor, bid_size, ask: ask * factor, ask_size }
149 }
150 }
151 }
152
153 pub fn shifted(&self, delta: f64) -> Quote {
156 match *self {
157 Quote::Mid(value) => Quote::Mid(value + delta),
158 Quote::TopOfBook { bid, ask } => {
159 Quote::TopOfBook { bid: bid + delta, ask: ask + delta }
160 }
161 Quote::Sized { bid, bid_size, ask, ask_size } => {
162 Quote::Sized { bid: bid + delta, bid_size, ask: ask + delta, ask_size }
163 }
164 }
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[test]
173 fn mid_is_derived_never_stored() {
174 assert_eq!(Quote::new(100.0).mid(), 100.0);
175 let top = Quote::from_bid_ask(99.0, 101.0).unwrap();
176 assert_eq!(top.mid(), 100.0);
177 assert_eq!(top.spread(), Some(2.0));
178 assert_eq!(top.bid(), Some(99.0));
179 assert_eq!(top.ask(), Some(101.0));
180 assert_eq!(top.value(), top.mid());
182 let mark = Quote::new(100.0);
184 assert_eq!(mark.bid(), None);
185 assert_eq!(mark.spread(), None);
186 }
187
188 #[test]
189 fn constructors_reject_crossed_and_non_finite_input() {
190 assert!(Quote::from_bid_ask(101.0, 99.0).is_err(), "crossed");
191 assert!(Quote::from_bid_ask(f64::NAN, 100.0).is_err());
192 assert!(Quote::from_bid_ask(99.0, f64::INFINITY).is_err());
193 assert!(Quote::from_bid_ask(100.0, 100.0).is_ok());
195 assert!(Quote::from_bid_ask_sized(99.0, 0.0, 101.0, 5.0).is_err(), "zero size");
196 assert!(Quote::from_bid_ask_sized(99.0, 5.0, 101.0, -1.0).is_err());
197 }
198
199 #[test]
200 fn microprice_weights_toward_the_thin_side() {
201 let quote = Quote::from_bid_ask_sized(99.0, 400.0, 101.0, 100.0).unwrap();
203 let micro = quote.microprice();
204 assert!((micro - (99.0 * 100.0 + 101.0 * 400.0) / 500.0).abs() < 1e-12);
205 assert!(micro > quote.mid(), "heavy bid pushes fair value up");
206 assert_eq!(Quote::from_bid_ask(99.0, 101.0).unwrap().microprice(), 100.0);
208 assert_eq!(Quote::new(100.0).microprice(), 100.0);
209 }
210
211 #[test]
212 fn bumps_preserve_shape_spread_and_sizes() {
213 let quote = Quote::from_bid_ask_sized(99.0, 400.0, 101.0, 100.0).unwrap();
214 let scaled = quote.scaled(0.8);
215 assert!((scaled.mid() - 80.0).abs() < 1e-12);
216 assert!((scaled.spread().unwrap() - 1.6).abs() < 1e-12, "relative bump scales the spread");
217 let shifted = quote.shifted(-20.0);
218 assert!((shifted.mid() - 80.0).abs() < 1e-12);
219 assert!((shifted.spread().unwrap() - 2.0).abs() < 1e-12, "absolute bump preserves the spread");
220 match (scaled, shifted) {
222 (
223 Quote::Sized { bid_size: s1, ask_size: a1, .. },
224 Quote::Sized { bid_size: s2, ask_size: a2, .. },
225 ) => {
226 assert_eq!((s1, a1), (400.0, 100.0));
227 assert_eq!((s2, a2), (400.0, 100.0));
228 }
229 other => panic!("bump must preserve the quote shape, got {other:?}"),
230 }
231 assert!((Quote::new(100.0).scaled(1.1).mid() - 110.0).abs() < 1e-12);
233 assert!((Quote::new(100.0).shifted(-1.0).mid() - 99.0).abs() < 1e-12);
234 }
235}