1use std::{borrow::Borrow, collections::HashMap, sync::Arc};
19
20use arrow::{
21 array::{
22 Array, ArrayRef, BooleanArray, Float64Array, Float64Builder, Int32Array, ListArray,
23 ListBuilder, StringArray, UInt64Array,
24 },
25 datatypes::{DataType, Field, Schema},
26 error::ArrowError,
27 record_batch::RecordBatch,
28};
29use nautilus_model::{
30 data::{
31 CustomData, CustomDataTrait, Data,
32 greeks::{GreeksData, OptionGreekValues, PortfolioGreeks, YieldCurve},
33 },
34 identifiers::InstrumentId,
35};
36
37use super::{
38 ArrowSchemaProvider, DecodeDataFromRecordBatch, DecodeTypedFromRecordBatch,
39 EncodeToRecordBatch, EncodingError, KEY_INSTRUMENT_ID, StringColumnRef, extract_column_by_name,
40 extract_column_string, identifier_array_from_display, metadata_with_type_name,
41 record_batch_with_timestamps, record_batch_with_u64_timestamps, timestamp_data_type,
42};
43
44impl ArrowSchemaProvider for GreeksData {
45 fn get_schema(metadata: Option<HashMap<String, String>>) -> Schema {
46 let mut fields = vec![
47 Field::new("ts_init", timestamp_data_type(), false),
48 Field::new("ts_event", timestamp_data_type(), false),
49 Field::new("instrument_id", DataType::Utf8, false),
50 Field::new("is_call", DataType::Boolean, false),
51 Field::new("strike", DataType::Float64, false),
52 Field::new("expiry", DataType::Int32, false),
53 Field::new("expiry_in_days", DataType::Int32, false),
54 Field::new("expiry_in_years", DataType::Float64, false),
55 Field::new("multiplier", DataType::Float64, false),
56 Field::new("quantity", DataType::Float64, false),
57 Field::new("underlying_price", DataType::Float64, false),
58 Field::new("interest_rate", DataType::Float64, false),
59 Field::new("cost_of_carry", DataType::Float64, false),
60 Field::new("vol", DataType::Float64, false),
61 Field::new("pnl", DataType::Float64, false),
62 Field::new("price", DataType::Float64, false),
63 ];
64
65 fields.extend(greek_fields());
66 fields.push(Field::new("itm_prob", DataType::Float64, false));
67 Schema::new_with_metadata(fields, metadata_with_type_name("GreeksData", metadata))
68 }
69}
70
71impl EncodeToRecordBatch for GreeksData {
72 fn encode_batch<T: Borrow<Self>>(
73 metadata: &HashMap<String, String>,
74 data: &[T],
75 ) -> Result<RecordBatch, ArrowError> {
76 let mut columns: Vec<ArrayRef> = vec![
77 Arc::new(UInt64Array::from_iter_values(
78 data.iter().map(|value| value.borrow().ts_init.as_u64()),
79 )),
80 Arc::new(UInt64Array::from_iter_values(
81 data.iter().map(|value| value.borrow().ts_event.as_u64()),
82 )),
83 Arc::new(identifier_array_from_display(
84 data.iter().map(|value| value.borrow().instrument_id),
85 )),
86 Arc::new(BooleanArray::from(
87 data.iter()
88 .map(|value| value.borrow().is_call)
89 .collect::<Vec<_>>(),
90 )),
91 Arc::new(Float64Array::from_iter_values(
92 data.iter().map(|value| value.borrow().strike),
93 )),
94 Arc::new(Int32Array::from_iter_values(
95 data.iter().map(|value| value.borrow().expiry),
96 )),
97 Arc::new(Int32Array::from_iter_values(
98 data.iter().map(|value| value.borrow().expiry_in_days),
99 )),
100 Arc::new(Float64Array::from_iter_values(
101 data.iter().map(|value| value.borrow().expiry_in_years),
102 )),
103 Arc::new(Float64Array::from_iter_values(
104 data.iter().map(|value| value.borrow().multiplier),
105 )),
106 Arc::new(Float64Array::from_iter_values(
107 data.iter().map(|value| value.borrow().quantity),
108 )),
109 Arc::new(Float64Array::from_iter_values(
110 data.iter().map(|value| value.borrow().underlying_price),
111 )),
112 Arc::new(Float64Array::from_iter_values(
113 data.iter().map(|value| value.borrow().interest_rate),
114 )),
115 Arc::new(Float64Array::from_iter_values(
116 data.iter().map(|value| value.borrow().cost_of_carry),
117 )),
118 Arc::new(Float64Array::from_iter_values(
119 data.iter().map(|value| value.borrow().vol),
120 )),
121 Arc::new(Float64Array::from_iter_values(
122 data.iter().map(|value| value.borrow().pnl),
123 )),
124 Arc::new(Float64Array::from_iter_values(
125 data.iter().map(|value| value.borrow().price),
126 )),
127 ];
128
129 columns.extend(greek_columns(
130 data.iter().map(|value| value.borrow().greeks),
131 ));
132 columns.push(Arc::new(Float64Array::from_iter_values(
133 data.iter().map(|value| value.borrow().itm_prob),
134 )));
135 record_batch_with_timestamps(Arc::new(Self::get_schema(Some(metadata.clone()))), columns)
136 }
137
138 fn metadata(&self) -> HashMap<String, String> {
139 let mut metadata = metadata_with_type_name("GreeksData", None);
140 metadata.insert(
141 KEY_INSTRUMENT_ID.to_string(),
142 self.instrument_id.to_string(),
143 );
144 metadata
145 }
146}
147
148impl DecodeTypedFromRecordBatch for GreeksData {
149 fn decode_typed_batch(
150 _metadata: &HashMap<String, String>,
151 batch: RecordBatch,
152 ) -> Result<Vec<Self>, EncodingError> {
153 let batch = record_batch_with_u64_timestamps(&batch)?;
154 let ts_init = required_column::<UInt64Array>(&batch, "ts_init", DataType::UInt64)?;
155 let ts_event = required_column::<UInt64Array>(&batch, "ts_event", DataType::UInt64)?;
156 let instrument_id = required_string_column(&batch, "instrument_id")?;
157 let is_call = required_column::<BooleanArray>(&batch, "is_call", DataType::Boolean)?;
158 let strike = required_column::<Float64Array>(&batch, "strike", DataType::Float64)?;
159 let expiry = required_column::<Int32Array>(&batch, "expiry", DataType::Int32)?;
160 let expiry_in_days =
161 required_column::<Int32Array>(&batch, "expiry_in_days", DataType::Int32)?;
162 let expiry_in_years =
163 required_column::<Float64Array>(&batch, "expiry_in_years", DataType::Float64)?;
164 let multiplier = required_column::<Float64Array>(&batch, "multiplier", DataType::Float64)?;
165 let quantity = required_column::<Float64Array>(&batch, "quantity", DataType::Float64)?;
166 let underlying_price =
167 required_column::<Float64Array>(&batch, "underlying_price", DataType::Float64)?;
168 let interest_rate =
169 required_column::<Float64Array>(&batch, "interest_rate", DataType::Float64)?;
170 let cost_of_carry =
171 required_column::<Float64Array>(&batch, "cost_of_carry", DataType::Float64)?;
172 let vol = required_column::<Float64Array>(&batch, "vol", DataType::Float64)?;
173 let pnl = required_column::<Float64Array>(&batch, "pnl", DataType::Float64)?;
174 let price = required_column::<Float64Array>(&batch, "price", DataType::Float64)?;
175 let itm_prob = required_column::<Float64Array>(&batch, "itm_prob", DataType::Float64)?;
176 let greeks = decode_greeks(&batch)?;
177
178 (0..batch.num_rows())
179 .map(|row| {
180 Ok(Self {
181 instrument_id: instrument_id
182 .value(row)
183 .parse::<InstrumentId>()
184 .map_err(|e| EncodingError::ParseError("instrument_id", e.to_string()))?,
185 is_call: is_call.value(row),
186 strike: strike.value(row),
187 expiry: expiry.value(row),
188 expiry_in_days: expiry_in_days.value(row),
189 expiry_in_years: expiry_in_years.value(row),
190 multiplier: multiplier.value(row),
191 quantity: quantity.value(row),
192 underlying_price: underlying_price.value(row),
193 interest_rate: interest_rate.value(row),
194 cost_of_carry: cost_of_carry.value(row),
195 vol: vol.value(row),
196 pnl: pnl.value(row),
197 price: price.value(row),
198 greeks: greeks[row],
199 itm_prob: itm_prob.value(row),
200 ts_event: ts_event.value(row).into(),
201 ts_init: ts_init.value(row).into(),
202 })
203 })
204 .collect()
205 }
206}
207
208impl ArrowSchemaProvider for PortfolioGreeks {
209 fn get_schema(metadata: Option<HashMap<String, String>>) -> Schema {
210 let mut fields = vec![
211 Field::new("ts_init", timestamp_data_type(), false),
212 Field::new("ts_event", timestamp_data_type(), false),
213 Field::new("pnl", DataType::Float64, false),
214 Field::new("price", DataType::Float64, false),
215 ];
216
217 fields.extend(greek_fields());
218 Schema::new_with_metadata(fields, metadata_with_type_name("PortfolioGreeks", metadata))
219 }
220}
221
222impl EncodeToRecordBatch for PortfolioGreeks {
223 fn encode_batch<T: Borrow<Self>>(
224 metadata: &HashMap<String, String>,
225 data: &[T],
226 ) -> Result<RecordBatch, ArrowError> {
227 let mut columns: Vec<ArrayRef> = vec![
228 Arc::new(UInt64Array::from_iter_values(
229 data.iter().map(|value| value.borrow().ts_init.as_u64()),
230 )),
231 Arc::new(UInt64Array::from_iter_values(
232 data.iter().map(|value| value.borrow().ts_event.as_u64()),
233 )),
234 Arc::new(Float64Array::from_iter_values(
235 data.iter().map(|value| value.borrow().pnl),
236 )),
237 Arc::new(Float64Array::from_iter_values(
238 data.iter().map(|value| value.borrow().price),
239 )),
240 ];
241
242 columns.extend(greek_columns(
243 data.iter().map(|value| value.borrow().greeks),
244 ));
245 record_batch_with_timestamps(Arc::new(Self::get_schema(Some(metadata.clone()))), columns)
246 }
247
248 fn metadata(&self) -> HashMap<String, String> {
249 metadata_with_type_name("PortfolioGreeks", None)
250 }
251}
252
253impl DecodeTypedFromRecordBatch for PortfolioGreeks {
254 fn decode_typed_batch(
255 _metadata: &HashMap<String, String>,
256 batch: RecordBatch,
257 ) -> Result<Vec<Self>, EncodingError> {
258 let batch = record_batch_with_u64_timestamps(&batch)?;
259 let ts_init = required_column::<UInt64Array>(&batch, "ts_init", DataType::UInt64)?;
260 let ts_event = required_column::<UInt64Array>(&batch, "ts_event", DataType::UInt64)?;
261 let pnl = required_column::<Float64Array>(&batch, "pnl", DataType::Float64)?;
262 let price = required_column::<Float64Array>(&batch, "price", DataType::Float64)?;
263 let greeks = decode_greeks(&batch)?;
264
265 (0..batch.num_rows())
266 .map(|row| {
267 Ok(Self {
268 pnl: pnl.value(row),
269 price: price.value(row),
270 greeks: greeks[row],
271 ts_event: ts_event.value(row).into(),
272 ts_init: ts_init.value(row).into(),
273 })
274 })
275 .collect()
276 }
277}
278
279impl ArrowSchemaProvider for YieldCurve {
280 fn get_schema(metadata: Option<HashMap<String, String>>) -> Schema {
281 let fields = vec![
282 Field::new("ts_init", timestamp_data_type(), false),
283 Field::new("ts_event", timestamp_data_type(), false),
284 Field::new("curve_name", DataType::Utf8, false),
285 Field::new("tenors", float_list_data_type(), false),
286 Field::new("interest_rates", float_list_data_type(), false),
287 ];
288 Schema::new_with_metadata(
289 fields,
290 metadata_with_type_name(Self::type_name_static(), metadata),
291 )
292 }
293}
294
295impl EncodeToRecordBatch for YieldCurve {
296 fn encode_batch<T: Borrow<Self>>(
297 metadata: &HashMap<String, String>,
298 data: &[T],
299 ) -> Result<RecordBatch, ArrowError> {
300 let columns: Vec<ArrayRef> = vec![
301 Arc::new(UInt64Array::from_iter_values(
302 data.iter().map(|value| value.borrow().ts_init.as_u64()),
303 )),
304 Arc::new(UInt64Array::from_iter_values(
305 data.iter().map(|value| value.borrow().ts_event.as_u64()),
306 )),
307 Arc::new(StringArray::from_iter_values(
308 data.iter().map(|value| value.borrow().curve_name.as_str()),
309 )),
310 Arc::new(float_lists(
311 data.iter().map(|value| value.borrow().tenors.as_slice()),
312 )),
313 Arc::new(float_lists(
314 data.iter()
315 .map(|value| value.borrow().interest_rates.as_slice()),
316 )),
317 ];
318 record_batch_with_timestamps(Arc::new(Self::get_schema(Some(metadata.clone()))), columns)
319 }
320
321 fn metadata(&self) -> HashMap<String, String> {
322 metadata_with_type_name(Self::type_name_static(), None)
323 }
324}
325
326impl DecodeTypedFromRecordBatch for YieldCurve {
327 fn decode_typed_batch(
328 _metadata: &HashMap<String, String>,
329 batch: RecordBatch,
330 ) -> Result<Vec<Self>, EncodingError> {
331 let batch = record_batch_with_u64_timestamps(&batch)?;
332 let ts_init = required_column::<UInt64Array>(&batch, "ts_init", DataType::UInt64)?;
333 let ts_event = required_column::<UInt64Array>(&batch, "ts_event", DataType::UInt64)?;
334 let curve_name = required_string_column(&batch, "curve_name")?;
335 let tenors = required_column::<ListArray>(&batch, "tenors", float_list_data_type())?;
336 let interest_rates =
337 required_column::<ListArray>(&batch, "interest_rates", float_list_data_type())?;
338 (0..batch.num_rows())
339 .map(|row| {
340 Ok(Self {
341 curve_name: curve_name.value(row).to_string(),
342 tenors: decode_float_list(tenors, "tenors", row)?,
343 interest_rates: decode_float_list(interest_rates, "interest_rates", row)?,
344 ts_event: ts_event.value(row).into(),
345 ts_init: ts_init.value(row).into(),
346 })
347 })
348 .collect()
349 }
350}
351
352impl DecodeDataFromRecordBatch for YieldCurve {
353 fn decode_data_batch(
354 metadata: &HashMap<String, String>,
355 record_batch: RecordBatch,
356 ) -> Result<Vec<Data>, EncodingError> {
357 Ok(Self::decode_typed_batch(metadata, record_batch)?
358 .into_iter()
359 .map(|curve| Data::Custom(CustomData::from_arc(Arc::new(curve))))
360 .collect())
361 }
362}
363
364fn greek_fields() -> Vec<Field> {
365 ["delta", "gamma", "vega", "theta", "rho"]
366 .into_iter()
367 .map(|name| Field::new(name, DataType::Float64, false))
368 .collect()
369}
370
371fn greek_columns(values: impl Iterator<Item = OptionGreekValues> + Clone) -> Vec<ArrayRef> {
372 vec![
373 Arc::new(Float64Array::from_iter_values(
374 values.clone().map(|value| value.delta),
375 )),
376 Arc::new(Float64Array::from_iter_values(
377 values.clone().map(|value| value.gamma),
378 )),
379 Arc::new(Float64Array::from_iter_values(
380 values.clone().map(|value| value.vega),
381 )),
382 Arc::new(Float64Array::from_iter_values(
383 values.clone().map(|value| value.theta),
384 )),
385 Arc::new(Float64Array::from_iter_values(
386 values.map(|value| value.rho),
387 )),
388 ]
389}
390
391fn decode_greeks(batch: &RecordBatch) -> Result<Vec<OptionGreekValues>, EncodingError> {
392 let delta = required_column::<Float64Array>(batch, "delta", DataType::Float64)?;
393 let gamma = required_column::<Float64Array>(batch, "gamma", DataType::Float64)?;
394 let vega = required_column::<Float64Array>(batch, "vega", DataType::Float64)?;
395 let theta = required_column::<Float64Array>(batch, "theta", DataType::Float64)?;
396 let rho = required_column::<Float64Array>(batch, "rho", DataType::Float64)?;
397 Ok((0..batch.num_rows())
398 .map(|row| OptionGreekValues {
399 delta: delta.value(row),
400 gamma: gamma.value(row),
401 vega: vega.value(row),
402 theta: theta.value(row),
403 rho: rho.value(row),
404 })
405 .collect())
406}
407
408fn required_column<'a, A: Array + 'static>(
409 batch: &'a RecordBatch,
410 name: &'static str,
411 data_type: DataType,
412) -> Result<&'a A, EncodingError> {
413 let column = extract_column_by_name::<A>(batch, name, data_type)?;
414
415 if column.null_count() != 0 {
416 return Err(EncodingError::ParseError(
417 name,
418 "required column contains nulls".to_string(),
419 ));
420 }
421
422 Ok(column)
423}
424
425fn required_string_column<'a>(
426 batch: &'a RecordBatch,
427 name: &'static str,
428) -> Result<StringColumnRef<'a>, EncodingError> {
429 let index = batch.schema().index_of(name)?;
430 let column = extract_column_string(batch.columns(), name, index)?;
431
432 if batch.column(index).logical_null_count() != 0 {
433 return Err(EncodingError::ParseError(
434 name,
435 "required column contains nulls".to_string(),
436 ));
437 }
438
439 Ok(column)
440}
441
442fn float_list_data_type() -> DataType {
443 DataType::List(Arc::new(Field::new("item", DataType::Float64, true)))
444}
445
446fn float_lists<'a>(values: impl Iterator<Item = &'a [f64]>) -> ListArray {
447 let mut builder = ListBuilder::new(Float64Builder::new());
448 for values in values {
449 builder.values().append_slice(values);
450 builder.append(true);
451 }
452
453 builder.finish()
454}
455
456fn decode_float_list(
457 values: &ListArray,
458 name: &'static str,
459 row: usize,
460) -> Result<Vec<f64>, EncodingError> {
461 let values = values.value(row);
462
463 let values = values
464 .as_any()
465 .downcast_ref::<Float64Array>()
466 .ok_or_else(|| {
467 EncodingError::ParseError(name, "expected Float64 list values".to_string())
468 })?;
469
470 if values.null_count() != 0 {
471 return Err(EncodingError::ParseError(
472 name,
473 format!("row {row}: list contains nulls"),
474 ));
475 }
476
477 Ok(values.values().to_vec())
478}
479
480#[cfg(test)]
481mod tests {
482 use nautilus_model::data::{decode_custom_from_arrow, encode_custom_to_arrow};
483 use rstest::rstest;
484
485 use super::*;
486
487 fn sensitivities() -> OptionGreekValues {
488 OptionGreekValues {
489 delta: 0.41,
490 gamma: 0.0012,
491 vega: 11.2,
492 theta: -2.3,
493 rho: 1.7,
494 }
495 }
496
497 fn assert_sensitivities(actual: OptionGreekValues, expected: OptionGreekValues) {
498 assert_eq!(actual.delta.to_bits(), expected.delta.to_bits());
499 assert_eq!(actual.gamma.to_bits(), expected.gamma.to_bits());
500 assert_eq!(actual.vega.to_bits(), expected.vega.to_bits());
501 assert_eq!(actual.theta.to_bits(), expected.theta.to_bits());
502 assert_eq!(actual.rho.to_bits(), expected.rho.to_bits());
503 }
504
505 #[rstest]
506 fn test_greeks_data_round_trip() {
507 let first = GreeksData::new(
508 "ESZ26 C7000.XCME".parse().unwrap(),
509 false,
510 7000.5,
511 20_261_218,
512 -68,
513 0.186,
514 50.0,
515 -3.0,
516 6950.25,
517 0.0425,
518 0.0125,
519 0.175,
520 12.5,
521 98.75,
522 sensitivities(),
523 0.62,
524 11.into(),
525 13.into(),
526 );
527 let mut second = first.clone();
528 second.instrument_id = "ESZ26 P6900.XCME".parse().unwrap();
529 second.is_call = true;
530 second.ts_init = 31.into();
531 second.ts_event = 29.into();
532 second.greeks.rho = -0.0;
533 second.greeks.delta = f64::from_bits(0x3fe0_0000_0000_0001);
534 let values = [first, second];
535 let batch = GreeksData::encode_batch(&values[0].metadata(), &values).unwrap();
536 let decoded =
537 GreeksData::decode_typed_batch(batch.schema().metadata(), batch.clone()).unwrap();
538
539 assert_eq!(
540 batch
541 .schema()
542 .field_with_name("expiry")
543 .unwrap()
544 .data_type(),
545 &DataType::Int32
546 );
547 assert_eq!(
548 batch.schema().field_with_name("rho").unwrap().data_type(),
549 &DataType::Float64
550 );
551 assert_eq!(decoded.len(), values.len());
552
553 for (actual, expected) in decoded.iter().zip(&values) {
554 assert_eq!(actual.instrument_id, expected.instrument_id);
555 assert_eq!(actual.is_call, expected.is_call);
556 assert_eq!(actual.expiry, expected.expiry);
557 assert_eq!(actual.expiry_in_days, expected.expiry_in_days);
558 assert_eq!(actual.ts_init, expected.ts_init);
559 assert_eq!(actual.ts_event, expected.ts_event);
560 macro_rules! assert_float { ($($field:ident),+) => { $(assert_eq!(actual.$field.to_bits(), expected.$field.to_bits(), stringify!($field));)+ }; }
561 assert_float!(
562 strike,
563 expiry_in_years,
564 multiplier,
565 quantity,
566 underlying_price,
567 interest_rate,
568 cost_of_carry,
569 vol,
570 pnl,
571 price,
572 itm_prob
573 );
574 assert_sensitivities(actual.greeks, expected.greeks);
575 }
576 }
577
578 #[rstest]
579 fn test_portfolio_greeks_round_trip() {
580 let values = [PortfolioGreeks {
581 pnl: -125.5,
582 price: 4310.25,
583 greeks: sensitivities(),
584 ts_event: 19.into(),
585 ts_init: 17.into(),
586 }];
587
588 let batch = PortfolioGreeks::encode_batch(&values[0].metadata(), &values).unwrap();
589 let decoded =
590 PortfolioGreeks::decode_typed_batch(batch.schema().metadata(), batch).unwrap();
591
592 assert_eq!(decoded.len(), values.len());
593 assert_eq!(decoded[0].ts_init, values[0].ts_init);
594 assert_eq!(decoded[0].ts_event, values[0].ts_event);
595 assert_eq!(decoded[0].pnl.to_bits(), values[0].pnl.to_bits());
596 assert_eq!(decoded[0].price.to_bits(), values[0].price.to_bits());
597 assert_sensitivities(decoded[0].greeks, values[0].greeks);
598 }
599
600 #[rstest]
601 fn test_yield_curve_registered_custom_data_round_trip() {
602 let curve = YieldCurve::new(
603 "USD-SOFR".to_string(),
604 vec![0.25, 1.0, 5.0],
605 vec![0.043, 0.041, 0.038],
606 23.into(),
607 29.into(),
608 );
609 let item: Arc<dyn CustomDataTrait> = Arc::new(curve.clone());
610
611 crate::ensure_custom_data_registered::<YieldCurve>();
612 let batch = encode_custom_to_arrow("YieldCurve", &[item])
613 .unwrap()
614 .unwrap();
615 let metadata = batch.schema().metadata().clone();
616 let decoded = decode_custom_from_arrow("YieldCurve", &metadata, batch)
617 .unwrap()
618 .unwrap();
619
620 let [Data::Custom(custom)] = decoded.as_slice() else {
621 panic!("expected one custom data row, was {decoded:?}");
622 };
623
624 assert_eq!(custom.data_type.type_name(), "YieldCurve");
625 assert_eq!(
626 custom.data.as_any().downcast_ref::<YieldCurve>(),
627 Some(&curve)
628 );
629 }
630
631 #[rstest]
632 #[case(vec![0.25, 1.0, 5.0], vec![0.043, 0.041, 0.038])]
633 #[case(vec![], vec![-0.0, f64::from_bits(0x3fe0_0000_0000_0001)])]
634 fn test_yield_curve_round_trip(#[case] tenors: Vec<f64>, #[case] interest_rates: Vec<f64>) {
635 let curve = YieldCurve::new(
636 "USD-SOFR".to_string(),
637 tenors,
638 interest_rates,
639 23.into(),
640 29.into(),
641 );
642 let batch =
643 YieldCurve::encode_batch(&curve.metadata(), std::slice::from_ref(&curve)).unwrap();
644 let decoded = YieldCurve::decode_typed_batch(batch.schema().metadata(), batch).unwrap();
645
646 assert_eq!(decoded.len(), 1);
647 assert_eq!(decoded[0].ts_init, curve.ts_init);
648 assert_eq!(decoded[0].ts_event, curve.ts_event);
649 assert_eq!(decoded[0].curve_name, curve.curve_name);
650 assert_eq!(
651 decoded[0]
652 .tenors
653 .iter()
654 .map(|v| v.to_bits())
655 .collect::<Vec<_>>(),
656 curve.tenors.iter().map(|v| v.to_bits()).collect::<Vec<_>>()
657 );
658 assert_eq!(
659 decoded[0]
660 .interest_rates
661 .iter()
662 .map(|v| v.to_bits())
663 .collect::<Vec<_>>(),
664 curve
665 .interest_rates
666 .iter()
667 .map(|v| v.to_bits())
668 .collect::<Vec<_>>()
669 );
670 }
671
672 #[rstest]
673 fn test_native_greeks_empty_batches() {
674 macro_rules! assert_empty {
675 ($type:ty) => {{
676 let batch = <$type>::encode_batch(&HashMap::new(), &Vec::<$type>::new()).unwrap();
677 let decoded =
678 <$type>::decode_typed_batch(batch.schema().metadata(), batch).unwrap();
679 assert!(decoded.is_empty());
680 }};
681 }
682 assert_empty!(GreeksData);
683 assert_empty!(PortfolioGreeks);
684 assert_empty!(YieldCurve);
685 }
686
687 #[rstest]
688 #[case("pnl")]
689 #[case("rho")]
690 fn test_portfolio_greeks_rejects_null_values(#[case] name: &'static str) {
691 let value = PortfolioGreeks {
692 pnl: -125.5,
693 price: 4310.25,
694 greeks: sensitivities(),
695 ts_event: 19.into(),
696 ts_init: 17.into(),
697 };
698
699 let batch = PortfolioGreeks::encode_batch(&value.metadata(), &[value]).unwrap();
700 let index = batch.schema().index_of(name).unwrap();
701 let mut fields = batch.schema().fields().to_vec();
702 fields[index] = Arc::new(fields[index].as_ref().clone().with_nullable(true));
703 let mut columns = batch.columns().to_vec();
704 columns[index] = Arc::new(Float64Array::from(vec![None::<f64>]));
705 let batch = RecordBatch::try_new(Arc::new(Schema::new(fields)), columns).unwrap();
706 let error = PortfolioGreeks::decode_typed_batch(&HashMap::new(), batch).unwrap_err();
707
708 assert!(
709 matches!(error, EncodingError::ParseError(field, ref message)
710 if field == name && message == "required column contains nulls")
711 );
712 }
713
714 #[rstest]
715 #[case(Some("USD-SOFR"))]
716 #[case(None)]
717 fn test_yield_curve_dictionary_name(#[case] name: Option<&str>) {
718 use arrow::{
719 array::{DictionaryArray, Int8Array},
720 datatypes::Int8Type,
721 };
722
723 let value = YieldCurve::new(
724 "USD-SOFR".to_string(),
725 vec![0.25],
726 vec![0.043],
727 23.into(),
728 29.into(),
729 );
730 let batch = YieldCurve::encode_batch(&value.metadata(), &[value]).unwrap();
731 let index = batch.schema().index_of("curve_name").unwrap();
732 let names = DictionaryArray::<Int8Type>::try_new(
733 Int8Array::from(vec![0]),
734 Arc::new(StringArray::from(vec![name])),
735 )
736 .unwrap();
737 let mut fields = batch.schema().fields().to_vec();
738 fields[index] = Arc::new(Field::new("curve_name", names.data_type().clone(), true));
739 let mut columns = batch.columns().to_vec();
740 columns[index] = Arc::new(names);
741 let batch = RecordBatch::try_new(Arc::new(Schema::new(fields)), columns).unwrap();
742 let decoded = YieldCurve::decode_typed_batch(&HashMap::new(), batch);
743
744 match name {
745 Some(name) => assert_eq!(decoded.unwrap()[0].curve_name, name),
746 None => assert!(
747 matches!(decoded.unwrap_err(), EncodingError::ParseError("curve_name", ref message) if message == "required column contains nulls")
748 ),
749 }
750 }
751
752 #[rstest]
753 #[case("curve_name")]
754 #[case("tenors")]
755 #[case("interest_rates")]
756 fn test_yield_curve_rejects_null_values(#[case] name: &'static str) {
757 let value = YieldCurve::new(
758 "USD-SOFR".to_string(),
759 vec![0.25],
760 vec![0.043],
761 23.into(),
762 29.into(),
763 );
764 let batch = YieldCurve::encode_batch(&value.metadata(), &[value]).unwrap();
765 let index = batch.schema().index_of(name).unwrap();
766 let mut fields = batch.schema().fields().to_vec();
767 let mut columns = batch.columns().to_vec();
768
769 let expected = if name == "curve_name" {
770 fields[index] = Arc::new(fields[index].as_ref().clone().with_nullable(true));
771 columns[index] = Arc::new(StringArray::from(vec![None::<&str>]));
772 "required column contains nulls"
773 } else {
774 let mut builder = ListBuilder::new(Float64Builder::new());
775 builder.values().append_null();
776 builder.append(true);
777 columns[index] = Arc::new(builder.finish());
778 "row 0: list contains nulls"
779 };
780
781 let batch = RecordBatch::try_new(Arc::new(Schema::new(fields)), columns).unwrap();
782 let error = YieldCurve::decode_typed_batch(&HashMap::new(), batch).unwrap_err();
783
784 assert!(
785 matches!(error, EncodingError::ParseError(field, ref message)
786 if field == name && message == expected)
787 );
788 }
789}