1use std::{cmp, collections::HashMap, fmt::Display, hash::Hash};
19
20use derive_builder::Builder;
21use indexmap::IndexMap;
22use nautilus_core::{
23 UnixNanos,
24 correctness::{FAILED, check_equal_u8},
25 serialization::Serializable,
26};
27use serde::{Deserialize, Serialize};
28
29use super::GetTsInit;
30use crate::{
31 enums::PriceType,
32 identifiers::InstrumentId,
33 types::{
34 Price, Quantity,
35 fixed::{FIXED_PRECISION, FIXED_SIZE_BINARY},
36 },
37};
38
39#[repr(C)]
41#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Builder)]
42#[serde(tag = "type")]
43#[cfg_attr(
44 feature = "python",
45 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
46)]
47pub struct QuoteTick {
48 pub instrument_id: InstrumentId,
50 pub bid_price: Price,
52 pub ask_price: Price,
54 pub bid_size: Quantity,
56 pub ask_size: Quantity,
58 pub ts_event: UnixNanos,
60 pub ts_init: UnixNanos,
62}
63
64impl QuoteTick {
65 pub fn new_checked(
77 instrument_id: InstrumentId,
78 bid_price: Price,
79 ask_price: Price,
80 bid_size: Quantity,
81 ask_size: Quantity,
82 ts_event: UnixNanos,
83 ts_init: UnixNanos,
84 ) -> anyhow::Result<Self> {
85 check_equal_u8(
86 bid_price.precision,
87 ask_price.precision,
88 "bid_price.precision",
89 "ask_price.precision",
90 )?;
91 check_equal_u8(
92 bid_size.precision,
93 ask_size.precision,
94 "bid_size.precision",
95 "ask_size.precision",
96 )?;
97 Ok(Self {
98 instrument_id,
99 bid_price,
100 ask_price,
101 bid_size,
102 ask_size,
103 ts_event,
104 ts_init,
105 })
106 }
107
108 pub fn new(
116 instrument_id: InstrumentId,
117 bid_price: Price,
118 ask_price: Price,
119 bid_size: Quantity,
120 ask_size: Quantity,
121 ts_event: UnixNanos,
122 ts_init: UnixNanos,
123 ) -> Self {
124 Self::new_checked(
125 instrument_id,
126 bid_price,
127 ask_price,
128 bid_size,
129 ask_size,
130 ts_event,
131 ts_init,
132 )
133 .expect(FAILED)
134 }
135
136 #[must_use]
138 pub fn get_metadata(
139 instrument_id: &InstrumentId,
140 price_precision: u8,
141 size_precision: u8,
142 ) -> HashMap<String, String> {
143 let mut metadata = HashMap::new();
144 metadata.insert("instrument_id".to_string(), instrument_id.to_string());
145 metadata.insert("price_precision".to_string(), price_precision.to_string());
146 metadata.insert("size_precision".to_string(), size_precision.to_string());
147 metadata
148 }
149
150 #[must_use]
152 pub fn get_fields() -> IndexMap<String, String> {
153 let mut metadata = IndexMap::new();
154 metadata.insert("bid_price".to_string(), FIXED_SIZE_BINARY.to_string());
155 metadata.insert("ask_price".to_string(), FIXED_SIZE_BINARY.to_string());
156 metadata.insert("bid_size".to_string(), FIXED_SIZE_BINARY.to_string());
157 metadata.insert("ask_size".to_string(), FIXED_SIZE_BINARY.to_string());
158 metadata.insert("ts_event".to_string(), "UInt64".to_string());
159 metadata.insert("ts_init".to_string(), "UInt64".to_string());
160 metadata
161 }
162
163 #[must_use]
165 pub fn extract_price(&self, price_type: PriceType) -> Price {
166 match price_type {
167 PriceType::Bid => self.bid_price,
168 PriceType::Ask => self.ask_price,
169 PriceType::Mid => Price::from_raw(
170 (self.bid_price.raw + self.ask_price.raw) / 2,
171 cmp::min(self.bid_price.precision + 1, FIXED_PRECISION),
172 ),
173 _ => panic!("Cannot extract with price type {price_type}"),
174 }
175 }
176
177 #[must_use]
179 pub fn extract_size(&self, price_type: PriceType) -> Quantity {
180 match price_type {
181 PriceType::Bid => self.bid_size,
182 PriceType::Ask => self.ask_size,
183 PriceType::Mid => Quantity::from_raw(
184 (self.bid_size.raw + self.ask_size.raw) / 2,
185 cmp::min(self.bid_size.precision + 1, FIXED_PRECISION),
186 ),
187 _ => panic!("Cannot extract with price type {price_type}"),
188 }
189 }
190}
191
192impl Display for QuoteTick {
193 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
194 write!(
195 f,
196 "{},{},{},{},{},{}",
197 self.instrument_id,
198 self.bid_price,
199 self.ask_price,
200 self.bid_size,
201 self.ask_size,
202 self.ts_event,
203 )
204 }
205}
206
207impl Serializable for QuoteTick {}
208
209impl GetTsInit for QuoteTick {
210 fn ts_init(&self) -> UnixNanos {
211 self.ts_init
212 }
213}
214
215#[cfg(test)]
219mod tests {
220 use nautilus_core::{UnixNanos, serialization::Serializable};
221 use rstest::rstest;
222
223 use crate::{
224 data::{QuoteTick, stubs::quote_ethusdt_binance},
225 enums::PriceType,
226 identifiers::InstrumentId,
227 types::{Price, Quantity},
228 };
229
230 #[rstest]
231 #[should_panic(
232 expected = "'bid_price.precision' u8 of 4 was not equal to 'ask_price.precision' u8 of 5"
233 )]
234 fn test_quote_tick_new_with_precision_mismatch_panics() {
235 let instrument_id = InstrumentId::from("ETH-USDT-SWAP.OKX");
236 let bid_price = Price::from("10000.0000"); let ask_price = Price::from("10000.00100"); let bid_size = Quantity::from("1.000000");
239 let ask_size = Quantity::from("1.000000");
240 let ts_event = UnixNanos::from(0);
241 let ts_init = UnixNanos::from(1);
242
243 let _ = QuoteTick::new(
244 instrument_id,
245 bid_price,
246 ask_price,
247 bid_size,
248 ask_size,
249 ts_event,
250 ts_init,
251 );
252 }
253
254 #[rstest]
255 fn test_quote_tick_new_checked_with_precision_mismatch_error() {
256 let instrument_id = InstrumentId::from("ETH-USDT-SWAP.OKX");
257 let bid_price = Price::from("10000.0000");
258 let ask_price = Price::from("10000.0010");
259 let bid_size = Quantity::from("10.000000"); let ask_size = Quantity::from("10.0000000"); let ts_event = UnixNanos::from(0);
262 let ts_init = UnixNanos::from(1);
263
264 let result = QuoteTick::new_checked(
265 instrument_id,
266 bid_price,
267 ask_price,
268 bid_size,
269 ask_size,
270 ts_event,
271 ts_init,
272 );
273
274 assert!(result.is_err());
275 }
276
277 #[rstest]
278 fn test_to_string(quote_ethusdt_binance: QuoteTick) {
279 let quote = quote_ethusdt_binance;
280 assert_eq!(
281 quote.to_string(),
282 "ETHUSDT-PERP.BINANCE,10000.0000,10001.0000,1.00000000,1.00000000,0"
283 );
284 }
285
286 #[rstest]
287 #[case(PriceType::Bid, Price::from("10000.0000"))]
288 #[case(PriceType::Ask, Price::from("10001.0000"))]
289 #[case(PriceType::Mid, Price::from("10000.5000"))]
290 fn test_extract_price(
291 #[case] input: PriceType,
292 #[case] expected: Price,
293 quote_ethusdt_binance: QuoteTick,
294 ) {
295 let quote = quote_ethusdt_binance;
296 let result = quote.extract_price(input);
297 assert_eq!(result, expected);
298 }
299
300 #[rstest]
301 fn test_json_serialization(quote_ethusdt_binance: QuoteTick) {
302 let quote = quote_ethusdt_binance;
303 let serialized = quote.as_json_bytes().unwrap();
304 let deserialized = QuoteTick::from_json_bytes(serialized.as_ref()).unwrap();
305 assert_eq!(deserialized, quote);
306 }
307
308 #[rstest]
309 fn test_msgpack_serialization(quote_ethusdt_binance: QuoteTick) {
310 let quote = quote_ethusdt_binance;
311 let serialized = quote.as_msgpack_bytes().unwrap();
312 let deserialized = QuoteTick::from_msgpack_bytes(serialized.as_ref()).unwrap();
313 assert_eq!(deserialized, quote);
314 }
315}