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::HasTsInit;
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 = "posei_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]
169 pub fn extract_price(&self, price_type: PriceType) -> Price {
170 match price_type {
171 PriceType::Bid => self.bid_price,
172 PriceType::Ask => self.ask_price,
173 PriceType::Mid => Price::from_raw(
174 (self.bid_price.raw + self.ask_price.raw) / 2,
175 cmp::min(self.bid_price.precision + 1, FIXED_PRECISION),
176 ),
177 _ => panic!("Cannot extract with price type {price_type}"),
178 }
179 }
180
181 #[must_use]
187 pub fn extract_size(&self, price_type: PriceType) -> Quantity {
188 match price_type {
189 PriceType::Bid => self.bid_size,
190 PriceType::Ask => self.ask_size,
191 PriceType::Mid => Quantity::from_raw(
192 (self.bid_size.raw + self.ask_size.raw) / 2,
193 cmp::min(self.bid_size.precision + 1, FIXED_PRECISION),
194 ),
195 _ => panic!("Cannot extract with price type {price_type}"),
196 }
197 }
198}
199
200impl Display for QuoteTick {
201 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202 write!(
203 f,
204 "{},{},{},{},{},{}",
205 self.instrument_id,
206 self.bid_price,
207 self.ask_price,
208 self.bid_size,
209 self.ask_size,
210 self.ts_event,
211 )
212 }
213}
214
215impl Serializable for QuoteTick {}
216
217impl HasTsInit for QuoteTick {
218 fn ts_init(&self) -> UnixNanos {
219 self.ts_init
220 }
221}
222
223#[cfg(test)]
227mod tests {
228 use nautilus_core::{UnixNanos, serialization::Serializable};
229 use rstest::rstest;
230
231 use crate::{
232 data::{QuoteTick, stubs::quote_ethusdt_binance},
233 enums::PriceType,
234 identifiers::InstrumentId,
235 types::{Price, Quantity},
236 };
237
238 #[rstest]
239 #[should_panic(
240 expected = "'bid_price.precision' u8 of 4 was not equal to 'ask_price.precision' u8 of 5"
241 )]
242 fn test_quote_tick_new_with_precision_mismatch_panics() {
243 let instrument_id = InstrumentId::from("ETH-USDT-SWAP.OKX");
244 let bid_price = Price::from("10000.0000"); let ask_price = Price::from("10000.00100"); let bid_size = Quantity::from("1.000000");
247 let ask_size = Quantity::from("1.000000");
248 let ts_event = UnixNanos::from(0);
249 let ts_init = UnixNanos::from(1);
250
251 let _ = QuoteTick::new(
252 instrument_id,
253 bid_price,
254 ask_price,
255 bid_size,
256 ask_size,
257 ts_event,
258 ts_init,
259 );
260 }
261
262 #[rstest]
263 fn test_quote_tick_new_checked_with_precision_mismatch_error() {
264 let instrument_id = InstrumentId::from("ETH-USDT-SWAP.OKX");
265 let bid_price = Price::from("10000.0000");
266 let ask_price = Price::from("10000.0010");
267 let bid_size = Quantity::from("10.000000"); let ask_size = Quantity::from("10.0000000"); let ts_event = UnixNanos::from(0);
270 let ts_init = UnixNanos::from(1);
271
272 let result = QuoteTick::new_checked(
273 instrument_id,
274 bid_price,
275 ask_price,
276 bid_size,
277 ask_size,
278 ts_event,
279 ts_init,
280 );
281
282 assert!(result.is_err());
283 }
284
285 #[rstest]
286 fn test_to_string(quote_ethusdt_binance: QuoteTick) {
287 let quote = quote_ethusdt_binance;
288 assert_eq!(
289 quote.to_string(),
290 "ETHUSDT-PERP.BINANCE,10000.0000,10001.0000,1.00000000,1.00000000,0"
291 );
292 }
293
294 #[rstest]
295 #[case(PriceType::Bid, Price::from("10000.0000"))]
296 #[case(PriceType::Ask, Price::from("10001.0000"))]
297 #[case(PriceType::Mid, Price::from("10000.5000"))]
298 fn test_extract_price(
299 #[case] input: PriceType,
300 #[case] expected: Price,
301 quote_ethusdt_binance: QuoteTick,
302 ) {
303 let quote = quote_ethusdt_binance;
304 let result = quote.extract_price(input);
305 assert_eq!(result, expected);
306 }
307
308 #[rstest]
309 fn test_json_serialization(quote_ethusdt_binance: QuoteTick) {
310 let quote = quote_ethusdt_binance;
311 let serialized = quote.to_json_bytes().unwrap();
312 let deserialized = QuoteTick::from_json_bytes(serialized.as_ref()).unwrap();
313 assert_eq!(deserialized, quote);
314 }
315
316 #[rstest]
317 fn test_msgpack_serialization(quote_ethusdt_binance: QuoteTick) {
318 let quote = quote_ethusdt_binance;
319 let serialized = quote.to_msgpack_bytes().unwrap();
320 let deserialized = QuoteTick::from_msgpack_bytes(serialized.as_ref()).unwrap();
321 assert_eq!(deserialized, quote);
322 }
323}