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