1use std::{collections::HashMap, str::FromStr, sync::Arc};
17
18use arrow::{
19 array::{Decimal128Array, UInt8Array, UInt64Array},
20 datatypes::{DataType, Field, Schema},
21 error::ArrowError,
22 record_batch::RecordBatch,
23};
24#[cfg(test)]
25use nautilus_model::identifiers::InstrumentId;
26use nautilus_model::{
27 data::{BookOrder, OrderBookDelta},
28 enums::{BookAction, OrderSide},
29};
30
31use super::{
32 DecodeDataFromRecordBatch, EncodingError, KEY_IDENTIFIER, decode_decimal_price,
33 decode_decimal_quantity, decode_required_timestamp, decode_required_u8, decode_required_u64,
34 enum_dictionary_array, enum_dictionary_data_type, extract_column, extract_column_string,
35 fixed_decimal_data_type, identifier_array_from_display, parse_metadata, price_decimal_array,
36 quantity_decimal_array,
37};
38#[cfg(test)]
39use super::{KEY_INSTRUMENT_ID, KEY_PRICE_PRECISION};
40use crate::arrow::{ArrowSchemaProvider, Data, DecodeFromRecordBatch, EncodeToRecordBatch};
41
42impl ArrowSchemaProvider for OrderBookDelta {
43 fn get_schema(metadata: Option<HashMap<String, String>>) -> Schema {
44 let fields = vec![
45 Field::new("action", enum_dictionary_data_type(), false),
46 Field::new("side", enum_dictionary_data_type(), false),
47 Field::new("price", fixed_decimal_data_type(), true),
48 Field::new("size", fixed_decimal_data_type(), true),
49 Field::new("order_id", DataType::UInt64, false),
50 Field::new("flags", DataType::UInt8, false),
51 Field::new("sequence", DataType::UInt64, false),
52 Field::new("ts_event", crate::arrow::timestamp_data_type(), false),
53 Field::new("ts_init", crate::arrow::timestamp_data_type(), false),
54 Field::new(KEY_IDENTIFIER, DataType::Utf8, true),
55 ];
56
57 match metadata {
58 Some(metadata) => Schema::new_with_metadata(fields, metadata),
59 None => Schema::new(fields),
60 }
61 }
62}
63
64impl EncodeToRecordBatch for OrderBookDelta {
65 fn encode_batch<T>(
66 metadata: &HashMap<String, String>,
67 data: &[T],
68 ) -> Result<RecordBatch, ArrowError>
69 where
70 T: std::borrow::Borrow<Self>,
71 {
72 let mut order_id_builder = UInt64Array::builder(data.len());
73 let mut flags_builder = UInt8Array::builder(data.len());
74 let mut sequence_builder = UInt64Array::builder(data.len());
75 let mut ts_event_builder = UInt64Array::builder(data.len());
76 let mut ts_init_builder = UInt64Array::builder(data.len());
77
78 for delta in data.iter().map(std::borrow::Borrow::borrow) {
79 order_id_builder.append_value(delta.order.order_id);
80 flags_builder.append_value(delta.flags);
81 sequence_builder.append_value(delta.sequence);
82 ts_event_builder.append_value(delta.ts_event.as_u64());
83 ts_init_builder.append_value(delta.ts_init.as_u64());
84 }
85
86 let action_array = enum_dictionary_array(
87 data.iter()
88 .map(std::borrow::Borrow::borrow)
89 .map(|delta| delta.action),
90 )?;
91 let side_array =
92 enum_dictionary_array(data.iter().map(std::borrow::Borrow::borrow).map(|delta| {
93 delta
94 .order
95 .side
96 .map_or_else(|| "NO_ORDER_SIDE".to_string(), |side| side.to_string())
97 }))?;
98 let price_array = price_decimal_array(
99 data.iter()
100 .map(std::borrow::Borrow::borrow)
101 .map(|delta| delta.order.price.raw()),
102 "price",
103 )?;
104 let size_array = quantity_decimal_array(
105 data.iter()
106 .map(std::borrow::Borrow::borrow)
107 .map(|delta| delta.order.size.raw()),
108 "size",
109 )?;
110 let order_id_array = order_id_builder.finish();
111 let flags_array = flags_builder.finish();
112 let sequence_array = sequence_builder.finish();
113 let ts_event_array = ts_event_builder.finish();
114 let ts_init_array = ts_init_builder.finish();
115
116 crate::arrow::record_batch_with_timestamps(
117 Self::get_schema(Some(metadata.clone())).into(),
118 vec![
119 Arc::new(action_array),
120 Arc::new(side_array),
121 Arc::new(price_array),
122 Arc::new(size_array),
123 Arc::new(order_id_array),
124 Arc::new(flags_array),
125 Arc::new(sequence_array),
126 Arc::new(ts_event_array),
127 Arc::new(ts_init_array),
128 Arc::new(identifier_array_from_display(
129 data.iter()
130 .map(std::borrow::Borrow::borrow)
131 .map(|delta| delta.instrument_id),
132 )),
133 ],
134 )
135 }
136
137 fn metadata(&self) -> HashMap<String, String> {
138 Self::get_metadata(
139 &self.instrument_id,
140 self.order.price.precision,
141 self.order.size.precision,
142 )
143 }
144
145 fn chunk_metadata<T>(chunk: &[T]) -> HashMap<String, String>
147 where
148 T: std::borrow::Borrow<Self>,
149 {
150 chunk
151 .iter()
152 .map(std::borrow::Borrow::borrow)
153 .find(|delta| delta.action != BookAction::Clear)
154 .or_else(|| chunk.first().map(std::borrow::Borrow::borrow))
155 .map(EncodeToRecordBatch::metadata)
156 .expect("Chunk must contain at least one element to encode")
157 }
158
159 fn matches_chunk_metadata(&self, metadata: &HashMap<String, String>) -> bool {
160 if self.action != BookAction::Clear {
161 return self.metadata() == *metadata;
162 }
163
164 parse_metadata(metadata)
165 .is_ok_and(|(instrument_id, _, _)| self.instrument_id == instrument_id)
166 }
167}
168
169impl DecodeFromRecordBatch for OrderBookDelta {
170 fn decode_batch(
171 metadata: &HashMap<String, String>,
172 record_batch: RecordBatch,
173 ) -> Result<Vec<Self>, EncodingError> {
174 let (instrument_id, price_precision, size_precision) = parse_metadata(metadata)?;
175 let record_batch = crate::arrow::record_batch_with_u64_timestamps(&record_batch)?;
176 let record_batch = &record_batch;
177 let cols = record_batch.columns();
178
179 let action_values = extract_column_string(cols, "action", 0)?;
180 let side_values = extract_column_string(cols, "side", 1)?;
181 let price_values =
182 extract_column::<Decimal128Array>(cols, "price", 2, fixed_decimal_data_type())?;
183 let size_values =
184 extract_column::<Decimal128Array>(cols, "size", 3, fixed_decimal_data_type())?;
185 let order_id_values = extract_column::<UInt64Array>(cols, "order_id", 4, DataType::UInt64)?;
186 let flags_values = extract_column::<UInt8Array>(cols, "flags", 5, DataType::UInt8)?;
187 let sequence_values = extract_column::<UInt64Array>(cols, "sequence", 6, DataType::UInt64)?;
188 let ts_event_values = extract_column::<UInt64Array>(cols, "ts_event", 7, DataType::UInt64)?;
189 let ts_init_values = extract_column::<UInt64Array>(cols, "ts_init", 8, DataType::UInt64)?;
190
191 let result: Result<Vec<Self>, EncodingError> = (0..record_batch.num_rows())
192 .map(|i| {
193 let action_value = action_values.value(i);
194 let action = BookAction::from_str(action_value).map_err(|e| {
195 EncodingError::ParseError(stringify!(BookAction), e.to_string())
196 })?;
197 let side_value = side_values.value(i);
198 let side = if side_value.eq_ignore_ascii_case("NO_ORDER_SIDE") {
199 None
200 } else {
201 Some(OrderSide::from_str(side_value).map_err(|e| {
202 EncodingError::ParseError(stringify!(OrderSide), e.to_string())
203 })?)
204 };
205 let price = decode_decimal_price(price_values, price_precision, "price", i)?;
206 let size = decode_decimal_quantity(size_values, size_precision, "size", i)?;
207 let order_id = decode_required_u64(order_id_values, "order_id", i)?;
208 let flags = decode_required_u8(flags_values, "flags", i)?;
209 let sequence = decode_required_u64(sequence_values, "sequence", i)?;
210 let ts_event = decode_required_timestamp(ts_event_values, "ts_event", i)?;
211 let ts_init = decode_required_timestamp(ts_init_values, "ts_init", i)?;
212
213 Ok(Self {
214 instrument_id,
215 action,
216 order: BookOrder {
217 side,
218 price,
219 size,
220 order_id,
221 },
222 flags,
223 sequence,
224 ts_event,
225 ts_init,
226 })
227 })
228 .collect();
229
230 result
231 }
232}
233
234impl DecodeDataFromRecordBatch for OrderBookDelta {
235 fn decode_data_batch(
236 metadata: &HashMap<String, String>,
237 record_batch: RecordBatch,
238 ) -> Result<Vec<Data>, EncodingError> {
239 let deltas: Vec<Self> = Self::decode_batch(metadata, record_batch)?;
240 Ok(deltas.into_iter().map(Data::from).collect())
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use std::sync::Arc;
247
248 use arrow::array::{Array, ArrayRef, TimestampNanosecondArray};
249 use nautilus_model::types::{
250 Price, Quantity,
251 fixed::FIXED_SCALAR,
252 price::{PRICE_UNDEF, PriceRaw},
253 quantity::{QUANTITY_UNDEF, QuantityRaw},
254 };
255 use pretty_assertions::assert_eq;
256 use rstest::rstest;
257
258 use super::*;
259 use crate::arrow::get_raw_price;
260
261 #[rstest]
262 fn test_get_schema() {
263 let instrument_id = InstrumentId::from("AAPL.XNAS");
264 let metadata = OrderBookDelta::get_metadata(&instrument_id, 2, 0);
265 let schema = OrderBookDelta::get_schema(Some(metadata.clone()));
266
267 let expected_fields = vec![
268 Field::new("action", enum_dictionary_data_type(), false),
269 Field::new("side", enum_dictionary_data_type(), false),
270 Field::new("price", fixed_decimal_data_type(), true),
271 Field::new("size", fixed_decimal_data_type(), true),
272 Field::new("order_id", DataType::UInt64, false),
273 Field::new("flags", DataType::UInt8, false),
274 Field::new("sequence", DataType::UInt64, false),
275 Field::new("ts_event", crate::arrow::timestamp_data_type(), false),
276 Field::new("ts_init", crate::arrow::timestamp_data_type(), false),
277 Field::new(KEY_IDENTIFIER, DataType::Utf8, true),
278 ];
279
280 let expected_schema = Schema::new_with_metadata(expected_fields, metadata);
281 assert_eq!(schema, expected_schema);
282 }
283
284 #[rstest]
285 fn test_get_schema_map() {
286 let schema_map = OrderBookDelta::get_schema_map();
287 let fixed_size_binary = "Decimal128(38, 16)".to_string();
288
289 assert_eq!(schema_map.get("action").unwrap(), "Dictionary(Int8, Utf8)");
290 assert_eq!(schema_map.get("side").unwrap(), "Dictionary(Int8, Utf8)");
291 assert_eq!(*schema_map.get("price").unwrap(), fixed_size_binary);
292 assert_eq!(*schema_map.get("size").unwrap(), fixed_size_binary);
293 assert_eq!(schema_map.get("order_id").unwrap(), "UInt64");
294 assert_eq!(schema_map.get("flags").unwrap(), "UInt8");
295 assert_eq!(schema_map.get("sequence").unwrap(), "UInt64");
296 assert_eq!(
297 schema_map.get("ts_event").unwrap(),
298 "Timestamp(Nanosecond, Some(\"UTC\"))"
299 );
300 assert_eq!(
301 schema_map.get("ts_init").unwrap(),
302 "Timestamp(Nanosecond, Some(\"UTC\"))"
303 );
304 assert_eq!(schema_map.get(KEY_IDENTIFIER).unwrap(), "Utf8");
305 }
306
307 #[rstest]
308 fn clear_delta_rejects_other_instrument_chunk_metadata() {
309 let delta = OrderBookDelta::clear(InstrumentId::from("AAPL.XNAS"), 0, 1.into(), 1.into());
310 let metadata = OrderBookDelta::get_metadata(&InstrumentId::from("MSFT.XNAS"), 2, 0);
311
312 assert!(!delta.matches_chunk_metadata(&metadata));
313 }
314
315 #[rstest]
316 fn clear_delta_rejects_chunk_metadata_without_price_precision() {
317 let instrument_id = InstrumentId::from("AAPL.XNAS");
318 let delta = OrderBookDelta::clear(instrument_id, 0, 1.into(), 1.into());
319 let mut metadata = OrderBookDelta::get_metadata(&instrument_id, 2, 0);
320 metadata.remove(KEY_PRICE_PRECISION);
321
322 assert!(!delta.matches_chunk_metadata(&metadata));
323 }
324
325 #[rstest]
326 fn test_encode_batch() {
327 let instrument_id = InstrumentId::from("AAPL.XNAS");
328 let metadata = OrderBookDelta::get_metadata(&instrument_id, 2, 0);
329
330 let delta1 = OrderBookDelta {
331 instrument_id,
332 action: BookAction::Add,
333 order: BookOrder {
334 side: OrderSide::Buy.into(),
335 price: Price::from("100.10"),
336 size: Quantity::from(100),
337 order_id: 1,
338 },
339 flags: 0,
340 sequence: 1,
341 ts_event: 1.into(),
342 ts_init: 3.into(),
343 };
344
345 let delta2 = OrderBookDelta {
346 instrument_id,
347 action: BookAction::Update,
348 order: BookOrder {
349 side: OrderSide::Sell.into(),
350 price: Price::from("101.20"),
351 size: Quantity::from(200),
352 order_id: 2,
353 },
354 flags: 1,
355 sequence: 2,
356 ts_event: 2.into(),
357 ts_init: 4.into(),
358 };
359
360 let data = vec![delta1, delta2];
361 let record_batch = OrderBookDelta::encode_batch(&metadata, &data).unwrap();
362
363 let columns = record_batch.columns();
364 let action_values = extract_column_string(columns, "action", 0).unwrap();
365 let side_values = extract_column_string(columns, "side", 1).unwrap();
366 let price_values = columns[2]
367 .as_any()
368 .downcast_ref::<Decimal128Array>()
369 .unwrap();
370 let size_values = columns[3]
371 .as_any()
372 .downcast_ref::<Decimal128Array>()
373 .unwrap();
374 let order_id_values = columns[4].as_any().downcast_ref::<UInt64Array>().unwrap();
375 let flags_values = columns[5].as_any().downcast_ref::<UInt8Array>().unwrap();
376 let sequence_values = columns[6].as_any().downcast_ref::<UInt64Array>().unwrap();
377 let ts_event_values = columns[7]
378 .as_any()
379 .downcast_ref::<TimestampNanosecondArray>()
380 .unwrap();
381 let ts_init_values = columns[8]
382 .as_any()
383 .downcast_ref::<TimestampNanosecondArray>()
384 .unwrap();
385
386 assert_eq!(columns.len(), 10);
387 assert_eq!(action_values.len(), 2);
388 assert_eq!(action_values.value(0), "ADD");
389 assert_eq!(action_values.value(1), "UPDATE");
390 assert_eq!(side_values.len(), 2);
391 assert_eq!(side_values.value(0), "BUY");
392 assert_eq!(side_values.value(1), "SELL");
393
394 assert_eq!(price_values.len(), 2);
395 assert_eq!(
396 get_raw_price(price_values.value(0)),
397 (100.10 * FIXED_SCALAR) as PriceRaw
398 );
399 assert_eq!(
400 get_raw_price(price_values.value(1)),
401 (101.20 * FIXED_SCALAR) as PriceRaw
402 );
403
404 assert_eq!(size_values.len(), 2);
405 assert_eq!(
406 get_raw_price(size_values.value(0)),
407 (100.0 * FIXED_SCALAR) as PriceRaw
408 );
409 assert_eq!(
410 get_raw_price(size_values.value(1)),
411 (200.0 * FIXED_SCALAR) as PriceRaw
412 );
413 assert_eq!(order_id_values.len(), 2);
414 assert_eq!(order_id_values.value(0), 1);
415 assert_eq!(order_id_values.value(1), 2);
416 assert_eq!(flags_values.len(), 2);
417 assert_eq!(flags_values.value(0), 0);
418 assert_eq!(flags_values.value(1), 1);
419 assert_eq!(sequence_values.len(), 2);
420 assert_eq!(sequence_values.value(0), 1);
421 assert_eq!(sequence_values.value(1), 2);
422 assert_eq!(ts_event_values.len(), 2);
423 assert_eq!(ts_event_values.value(0), 1);
424 assert_eq!(ts_event_values.value(1), 2);
425 assert_eq!(ts_init_values.len(), 2);
426 assert_eq!(ts_init_values.value(0), 3);
427 assert_eq!(ts_init_values.value(1), 4);
428 }
429
430 #[rstest]
431 fn test_decode_batch() {
432 let instrument_id = InstrumentId::from("AAPL.XNAS");
433 let metadata = OrderBookDelta::get_metadata(&instrument_id, 2, 0);
434
435 let action = enum_dictionary_array([BookAction::Add, BookAction::Update]).unwrap();
436 let side = enum_dictionary_array([OrderSide::Buy, OrderSide::Buy]).unwrap();
437 let price = crate::arrow::test_support::decimal_array_from_bytes(vec![
438 &((101.10 * FIXED_SCALAR) as PriceRaw).to_le_bytes(),
439 &((101.20 * FIXED_SCALAR) as PriceRaw).to_le_bytes(),
440 ]);
441 let size = crate::arrow::test_support::decimal_array_from_bytes(vec![
442 &((10000.0 * FIXED_SCALAR) as PriceRaw).to_le_bytes(),
443 &((9000.0 * FIXED_SCALAR) as PriceRaw).to_le_bytes(),
444 ]);
445 let order_id = UInt64Array::from(vec![1, 2]);
446 let flags = UInt8Array::from(vec![0, 0]);
447 let sequence = UInt64Array::from(vec![1, 2]);
448 let ts_event = UInt64Array::from(vec![1, 2]);
449 let ts_init = UInt64Array::from(vec![3, 4]);
450
451 let record_batch = crate::arrow::record_batch_with_timestamps(
452 crate::arrow::schema_without_identifier_column(&OrderBookDelta::get_schema(Some(
453 metadata.clone(),
454 )))
455 .into(),
456 vec![
457 Arc::new(action),
458 Arc::new(side),
459 Arc::new(price),
460 Arc::new(size),
461 Arc::new(order_id),
462 Arc::new(flags),
463 Arc::new(sequence),
464 Arc::new(ts_event),
465 Arc::new(ts_init),
466 ],
467 )
468 .unwrap();
469
470 let decoded_data = OrderBookDelta::decode_batch(&metadata, record_batch).unwrap();
471 assert_eq!(decoded_data.len(), 2);
472 }
473
474 #[rstest]
475 fn test_decode_batch_rejects_null_timestamp_with_field_and_row() {
476 let instrument_id = InstrumentId::from("AAPL.XNAS");
477 let metadata = OrderBookDelta::get_metadata(&instrument_id, 2, 0);
478 let delta = OrderBookDelta {
479 instrument_id,
480 action: BookAction::Add,
481 order: BookOrder {
482 side: OrderSide::Buy.into(),
483 price: Price::from("100.10"),
484 size: Quantity::from(100),
485 order_id: 1,
486 },
487 flags: 0,
488 sequence: 1,
489 ts_event: 1.into(),
490 ts_init: 2.into(),
491 };
492 let encoded = OrderBookDelta::encode_batch(&metadata, &[delta]).unwrap();
493 let mut columns = encoded.columns().to_vec();
494 columns[8] = Arc::new(TimestampNanosecondArray::from(vec![None]).with_timezone("UTC"));
495 let fields = encoded
496 .schema()
497 .fields()
498 .iter()
499 .map(|field| {
500 if field.name() == "ts_init" {
501 Arc::new(field.as_ref().clone().with_nullable(true))
502 } else {
503 field.clone()
504 }
505 })
506 .collect::<Vec<_>>();
507 let schema = Arc::new(Schema::new_with_metadata(fields, metadata.clone()));
508 let batch = RecordBatch::try_new(schema, columns).unwrap();
509
510 let error = OrderBookDelta::decode_batch(&metadata, batch).unwrap_err();
511
512 assert!(error.to_string().contains("ts_init"));
513 assert!(error.to_string().contains("row 0"));
514 }
515
516 #[rstest]
517 fn test_decode_batch_rejects_null_required_integers_with_field_and_row() {
518 let instrument_id = InstrumentId::from("AAPL.XNAS");
519 let metadata = OrderBookDelta::get_metadata(&instrument_id, 2, 0);
520 let delta = OrderBookDelta {
521 instrument_id,
522 action: BookAction::Add,
523 order: BookOrder::new(
524 OrderSide::Buy,
525 Price::from("100.10"),
526 Quantity::from(100),
527 1,
528 ),
529 flags: 0,
530 sequence: 1,
531 ts_event: 1.into(),
532 ts_init: 2.into(),
533 };
534 let encoded = OrderBookDelta::encode_batch(&metadata, &[delta]).unwrap();
535 let corruptions: [(usize, &str, ArrayRef); 3] = [
536 (4, "order_id", Arc::new(UInt64Array::from(vec![None]))),
537 (5, "flags", Arc::new(UInt8Array::from(vec![None]))),
538 (6, "sequence", Arc::new(UInt64Array::from(vec![None]))),
539 ];
540
541 for (index, field, column) in corruptions {
542 let mut columns = encoded.columns().to_vec();
543 columns[index] = column;
544 let fields = encoded
545 .schema()
546 .fields()
547 .iter()
548 .map(|schema_field| {
549 if schema_field.name() == field {
550 Arc::new(schema_field.as_ref().clone().with_nullable(true))
551 } else {
552 schema_field.clone()
553 }
554 })
555 .collect::<Vec<_>>();
556 let schema = Arc::new(Schema::new_with_metadata(fields, metadata.clone()));
557 let batch = RecordBatch::try_new(schema, columns).unwrap();
558
559 let error = OrderBookDelta::decode_batch(&metadata, batch).unwrap_err();
560 assert!(error.to_string().contains(field));
561 assert!(error.to_string().contains("row 0"));
562 }
563 }
564
565 #[rstest]
566 fn test_decode_batch_with_undef_values() {
567 let instrument_id = InstrumentId::from("PLTR.XNAS");
568 let metadata = OrderBookDelta::get_metadata(&instrument_id, 2, 0);
569
570 let action = enum_dictionary_array([BookAction::Clear, BookAction::Add]).unwrap();
572 let side = enum_dictionary_array(["NO_ORDER_SIDE", "BUY"]).unwrap();
573 let price = crate::arrow::test_support::decimal_array_from_bytes(vec![
574 &PRICE_UNDEF.to_le_bytes(),
575 &((100.50 * FIXED_SCALAR) as PriceRaw).to_le_bytes(),
576 ]);
577 let size = crate::arrow::test_support::decimal_array_from_bytes(vec![
578 &QUANTITY_UNDEF.to_le_bytes(),
579 &((1000.0 * FIXED_SCALAR) as PriceRaw).to_le_bytes(),
580 ]);
581 let order_id = UInt64Array::from(vec![0, 1]);
582 let flags = UInt8Array::from(vec![0, 0]);
583 let sequence = UInt64Array::from(vec![1, 2]);
584 let ts_event = UInt64Array::from(vec![1, 2]);
585 let ts_init = UInt64Array::from(vec![3, 4]);
586
587 let record_batch = crate::arrow::record_batch_with_timestamps(
588 crate::arrow::schema_without_identifier_column(&OrderBookDelta::get_schema(Some(
589 metadata.clone(),
590 )))
591 .into(),
592 vec![
593 Arc::new(action),
594 Arc::new(side),
595 Arc::new(price),
596 Arc::new(size),
597 Arc::new(order_id),
598 Arc::new(flags),
599 Arc::new(sequence),
600 Arc::new(ts_event),
601 Arc::new(ts_init),
602 ],
603 )
604 .unwrap();
605
606 let decoded_data = OrderBookDelta::decode_batch(&metadata, record_batch).unwrap();
607 assert_eq!(decoded_data.len(), 2);
608 assert_eq!(decoded_data[0].order.price.raw(), PRICE_UNDEF);
609 assert_eq!(decoded_data[0].order.price.precision, 0);
610 assert_eq!(decoded_data[0].order.size.raw(), QUANTITY_UNDEF);
611 assert_eq!(decoded_data[0].order.size.precision, 0);
612 assert_eq!(decoded_data[1].order.price.precision, 2);
613 assert_eq!(decoded_data[1].order.size.precision, 0);
614 }
615
616 #[rstest]
617 fn test_decode_batch_invalid_price_returns_error() {
618 let instrument_id = InstrumentId::from("AAPL.XNAS");
619 let metadata = OrderBookDelta::get_metadata(&instrument_id, 2, 0);
620
621 let action = enum_dictionary_array([BookAction::Add]).unwrap();
622 let side = enum_dictionary_array([OrderSide::Buy]).unwrap();
623
624 let invalid_price: PriceRaw = PriceRaw::MAX - 1000;
625 let price = crate::arrow::test_support::decimal_array_from_bytes(vec![
626 &invalid_price.to_le_bytes(),
627 ]);
628 let size = crate::arrow::test_support::decimal_array_from_bytes(vec![
629 &((100.0 * FIXED_SCALAR) as QuantityRaw).to_le_bytes(),
630 ]);
631 let order_id = UInt64Array::from(vec![1]);
632 let flags = UInt8Array::from(vec![0]);
633 let sequence = UInt64Array::from(vec![1]);
634 let ts_event = UInt64Array::from(vec![1]);
635 let ts_init = UInt64Array::from(vec![2]);
636
637 let record_batch = crate::arrow::record_batch_with_timestamps(
638 crate::arrow::schema_without_identifier_column(&OrderBookDelta::get_schema(Some(
639 metadata.clone(),
640 )))
641 .into(),
642 vec![
643 Arc::new(action),
644 Arc::new(side),
645 Arc::new(price),
646 Arc::new(size),
647 Arc::new(order_id),
648 Arc::new(flags),
649 Arc::new(sequence),
650 Arc::new(ts_event),
651 Arc::new(ts_init),
652 ],
653 )
654 .unwrap();
655
656 let result = OrderBookDelta::decode_batch(&metadata, record_batch);
657 assert!(result.is_err());
658 let err = result.unwrap_err();
659 assert!(
660 err.to_string().contains("price") && err.to_string().contains("row 0"),
661 "Expected price error at row 0, was: {err}"
662 );
663 }
664
665 #[rstest]
666 fn test_decode_batch_invalid_action_returns_error() {
667 let instrument_id = InstrumentId::from("AAPL.XNAS");
668 let metadata = OrderBookDelta::get_metadata(&instrument_id, 2, 0);
669
670 let action = enum_dictionary_array(["INVALID"]).unwrap();
671 let side = enum_dictionary_array([OrderSide::Buy]).unwrap();
672 let price = crate::arrow::test_support::decimal_array_from_bytes(vec![
673 &((100.0 * FIXED_SCALAR) as PriceRaw).to_le_bytes(),
674 ]);
675 let size = crate::arrow::test_support::decimal_array_from_bytes(vec![
676 &((100.0 * FIXED_SCALAR) as QuantityRaw).to_le_bytes(),
677 ]);
678 let order_id = UInt64Array::from(vec![1]);
679 let flags = UInt8Array::from(vec![0]);
680 let sequence = UInt64Array::from(vec![1]);
681 let ts_event = UInt64Array::from(vec![1]);
682 let ts_init = UInt64Array::from(vec![2]);
683
684 let record_batch = crate::arrow::record_batch_with_timestamps(
685 crate::arrow::schema_without_identifier_column(&OrderBookDelta::get_schema(Some(
686 metadata.clone(),
687 )))
688 .into(),
689 vec![
690 Arc::new(action),
691 Arc::new(side),
692 Arc::new(price),
693 Arc::new(size),
694 Arc::new(order_id),
695 Arc::new(flags),
696 Arc::new(sequence),
697 Arc::new(ts_event),
698 Arc::new(ts_init),
699 ],
700 )
701 .unwrap();
702
703 let result = OrderBookDelta::decode_batch(&metadata, record_batch);
704 assert!(result.is_err());
705 let err = result.unwrap_err();
706 assert!(
707 err.to_string().contains("BookAction"),
708 "Expected BookAction error, was: {err}"
709 );
710 }
711
712 #[rstest]
713 fn test_decode_batch_missing_instrument_id_returns_error() {
714 let instrument_id = InstrumentId::from("AAPL.XNAS");
715 let mut metadata = OrderBookDelta::get_metadata(&instrument_id, 2, 0);
716 metadata.remove(KEY_INSTRUMENT_ID);
717
718 let action = enum_dictionary_array([BookAction::Add]).unwrap();
719 let side = enum_dictionary_array([OrderSide::Buy]).unwrap();
720 let price = crate::arrow::test_support::decimal_array_from_bytes(vec![
721 &((100.0 * FIXED_SCALAR) as PriceRaw).to_le_bytes(),
722 ]);
723 let size = crate::arrow::test_support::decimal_array_from_bytes(vec![
724 &((100.0 * FIXED_SCALAR) as QuantityRaw).to_le_bytes(),
725 ]);
726 let order_id = UInt64Array::from(vec![1]);
727 let flags = UInt8Array::from(vec![0]);
728 let sequence = UInt64Array::from(vec![1]);
729 let ts_event = UInt64Array::from(vec![1]);
730 let ts_init = UInt64Array::from(vec![2]);
731
732 let record_batch = crate::arrow::record_batch_with_timestamps(
733 crate::arrow::schema_without_identifier_column(&OrderBookDelta::get_schema(Some(
734 metadata.clone(),
735 )))
736 .into(),
737 vec![
738 Arc::new(action),
739 Arc::new(side),
740 Arc::new(price),
741 Arc::new(size),
742 Arc::new(order_id),
743 Arc::new(flags),
744 Arc::new(sequence),
745 Arc::new(ts_event),
746 Arc::new(ts_init),
747 ],
748 )
749 .unwrap();
750
751 let result = OrderBookDelta::decode_batch(&metadata, record_batch);
752 assert!(result.is_err());
753 let err = result.unwrap_err();
754 assert!(
755 err.to_string().contains("instrument_id"),
756 "Expected missing instrument_id error, was: {err}"
757 );
758 }
759
760 #[rstest]
761 fn test_encode_decode_round_trip() {
762 let instrument_id = InstrumentId::from("AAPL.XNAS");
763 let metadata = OrderBookDelta::get_metadata(&instrument_id, 2, 0);
764
765 let delta1 = OrderBookDelta {
766 instrument_id,
767 action: BookAction::Add,
768 order: BookOrder {
769 side: OrderSide::Buy.into(),
770 price: Price::from("100.10"),
771 size: Quantity::from(100),
772 order_id: 1,
773 },
774 flags: 0,
775 sequence: 1,
776 ts_event: 1_000_000_000.into(),
777 ts_init: 1_000_000_001.into(),
778 };
779
780 let delta2 = OrderBookDelta {
781 instrument_id,
782 action: BookAction::Update,
783 order: BookOrder {
784 side: OrderSide::Sell.into(),
785 price: Price::from("101.20"),
786 size: Quantity::from(200),
787 order_id: 2,
788 },
789 flags: 1,
790 sequence: 2,
791 ts_event: 2_000_000_000.into(),
792 ts_init: 2_000_000_001.into(),
793 };
794
795 let original = vec![delta1, delta2];
796 let record_batch = OrderBookDelta::encode_batch(&metadata, &original).unwrap();
797 let decoded = OrderBookDelta::decode_batch(&metadata, record_batch).unwrap();
798
799 assert_eq!(decoded.len(), original.len());
800 for (orig, dec) in original.iter().zip(decoded.iter()) {
801 assert_eq!(dec.instrument_id, orig.instrument_id);
802 assert_eq!(dec.action, orig.action);
803 assert_eq!(dec.order.side, orig.order.side);
804 assert_eq!(dec.order.price, orig.order.price);
805 assert_eq!(dec.order.size, orig.order.size);
806 assert_eq!(dec.order.order_id, orig.order.order_id);
807 assert_eq!(dec.flags, orig.flags);
808 assert_eq!(dec.sequence, orig.sequence);
809 assert_eq!(dec.ts_event, orig.ts_event);
810 assert_eq!(dec.ts_init, orig.ts_init);
811 }
812 }
813}