1use std::{collections::HashMap, str::FromStr, sync::Arc};
17
18use arrow::{
19 array::{FixedSizeBinaryArray, FixedSizeBinaryBuilder, UInt64Array},
20 datatypes::{DataType, Field, Schema},
21 error::ArrowError,
22 record_batch::RecordBatch,
23};
24use nautilus_model::{data::QuoteTick, identifiers::InstrumentId, types::fixed::PRECISION_BYTES};
25
26use super::{
27 DecodeDataFromRecordBatch, EncodingError, KEY_INSTRUMENT_ID, KEY_PRICE_PRECISION,
28 KEY_SIZE_PRECISION, decode_price, decode_quantity, extract_column,
29};
30use crate::arrow::{ArrowSchemaProvider, Data, DecodeFromRecordBatch, EncodeToRecordBatch};
31
32impl ArrowSchemaProvider for QuoteTick {
33 fn get_schema(metadata: Option<HashMap<String, String>>) -> Schema {
34 let fields = vec![
35 Field::new(
36 "bid_price",
37 DataType::FixedSizeBinary(PRECISION_BYTES),
38 false,
39 ),
40 Field::new(
41 "ask_price",
42 DataType::FixedSizeBinary(PRECISION_BYTES),
43 false,
44 ),
45 Field::new(
46 "bid_size",
47 DataType::FixedSizeBinary(PRECISION_BYTES),
48 false,
49 ),
50 Field::new(
51 "ask_size",
52 DataType::FixedSizeBinary(PRECISION_BYTES),
53 false,
54 ),
55 Field::new("ts_event", DataType::UInt64, false),
56 Field::new("ts_init", DataType::UInt64, false),
57 ];
58
59 match metadata {
60 Some(metadata) => Schema::new_with_metadata(fields, metadata),
61 None => Schema::new(fields),
62 }
63 }
64}
65
66fn parse_metadata(
67 metadata: &HashMap<String, String>,
68) -> Result<(InstrumentId, u8, u8), EncodingError> {
69 let instrument_id_str = metadata
70 .get(KEY_INSTRUMENT_ID)
71 .ok_or_else(|| EncodingError::MissingMetadata(KEY_INSTRUMENT_ID))?;
72 let instrument_id = InstrumentId::from_str(instrument_id_str)
73 .map_err(|e| EncodingError::ParseError(KEY_INSTRUMENT_ID, e.to_string()))?;
74
75 let price_precision = metadata
76 .get(KEY_PRICE_PRECISION)
77 .ok_or_else(|| EncodingError::MissingMetadata(KEY_PRICE_PRECISION))?
78 .parse::<u8>()
79 .map_err(|e| EncodingError::ParseError(KEY_PRICE_PRECISION, e.to_string()))?;
80
81 let size_precision = metadata
82 .get(KEY_SIZE_PRECISION)
83 .ok_or_else(|| EncodingError::MissingMetadata(KEY_SIZE_PRECISION))?
84 .parse::<u8>()
85 .map_err(|e| EncodingError::ParseError(KEY_SIZE_PRECISION, e.to_string()))?;
86
87 Ok((instrument_id, price_precision, size_precision))
88}
89
90impl EncodeToRecordBatch for QuoteTick {
91 fn encode_batch(
92 metadata: &HashMap<String, String>,
93 data: &[Self],
94 ) -> Result<RecordBatch, ArrowError> {
95 let mut bid_price_builder =
96 FixedSizeBinaryBuilder::with_capacity(data.len(), PRECISION_BYTES);
97 let mut ask_price_builder =
98 FixedSizeBinaryBuilder::with_capacity(data.len(), PRECISION_BYTES);
99 let mut bid_size_builder =
100 FixedSizeBinaryBuilder::with_capacity(data.len(), PRECISION_BYTES);
101 let mut ask_size_builder =
102 FixedSizeBinaryBuilder::with_capacity(data.len(), PRECISION_BYTES);
103 let mut ts_event_builder = UInt64Array::builder(data.len());
104 let mut ts_init_builder = UInt64Array::builder(data.len());
105
106 for quote in data {
107 bid_price_builder
108 .append_value(quote.bid_price.raw.to_le_bytes())
109 .unwrap();
110 ask_price_builder
111 .append_value(quote.ask_price.raw.to_le_bytes())
112 .unwrap();
113 bid_size_builder
114 .append_value(quote.bid_size.raw.to_le_bytes())
115 .unwrap();
116 ask_size_builder
117 .append_value(quote.ask_size.raw.to_le_bytes())
118 .unwrap();
119 ts_event_builder.append_value(quote.ts_event.as_u64());
120 ts_init_builder.append_value(quote.ts_init.as_u64());
121 }
122
123 RecordBatch::try_new(
124 Self::get_schema(Some(metadata.clone())).into(),
125 vec![
126 Arc::new(bid_price_builder.finish()),
127 Arc::new(ask_price_builder.finish()),
128 Arc::new(bid_size_builder.finish()),
129 Arc::new(ask_size_builder.finish()),
130 Arc::new(ts_event_builder.finish()),
131 Arc::new(ts_init_builder.finish()),
132 ],
133 )
134 }
135
136 fn metadata(&self) -> HashMap<String, String> {
137 Self::get_metadata(
138 &self.instrument_id,
139 self.bid_price.precision,
140 self.bid_size.precision,
141 )
142 }
143}
144
145impl DecodeFromRecordBatch for QuoteTick {
146 fn decode_batch(
147 metadata: &HashMap<String, String>,
148 record_batch: RecordBatch,
149 ) -> Result<Vec<Self>, EncodingError> {
150 let (instrument_id, price_precision, size_precision) = parse_metadata(metadata)?;
151 let cols = record_batch.columns();
152
153 let bid_price_values = extract_column::<FixedSizeBinaryArray>(
154 cols,
155 "bid_price",
156 0,
157 DataType::FixedSizeBinary(PRECISION_BYTES),
158 )?;
159 let ask_price_values = extract_column::<FixedSizeBinaryArray>(
160 cols,
161 "ask_price",
162 1,
163 DataType::FixedSizeBinary(PRECISION_BYTES),
164 )?;
165 let bid_size_values = extract_column::<FixedSizeBinaryArray>(
166 cols,
167 "bid_size",
168 2,
169 DataType::FixedSizeBinary(PRECISION_BYTES),
170 )?;
171 let ask_size_values = extract_column::<FixedSizeBinaryArray>(
172 cols,
173 "ask_size",
174 3,
175 DataType::FixedSizeBinary(PRECISION_BYTES),
176 )?;
177 let ts_event_values = extract_column::<UInt64Array>(cols, "ts_event", 4, DataType::UInt64)?;
178 let ts_init_values = extract_column::<UInt64Array>(cols, "ts_init", 5, DataType::UInt64)?;
179
180 if bid_price_values.value_length() != PRECISION_BYTES {
181 return Err(EncodingError::ParseError(
182 "bid_price",
183 format!(
184 "Invalid value length: expected {PRECISION_BYTES}, found {}",
185 bid_price_values.value_length()
186 ),
187 ));
188 }
189 if ask_price_values.value_length() != PRECISION_BYTES {
190 return Err(EncodingError::ParseError(
191 "ask_price",
192 format!(
193 "Invalid value length: expected {PRECISION_BYTES}, found {}",
194 ask_price_values.value_length()
195 ),
196 ));
197 }
198
199 let result: Result<Vec<Self>, EncodingError> = (0..record_batch.num_rows())
200 .map(|row| {
201 let bid_price = decode_price(
202 bid_price_values.value(row),
203 price_precision,
204 "bid_price",
205 row,
206 )?;
207 let ask_price = decode_price(
208 ask_price_values.value(row),
209 price_precision,
210 "ask_price",
211 row,
212 )?;
213 let bid_size =
214 decode_quantity(bid_size_values.value(row), size_precision, "bid_size", row)?;
215 let ask_size =
216 decode_quantity(ask_size_values.value(row), size_precision, "ask_size", row)?;
217 Ok(Self {
218 instrument_id,
219 bid_price,
220 ask_price,
221 bid_size,
222 ask_size,
223 ts_event: ts_event_values.value(row).into(),
224 ts_init: ts_init_values.value(row).into(),
225 })
226 })
227 .collect();
228
229 result
230 }
231}
232
233impl DecodeDataFromRecordBatch for QuoteTick {
234 fn decode_data_batch(
235 metadata: &HashMap<String, String>,
236 record_batch: RecordBatch,
237 ) -> Result<Vec<Data>, EncodingError> {
238 let ticks: Vec<Self> = Self::decode_batch(metadata, record_batch)?;
239 Ok(ticks.into_iter().map(Data::from).collect())
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use std::{collections::HashMap, sync::Arc};
246
247 use arrow::{array::Array, record_batch::RecordBatch};
248 use nautilus_model::types::{
249 Price, Quantity, fixed::FIXED_SCALAR, price::PriceRaw, quantity::QuantityRaw,
250 };
251 use rstest::rstest;
252
253 use super::*;
254 use crate::arrow::{get_raw_price, get_raw_quantity};
255
256 #[rstest]
257 fn test_get_schema() {
258 let instrument_id = InstrumentId::from("AAPL.XNAS");
259 let metadata = QuoteTick::get_metadata(&instrument_id, 2, 0);
260 let schema = QuoteTick::get_schema(Some(metadata.clone()));
261
262 let mut expected_fields = Vec::with_capacity(6);
263
264 expected_fields.push(Field::new(
265 "bid_price",
266 DataType::FixedSizeBinary(PRECISION_BYTES),
267 false,
268 ));
269 expected_fields.push(Field::new(
270 "ask_price",
271 DataType::FixedSizeBinary(PRECISION_BYTES),
272 false,
273 ));
274
275 expected_fields.extend(vec![
276 Field::new(
277 "bid_size",
278 DataType::FixedSizeBinary(PRECISION_BYTES),
279 false,
280 ),
281 Field::new(
282 "ask_size",
283 DataType::FixedSizeBinary(PRECISION_BYTES),
284 false,
285 ),
286 Field::new("ts_event", DataType::UInt64, false),
287 Field::new("ts_init", DataType::UInt64, false),
288 ]);
289
290 let expected_schema = Schema::new_with_metadata(expected_fields, metadata);
291 assert_eq!(schema, expected_schema);
292 }
293
294 #[rstest]
295 fn test_get_schema_map() {
296 let arrow_schema = QuoteTick::get_schema_map();
297 let mut expected_map = HashMap::new();
298
299 let fixed_size_binary = format!("FixedSizeBinary({PRECISION_BYTES})");
300 expected_map.insert("bid_price".to_string(), fixed_size_binary.clone());
301 expected_map.insert("ask_price".to_string(), fixed_size_binary.clone());
302 expected_map.insert("bid_size".to_string(), fixed_size_binary.clone());
303 expected_map.insert("ask_size".to_string(), fixed_size_binary);
304 expected_map.insert("ts_event".to_string(), "UInt64".to_string());
305 expected_map.insert("ts_init".to_string(), "UInt64".to_string());
306 assert_eq!(arrow_schema, expected_map);
307 }
308
309 #[rstest]
310 fn test_encode_quote_tick() {
311 let instrument_id = InstrumentId::from("AAPL.XNAS");
313 let tick1 = QuoteTick {
314 instrument_id,
315 bid_price: Price::from("100.10"),
316 ask_price: Price::from("101.50"),
317 bid_size: Quantity::from(1000),
318 ask_size: Quantity::from(500),
319 ts_event: 1.into(),
320 ts_init: 3.into(),
321 };
322
323 let tick2 = QuoteTick {
324 instrument_id,
325 bid_price: Price::from("100.75"),
326 ask_price: Price::from("100.20"),
327 bid_size: Quantity::from(750),
328 ask_size: Quantity::from(300),
329 ts_event: 2.into(),
330 ts_init: 4.into(),
331 };
332
333 let data = vec![tick1, tick2];
334 let metadata = QuoteTick::get_metadata(&instrument_id, 2, 0);
335 let record_batch = QuoteTick::encode_batch(&metadata, &data).unwrap();
336
337 let columns = record_batch.columns();
339
340 let bid_price_values = columns[0]
341 .as_any()
342 .downcast_ref::<FixedSizeBinaryArray>()
343 .unwrap();
344 let ask_price_values = columns[1]
345 .as_any()
346 .downcast_ref::<FixedSizeBinaryArray>()
347 .unwrap();
348 assert_eq!(
349 get_raw_price(bid_price_values.value(0)),
350 (100.10 * FIXED_SCALAR) as PriceRaw
351 );
352 assert_eq!(
353 get_raw_price(bid_price_values.value(1)),
354 (100.75 * FIXED_SCALAR) as PriceRaw
355 );
356 assert_eq!(
357 get_raw_price(ask_price_values.value(0)),
358 (101.50 * FIXED_SCALAR) as PriceRaw
359 );
360 assert_eq!(
361 get_raw_price(ask_price_values.value(1)),
362 (100.20 * FIXED_SCALAR) as PriceRaw
363 );
364
365 let bid_size_values = columns[2]
366 .as_any()
367 .downcast_ref::<FixedSizeBinaryArray>()
368 .unwrap();
369 let ask_size_values = columns[3]
370 .as_any()
371 .downcast_ref::<FixedSizeBinaryArray>()
372 .unwrap();
373 let ts_event_values = columns[4].as_any().downcast_ref::<UInt64Array>().unwrap();
374 let ts_init_values = columns[5].as_any().downcast_ref::<UInt64Array>().unwrap();
375
376 assert_eq!(columns.len(), 6);
377 assert_eq!(bid_size_values.len(), 2);
378 assert_eq!(
379 get_raw_quantity(bid_size_values.value(0)),
380 (1000.0 * FIXED_SCALAR) as QuantityRaw
381 );
382 assert_eq!(
383 get_raw_quantity(bid_size_values.value(1)),
384 (750.0 * FIXED_SCALAR) as QuantityRaw
385 );
386 assert_eq!(ask_size_values.len(), 2);
387 assert_eq!(
388 get_raw_quantity(ask_size_values.value(0)),
389 (500.0 * FIXED_SCALAR) as QuantityRaw
390 );
391 assert_eq!(
392 get_raw_quantity(ask_size_values.value(1)),
393 (300.0 * FIXED_SCALAR) as QuantityRaw
394 );
395 assert_eq!(ts_event_values.len(), 2);
396 assert_eq!(ts_event_values.value(0), 1);
397 assert_eq!(ts_event_values.value(1), 2);
398 assert_eq!(ts_init_values.len(), 2);
399 assert_eq!(ts_init_values.value(0), 3);
400 assert_eq!(ts_init_values.value(1), 4);
401 }
402
403 #[rstest]
404 fn test_decode_batch() {
405 let instrument_id = InstrumentId::from("AAPL.XNAS");
406 let metadata = QuoteTick::get_metadata(&instrument_id, 2, 0);
407
408 let raw_bid1 = (100.00 * FIXED_SCALAR) as PriceRaw;
409 let raw_bid2 = (99.00 * FIXED_SCALAR) as PriceRaw;
410 let raw_ask1 = (101.00 * FIXED_SCALAR) as PriceRaw;
411 let raw_ask2 = (100.00 * FIXED_SCALAR) as PriceRaw;
412
413 let (bid_price, ask_price) = (
414 FixedSizeBinaryArray::from(vec![&raw_bid1.to_le_bytes(), &raw_bid2.to_le_bytes()]),
415 FixedSizeBinaryArray::from(vec![&raw_ask1.to_le_bytes(), &raw_ask2.to_le_bytes()]),
416 );
417
418 let bid_size = FixedSizeBinaryArray::from(vec![
419 &((100.0 * FIXED_SCALAR) as PriceRaw).to_le_bytes(),
420 &((90.0 * FIXED_SCALAR) as PriceRaw).to_le_bytes(),
421 ]);
422 let ask_size = FixedSizeBinaryArray::from(vec![
423 &((110.0 * FIXED_SCALAR) as PriceRaw).to_le_bytes(),
424 &((100.0 * FIXED_SCALAR) as PriceRaw).to_le_bytes(),
425 ]);
426 let ts_event = UInt64Array::from(vec![1, 2]);
427 let ts_init = UInt64Array::from(vec![3, 4]);
428
429 let record_batch = RecordBatch::try_new(
430 QuoteTick::get_schema(Some(metadata.clone())).into(),
431 vec![
432 Arc::new(bid_price),
433 Arc::new(ask_price),
434 Arc::new(bid_size),
435 Arc::new(ask_size),
436 Arc::new(ts_event),
437 Arc::new(ts_init),
438 ],
439 )
440 .unwrap();
441
442 let decoded_data = QuoteTick::decode_batch(&metadata, record_batch).unwrap();
443 assert_eq!(decoded_data.len(), 2);
444
445 assert_eq!(decoded_data[0].bid_price, Price::from_raw(raw_bid1, 2));
447 assert_eq!(decoded_data[0].ask_price, Price::from_raw(raw_ask1, 2));
448 assert_eq!(decoded_data[1].bid_price, Price::from_raw(raw_bid2, 2));
449 assert_eq!(decoded_data[1].ask_price, Price::from_raw(raw_ask2, 2));
450 }
451
452 #[rstest]
453 fn test_decode_batch_invalid_bid_price_returns_error() {
454 let instrument_id = InstrumentId::from("AAPL.XNAS");
455 let metadata = QuoteTick::get_metadata(&instrument_id, 2, 0);
456
457 let invalid_price: PriceRaw = PriceRaw::MAX - 1000;
458 let valid_price = (100.00 * FIXED_SCALAR) as PriceRaw;
459
460 let bid_price = FixedSizeBinaryArray::from(vec![&invalid_price.to_le_bytes()]);
461 let ask_price = FixedSizeBinaryArray::from(vec![&valid_price.to_le_bytes()]);
462 let bid_size = FixedSizeBinaryArray::from(vec![
463 &((100.0 * FIXED_SCALAR) as QuantityRaw).to_le_bytes(),
464 ]);
465 let ask_size = FixedSizeBinaryArray::from(vec![
466 &((100.0 * FIXED_SCALAR) as QuantityRaw).to_le_bytes(),
467 ]);
468 let ts_event = UInt64Array::from(vec![1]);
469 let ts_init = UInt64Array::from(vec![2]);
470
471 let record_batch = RecordBatch::try_new(
472 QuoteTick::get_schema(Some(metadata.clone())).into(),
473 vec![
474 Arc::new(bid_price),
475 Arc::new(ask_price),
476 Arc::new(bid_size),
477 Arc::new(ask_size),
478 Arc::new(ts_event),
479 Arc::new(ts_init),
480 ],
481 )
482 .unwrap();
483
484 let result = QuoteTick::decode_batch(&metadata, record_batch);
485 assert!(result.is_err());
486 let err = result.unwrap_err();
487 assert!(
488 err.to_string().contains("bid_price") && err.to_string().contains("row 0"),
489 "Expected bid_price error at row 0, got: {err}"
490 );
491 }
492
493 #[rstest]
494 fn test_decode_batch_invalid_ask_size_returns_error() {
495 use nautilus_model::types::quantity::QUANTITY_RAW_MAX;
496
497 let instrument_id = InstrumentId::from("AAPL.XNAS");
498 let metadata = QuoteTick::get_metadata(&instrument_id, 2, 0);
499
500 let valid_price = (100.00 * FIXED_SCALAR) as PriceRaw;
501 let bid_price = FixedSizeBinaryArray::from(vec![&valid_price.to_le_bytes()]);
502 let ask_price = FixedSizeBinaryArray::from(vec![&valid_price.to_le_bytes()]);
503 let bid_size = FixedSizeBinaryArray::from(vec![
504 &((100.0 * FIXED_SCALAR) as QuantityRaw).to_le_bytes(),
505 ]);
506
507 let invalid_size = QUANTITY_RAW_MAX + 1;
508 let ask_size = FixedSizeBinaryArray::from(vec![&invalid_size.to_le_bytes()]);
509 let ts_event = UInt64Array::from(vec![1]);
510 let ts_init = UInt64Array::from(vec![2]);
511
512 let record_batch = RecordBatch::try_new(
513 QuoteTick::get_schema(Some(metadata.clone())).into(),
514 vec![
515 Arc::new(bid_price),
516 Arc::new(ask_price),
517 Arc::new(bid_size),
518 Arc::new(ask_size),
519 Arc::new(ts_event),
520 Arc::new(ts_init),
521 ],
522 )
523 .unwrap();
524
525 let result = QuoteTick::decode_batch(&metadata, record_batch);
526 assert!(result.is_err());
527 let err = result.unwrap_err();
528 assert!(
529 err.to_string().contains("ask_size") && err.to_string().contains("row 0"),
530 "Expected ask_size error at row 0, got: {err}"
531 );
532 }
533
534 #[rstest]
535 fn test_decode_batch_missing_instrument_id_returns_error() {
536 let instrument_id = InstrumentId::from("AAPL.XNAS");
537 let mut metadata = QuoteTick::get_metadata(&instrument_id, 2, 0);
538 metadata.remove(KEY_INSTRUMENT_ID);
539
540 let valid_price = (100.00 * FIXED_SCALAR) as PriceRaw;
541 let bid_price = FixedSizeBinaryArray::from(vec![&valid_price.to_le_bytes()]);
542 let ask_price = FixedSizeBinaryArray::from(vec![&valid_price.to_le_bytes()]);
543 let bid_size = FixedSizeBinaryArray::from(vec![
544 &((100.0 * FIXED_SCALAR) as QuantityRaw).to_le_bytes(),
545 ]);
546 let ask_size = FixedSizeBinaryArray::from(vec![
547 &((100.0 * FIXED_SCALAR) as QuantityRaw).to_le_bytes(),
548 ]);
549 let ts_event = UInt64Array::from(vec![1]);
550 let ts_init = UInt64Array::from(vec![2]);
551
552 let record_batch = RecordBatch::try_new(
553 QuoteTick::get_schema(Some(metadata.clone())).into(),
554 vec![
555 Arc::new(bid_price),
556 Arc::new(ask_price),
557 Arc::new(bid_size),
558 Arc::new(ask_size),
559 Arc::new(ts_event),
560 Arc::new(ts_init),
561 ],
562 )
563 .unwrap();
564
565 let result = QuoteTick::decode_batch(&metadata, record_batch);
566 assert!(result.is_err());
567 let err = result.unwrap_err();
568 assert!(
569 err.to_string().contains("instrument_id"),
570 "Expected missing instrument_id error, got: {err}"
571 );
572 }
573
574 #[rstest]
575 fn test_decode_batch_missing_price_precision_returns_error() {
576 let instrument_id = InstrumentId::from("AAPL.XNAS");
577 let mut metadata = QuoteTick::get_metadata(&instrument_id, 2, 0);
578 metadata.remove(KEY_PRICE_PRECISION);
579
580 let valid_price = (100.00 * FIXED_SCALAR) as PriceRaw;
581 let bid_price = FixedSizeBinaryArray::from(vec![&valid_price.to_le_bytes()]);
582 let ask_price = FixedSizeBinaryArray::from(vec![&valid_price.to_le_bytes()]);
583 let bid_size = FixedSizeBinaryArray::from(vec![
584 &((100.0 * FIXED_SCALAR) as QuantityRaw).to_le_bytes(),
585 ]);
586 let ask_size = FixedSizeBinaryArray::from(vec![
587 &((100.0 * FIXED_SCALAR) as QuantityRaw).to_le_bytes(),
588 ]);
589 let ts_event = UInt64Array::from(vec![1]);
590 let ts_init = UInt64Array::from(vec![2]);
591
592 let record_batch = RecordBatch::try_new(
593 QuoteTick::get_schema(Some(metadata.clone())).into(),
594 vec![
595 Arc::new(bid_price),
596 Arc::new(ask_price),
597 Arc::new(bid_size),
598 Arc::new(ask_size),
599 Arc::new(ts_event),
600 Arc::new(ts_init),
601 ],
602 )
603 .unwrap();
604
605 let result = QuoteTick::decode_batch(&metadata, record_batch);
606 assert!(result.is_err());
607 let err = result.unwrap_err();
608 assert!(
609 err.to_string().contains("price_precision"),
610 "Expected missing price_precision error, got: {err}"
611 );
612 }
613
614 #[rstest]
615 fn test_encode_decode_round_trip() {
616 let instrument_id = InstrumentId::from("AAPL.XNAS");
617 let metadata = QuoteTick::get_metadata(&instrument_id, 2, 0);
618
619 let tick1 = QuoteTick {
620 instrument_id,
621 bid_price: Price::from("100.10"),
622 ask_price: Price::from("100.20"),
623 bid_size: Quantity::from(1000),
624 ask_size: Quantity::from(500),
625 ts_event: 1_000_000_000.into(),
626 ts_init: 1_000_000_001.into(),
627 };
628
629 let tick2 = QuoteTick {
630 instrument_id,
631 bid_price: Price::from("100.15"),
632 ask_price: Price::from("100.25"),
633 bid_size: Quantity::from(750),
634 ask_size: Quantity::from(250),
635 ts_event: 2_000_000_000.into(),
636 ts_init: 2_000_000_001.into(),
637 };
638
639 let original = vec![tick1, tick2];
640 let record_batch = QuoteTick::encode_batch(&metadata, &original).unwrap();
641 let decoded = QuoteTick::decode_batch(&metadata, record_batch).unwrap();
642
643 assert_eq!(decoded.len(), original.len());
644 for (orig, dec) in original.iter().zip(decoded.iter()) {
645 assert_eq!(dec.instrument_id, orig.instrument_id);
646 assert_eq!(dec.bid_price, orig.bid_price);
647 assert_eq!(dec.ask_price, orig.ask_price);
648 assert_eq!(dec.bid_size, orig.bid_size);
649 assert_eq!(dec.ask_size, orig.ask_size);
650 assert_eq!(dec.ts_event, orig.ts_event);
651 assert_eq!(dec.ts_init, orig.ts_init);
652 }
653 }
654}