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