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