1use std::{borrow::Borrow, collections::HashMap, str::FromStr, sync::Arc};
19
20use arrow::{
21 array::{
22 Array, BooleanArray, BooleanBuilder, StringArray, StringBuilder, UInt8Array, UInt64Array,
23 },
24 datatypes::{DataType, Field, Schema},
25 error::ArrowError,
26 record_batch::RecordBatch,
27};
28use nautilus_core::Params;
29use nautilus_model::{
30 identifiers::{InstrumentId, Symbol},
31 instruments::crypto_future::CryptoFuture,
32 types::{money::Money, price::Price, quantity::Quantity},
33};
34use rust_decimal::Decimal;
35
36use crate::arrow::{
37 ArrowSchemaProvider, EncodeToRecordBatch, EncodingError, KEY_INSTRUMENT_ID,
38 KEY_PRICE_PRECISION, KEY_SIZE_PRECISION, extract_column, extract_column_by_name,
39 extract_optional_string_column_by_name, json_string_field, metadata_with_type_name,
40 optional_ustr_value, record_batch_with_timestamps, record_batch_with_u64_timestamps,
41 timestamp_data_type,
42};
43
44impl ArrowSchemaProvider for CryptoFuture {
45 fn get_schema(metadata: Option<HashMap<String, String>>) -> Schema {
46 let fields = vec![
47 Field::new("id", DataType::Utf8, false),
48 Field::new("raw_symbol", DataType::Utf8, false),
49 Field::new("underlying", DataType::Utf8, false),
50 Field::new("quote_currency", DataType::Utf8, false),
51 Field::new("settlement_currency", DataType::Utf8, false),
52 Field::new("is_inverse", DataType::Boolean, false),
53 Field::new("activation_ns", timestamp_data_type(), false),
54 Field::new("expiration_ns", timestamp_data_type(), false),
55 Field::new("price_precision", DataType::UInt8, false),
56 Field::new("size_precision", DataType::UInt8, false),
57 Field::new("price_increment", DataType::Utf8, false),
58 Field::new("size_increment", DataType::Utf8, false),
59 Field::new("multiplier", DataType::Utf8, false),
60 Field::new("lot_size", DataType::Utf8, true),
61 Field::new("max_quantity", DataType::Utf8, true), Field::new("min_quantity", DataType::Utf8, true), Field::new("max_notional", DataType::Utf8, true), Field::new("min_notional", DataType::Utf8, true), Field::new("max_price", DataType::Utf8, true), Field::new("min_price", DataType::Utf8, true), Field::new("margin_init", DataType::Utf8, false),
68 Field::new("margin_maint", DataType::Utf8, false),
69 Field::new("tick_scheme", DataType::Utf8, true),
70 json_string_field("info", true),
71 Field::new("ts_event", timestamp_data_type(), false),
72 Field::new("ts_init", timestamp_data_type(), false),
73 ];
74
75 Schema::new_with_metadata(fields, metadata_with_type_name("CryptoFuture", metadata))
76 }
77}
78
79impl EncodeToRecordBatch for CryptoFuture {
80 fn encode_batch<T>(
81 #[allow(unused)] metadata: &HashMap<String, String>,
82 data: &[T],
83 ) -> Result<RecordBatch, ArrowError>
84 where
85 T: std::borrow::Borrow<Self>,
86 {
87 let mut id_builder = StringBuilder::new();
88 let mut raw_symbol_builder = StringBuilder::new();
89 let mut underlying_builder = StringBuilder::new();
90 let mut quote_currency_builder = StringBuilder::new();
91 let mut settlement_currency_builder = StringBuilder::new();
92 let mut is_inverse_builder = BooleanBuilder::new();
93 let mut activation_ns_builder = UInt64Array::builder(data.len());
94 let mut expiration_ns_builder = UInt64Array::builder(data.len());
95 let mut price_precision_builder = UInt8Array::builder(data.len());
96 let mut size_precision_builder = UInt8Array::builder(data.len());
97 let mut price_increment_builder = StringBuilder::new();
98 let mut size_increment_builder = StringBuilder::new();
99 let mut multiplier_builder = StringBuilder::new();
100 let mut lot_size_builder = StringBuilder::new();
101 let mut max_quantity_builder = StringBuilder::new();
102 let mut min_quantity_builder = StringBuilder::new();
103 let mut max_notional_builder = StringBuilder::new();
104 let mut min_notional_builder = StringBuilder::new();
105 let mut max_price_builder = StringBuilder::new();
106 let mut min_price_builder = StringBuilder::new();
107 let mut margin_init_builder = StringBuilder::new();
108 let mut margin_maint_builder = StringBuilder::new();
109 let mut tick_scheme_builder = StringBuilder::new();
110 let mut info_builder = StringBuilder::new();
111 let mut ts_event_builder = UInt64Array::builder(data.len());
112 let mut ts_init_builder = UInt64Array::builder(data.len());
113
114 for cf in data.iter().map(Borrow::borrow) {
115 id_builder.append_value(cf.id.to_string());
116 raw_symbol_builder.append_value(cf.raw_symbol);
117 underlying_builder.append_value(cf.underlying.to_string());
118 quote_currency_builder.append_value(cf.quote_currency.to_string());
119 settlement_currency_builder.append_value(cf.settlement_currency.to_string());
120 is_inverse_builder.append_value(cf.is_inverse);
121 activation_ns_builder.append_value(cf.activation_ns.as_u64());
122 expiration_ns_builder.append_value(cf.expiration_ns.as_u64());
123 price_precision_builder.append_value(cf.price_precision);
124 size_precision_builder.append_value(cf.size_precision);
125 price_increment_builder.append_value(cf.price_increment.to_string());
126 size_increment_builder.append_value(cf.size_increment.to_string());
127 multiplier_builder.append_value(cf.multiplier.to_string());
128 lot_size_builder.append_value(cf.lot_size.to_string());
129
130 if let Some(max_qty) = cf.max_quantity {
131 max_quantity_builder.append_value(max_qty.to_string());
132 } else {
133 max_quantity_builder.append_null();
134 }
135
136 if let Some(min_qty) = cf.min_quantity {
137 min_quantity_builder.append_value(min_qty.to_string());
138 } else {
139 min_quantity_builder.append_null();
140 }
141
142 if let Some(max_not) = cf.max_notional {
143 max_notional_builder.append_value(max_not.to_string());
144 } else {
145 max_notional_builder.append_null();
146 }
147
148 if let Some(min_not) = cf.min_notional {
149 min_notional_builder.append_value(min_not.to_string());
150 } else {
151 min_notional_builder.append_null();
152 }
153
154 if let Some(max_p) = cf.max_price {
155 max_price_builder.append_value(max_p.to_string());
156 } else {
157 max_price_builder.append_null();
158 }
159
160 if let Some(min_p) = cf.min_price {
161 min_price_builder.append_value(min_p.to_string());
162 } else {
163 min_price_builder.append_null();
164 }
165
166 margin_init_builder.append_value(cf.margin_init.to_string());
167 margin_maint_builder.append_value(cf.margin_maint.to_string());
168
169 if let Some(tick_scheme) = cf.tick_scheme {
170 tick_scheme_builder.append_value(tick_scheme);
171 } else {
172 tick_scheme_builder.append_null();
173 }
174
175 if let Some(ref info) = cf.info {
176 match serde_json::to_string(info) {
177 Ok(json) => {
178 info_builder.append_value(json);
179 }
180 Err(e) => {
181 return Err(ArrowError::InvalidArgumentError(format!(
182 "Failed to serialize info dict to JSON: {e}"
183 )));
184 }
185 }
186 } else {
187 info_builder.append_null();
188 }
189
190 ts_event_builder.append_value(cf.ts_event.as_u64());
191 ts_init_builder.append_value(cf.ts_init.as_u64());
192 }
193
194 record_batch_with_timestamps(
195 Self::get_schema(Some(metadata.clone())).into(),
196 vec![
197 Arc::new(id_builder.finish()),
198 Arc::new(raw_symbol_builder.finish()),
199 Arc::new(underlying_builder.finish()),
200 Arc::new(quote_currency_builder.finish()),
201 Arc::new(settlement_currency_builder.finish()),
202 Arc::new(is_inverse_builder.finish()),
203 Arc::new(activation_ns_builder.finish()),
204 Arc::new(expiration_ns_builder.finish()),
205 Arc::new(price_precision_builder.finish()),
206 Arc::new(size_precision_builder.finish()),
207 Arc::new(price_increment_builder.finish()),
208 Arc::new(size_increment_builder.finish()),
209 Arc::new(multiplier_builder.finish()),
210 Arc::new(lot_size_builder.finish()),
211 Arc::new(max_quantity_builder.finish()),
212 Arc::new(min_quantity_builder.finish()),
213 Arc::new(max_notional_builder.finish()),
214 Arc::new(min_notional_builder.finish()),
215 Arc::new(max_price_builder.finish()),
216 Arc::new(min_price_builder.finish()),
217 Arc::new(margin_init_builder.finish()),
218 Arc::new(margin_maint_builder.finish()),
219 Arc::new(tick_scheme_builder.finish()),
220 Arc::new(info_builder.finish()),
221 Arc::new(ts_event_builder.finish()),
222 Arc::new(ts_init_builder.finish()),
223 ],
224 )
225 }
226
227 fn metadata(&self) -> HashMap<String, String> {
228 let mut metadata = HashMap::new();
229 metadata.insert(KEY_INSTRUMENT_ID.to_string(), self.id.to_string());
230 metadata.insert(
231 KEY_PRICE_PRECISION.to_string(),
232 self.price_precision.to_string(),
233 );
234 metadata.insert(
235 KEY_SIZE_PRECISION.to_string(),
236 self.size_precision.to_string(),
237 );
238 metadata
239 }
240}
241
242pub fn decode_crypto_future_batch(
252 #[allow(unused)] metadata: &HashMap<String, String>,
253 record_batch: &RecordBatch,
254) -> Result<Vec<CryptoFuture>, EncodingError> {
255 let record_batch = record_batch_with_u64_timestamps(record_batch)?;
256 let record_batch = &record_batch;
257 let cols = record_batch.columns();
258 let num_rows = record_batch.num_rows();
259
260 let id_values = extract_column::<StringArray>(cols, "id", 0, DataType::Utf8)?;
261 let raw_symbol_values = extract_column::<StringArray>(cols, "raw_symbol", 1, DataType::Utf8)?;
262 let underlying_values = extract_column::<StringArray>(cols, "underlying", 2, DataType::Utf8)?;
263 let quote_currency_values =
264 extract_column::<StringArray>(cols, "quote_currency", 3, DataType::Utf8)?;
265 let settlement_currency_values =
266 extract_column::<StringArray>(cols, "settlement_currency", 4, DataType::Utf8)?;
267 let is_inverse_values =
268 extract_column::<BooleanArray>(cols, "is_inverse", 5, DataType::Boolean)?;
269 let activation_ns_values =
270 extract_column::<UInt64Array>(cols, "activation_ns", 6, DataType::UInt64)?;
271 let expiration_ns_values =
272 extract_column::<UInt64Array>(cols, "expiration_ns", 7, DataType::UInt64)?;
273 let price_precision_values =
274 extract_column::<UInt8Array>(cols, "price_precision", 8, DataType::UInt8)?;
275 let size_precision_values =
276 extract_column::<UInt8Array>(cols, "size_precision", 9, DataType::UInt8)?;
277 let price_increment_values =
278 extract_column::<StringArray>(cols, "price_increment", 10, DataType::Utf8)?;
279 let size_increment_values =
280 extract_column::<StringArray>(cols, "size_increment", 11, DataType::Utf8)?;
281 let multiplier_values = extract_column::<StringArray>(cols, "multiplier", 12, DataType::Utf8)?;
282 let lot_size_values = record_batch
283 .schema()
284 .index_of("lot_size")
285 .ok()
286 .map(|index| extract_column::<StringArray>(cols, "lot_size", index, DataType::Utf8))
287 .transpose()?;
288 let lot_size_offset = usize::from(lot_size_values.is_some());
289 let max_quantity_values = cols
290 .get(13 + lot_size_offset)
291 .ok_or_else(|| EncodingError::MissingColumn("max_quantity", 13 + lot_size_offset))?;
292 let min_quantity_values = cols
293 .get(14 + lot_size_offset)
294 .ok_or_else(|| EncodingError::MissingColumn("min_quantity", 14 + lot_size_offset))?;
295 let max_notional_values = cols
296 .get(15 + lot_size_offset)
297 .ok_or_else(|| EncodingError::MissingColumn("max_notional", 15 + lot_size_offset))?;
298 let min_notional_values = cols
299 .get(16 + lot_size_offset)
300 .ok_or_else(|| EncodingError::MissingColumn("min_notional", 16 + lot_size_offset))?;
301 let max_price_values = cols
302 .get(17 + lot_size_offset)
303 .ok_or_else(|| EncodingError::MissingColumn("max_price", 17 + lot_size_offset))?;
304 let min_price_values = cols
305 .get(18 + lot_size_offset)
306 .ok_or_else(|| EncodingError::MissingColumn("min_price", 18 + lot_size_offset))?;
307 let margin_init_values =
308 extract_column::<StringArray>(cols, "margin_init", 19 + lot_size_offset, DataType::Utf8)?;
309 let margin_maint_values =
310 extract_column::<StringArray>(cols, "margin_maint", 20 + lot_size_offset, DataType::Utf8)?;
311 let tick_scheme_values = extract_optional_string_column_by_name(record_batch, "tick_scheme")?;
312 let info_values = extract_column_by_name::<StringArray>(record_batch, "info", DataType::Utf8)?;
313 let ts_event_values =
314 extract_column_by_name::<UInt64Array>(record_batch, "ts_event", DataType::UInt64)?;
315 let ts_init_values =
316 extract_column_by_name::<UInt64Array>(record_batch, "ts_init", DataType::UInt64)?;
317
318 let mut result = Vec::with_capacity(num_rows);
319
320 for i in 0..num_rows {
321 let id = InstrumentId::from_str(id_values.value(i))
322 .map_err(|e| EncodingError::ParseError("id", format!("row {i}: {e}")))?;
323 let raw_symbol = Symbol::from(raw_symbol_values.value(i));
324 let underlying = super::decode_currency(
325 underlying_values.value(i),
326 "underlying",
327 "crypto_future.underlying",
328 i,
329 )?;
330 let quote_currency = super::decode_currency(
331 quote_currency_values.value(i),
332 "quote_currency",
333 "crypto_future.quote_currency",
334 i,
335 )?;
336 let settlement_currency = super::decode_currency(
337 settlement_currency_values.value(i),
338 "settlement_currency",
339 "crypto_future.settlement_currency",
340 i,
341 )?;
342 let is_inverse = is_inverse_values.value(i);
343 let activation_ns = nautilus_core::UnixNanos::from(activation_ns_values.value(i));
344 let expiration_ns = nautilus_core::UnixNanos::from(expiration_ns_values.value(i));
345 let price_prec = price_precision_values.value(i);
346 let size_prec = size_precision_values.value(i);
347
348 let price_increment = Price::from_str(price_increment_values.value(i))
349 .map_err(|e| EncodingError::ParseError("price_increment", format!("row {i}: {e}")))?;
350 let size_increment = Quantity::from_str(size_increment_values.value(i))
351 .map_err(|e| EncodingError::ParseError("size_increment", format!("row {i}: {e}")))?;
352 let multiplier = Quantity::from_str(multiplier_values.value(i))
353 .map_err(|e| EncodingError::ParseError("multiplier", format!("row {i}: {e}")))?;
354 let lot_size =
355 if let Some(values) = lot_size_values {
356 if values.is_null(i) {
357 None
358 } else {
359 Some(Quantity::from_str(values.value(i)).map_err(|e| {
360 EncodingError::ParseError("lot_size", format!("row {i}: {e}"))
361 })?)
362 }
363 } else {
364 None
365 };
366
367 let max_quantity =
368 if max_quantity_values.is_null(i) {
369 None
370 } else {
371 let max_qty_str = max_quantity_values
372 .as_any()
373 .downcast_ref::<StringArray>()
374 .ok_or_else(|| {
375 EncodingError::ParseError("max_quantity", format!("row {i}: invalid type"))
376 })?
377 .value(i);
378 Some(Quantity::from_str(max_qty_str).map_err(|e| {
379 EncodingError::ParseError("max_quantity", format!("row {i}: {e}"))
380 })?)
381 };
382
383 let min_quantity =
384 if min_quantity_values.is_null(i) {
385 None
386 } else {
387 let min_qty_str = min_quantity_values
388 .as_any()
389 .downcast_ref::<StringArray>()
390 .ok_or_else(|| {
391 EncodingError::ParseError("min_quantity", format!("row {i}: invalid type"))
392 })?
393 .value(i);
394 Some(Quantity::from_str(min_qty_str).map_err(|e| {
395 EncodingError::ParseError("min_quantity", format!("row {i}: {e}"))
396 })?)
397 };
398
399 let max_notional =
400 if max_notional_values.is_null(i) {
401 None
402 } else {
403 let max_not_str = max_notional_values
404 .as_any()
405 .downcast_ref::<StringArray>()
406 .ok_or_else(|| {
407 EncodingError::ParseError("max_notional", format!("row {i}: invalid type"))
408 })?
409 .value(i);
410 Some(Money::from_str(max_not_str).map_err(|e| {
411 EncodingError::ParseError("max_notional", format!("row {i}: {e}"))
412 })?)
413 };
414
415 let min_notional =
416 if min_notional_values.is_null(i) {
417 None
418 } else {
419 let min_not_str = min_notional_values
420 .as_any()
421 .downcast_ref::<StringArray>()
422 .ok_or_else(|| {
423 EncodingError::ParseError("min_notional", format!("row {i}: invalid type"))
424 })?
425 .value(i);
426 Some(Money::from_str(min_not_str).map_err(|e| {
427 EncodingError::ParseError("min_notional", format!("row {i}: {e}"))
428 })?)
429 };
430
431 let max_price = if max_price_values.is_null(i) {
432 None
433 } else {
434 let max_p_str = max_price_values
435 .as_any()
436 .downcast_ref::<StringArray>()
437 .ok_or_else(|| {
438 EncodingError::ParseError("max_price", format!("row {i}: invalid type"))
439 })?
440 .value(i);
441 Some(
442 Price::from_str(max_p_str)
443 .map_err(|e| EncodingError::ParseError("max_price", format!("row {i}: {e}")))?,
444 )
445 };
446
447 let min_price = if min_price_values.is_null(i) {
448 None
449 } else {
450 let min_p_str = min_price_values
451 .as_any()
452 .downcast_ref::<StringArray>()
453 .ok_or_else(|| {
454 EncodingError::ParseError("min_price", format!("row {i}: invalid type"))
455 })?
456 .value(i);
457 Some(
458 Price::from_str(min_p_str)
459 .map_err(|e| EncodingError::ParseError("min_price", format!("row {i}: {e}")))?,
460 )
461 };
462
463 let margin_init = Decimal::from_str(margin_init_values.value(i))
464 .map_err(|e| EncodingError::ParseError("margin_init", format!("row {i}: {e}")))?;
465 let margin_maint = Decimal::from_str(margin_maint_values.value(i))
466 .map_err(|e| EncodingError::ParseError("margin_maint", format!("row {i}: {e}")))?;
467
468 let info = if info_values.is_null(i) {
469 None
470 } else {
471 let info_json = info_values
472 .as_any()
473 .downcast_ref::<StringArray>()
474 .ok_or_else(|| EncodingError::ParseError("info", format!("row {i}: invalid type")))?
475 .value(i);
476
477 match serde_json::from_str::<Params>(info_json) {
478 Ok(info_dict) => Some(info_dict),
479 Err(e) => {
480 return Err(EncodingError::ParseError(
481 "info",
482 format!("row {i}: failed to deserialize JSON: {e}"),
483 ));
484 }
485 }
486 };
487
488 let ts_event = nautilus_core::UnixNanos::from(ts_event_values.value(i));
489 let ts_init = nautilus_core::UnixNanos::from(ts_init_values.value(i));
490
491 let tick_scheme = optional_ustr_value(tick_scheme_values, i);
492
493 let crypto_future = CryptoFuture::builder()
494 .instrument_id(id)
495 .raw_symbol(raw_symbol)
496 .underlying(underlying)
497 .quote_currency(quote_currency)
498 .settlement_currency(settlement_currency)
499 .is_inverse(is_inverse)
500 .activation_ns(activation_ns)
501 .expiration_ns(expiration_ns)
502 .price_precision(price_prec)
503 .size_precision(size_prec)
504 .price_increment(price_increment)
505 .size_increment(size_increment)
506 .multiplier(multiplier)
507 .maybe_lot_size(lot_size)
508 .maybe_max_quantity(max_quantity)
509 .maybe_min_quantity(min_quantity)
510 .maybe_max_notional(max_notional)
511 .maybe_min_notional(min_notional)
512 .maybe_max_price(max_price)
513 .maybe_min_price(min_price)
514 .margin_init(margin_init)
515 .margin_maint(margin_maint)
516 .maybe_tick_scheme(tick_scheme)
517 .maybe_info(info)
518 .ts_event(ts_event)
519 .ts_init(ts_init)
520 .build()
521 .map_err(|e| super::instrument_validation_error::<CryptoFuture>(i, e))?;
522
523 result.push(crypto_future);
524 }
525
526 Ok(result)
527}