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