1use std::{collections::HashMap, str::FromStr, sync::Arc};
19
20use arrow::{
21 array::{
22 Array, BinaryArray, BinaryBuilder, Float64Array, StringArray, StringBuilder, UInt8Array,
23 UInt64Array,
24 },
25 datatypes::{DataType, Field, Schema},
26 error::ArrowError,
27 record_batch::RecordBatch,
28};
29use nautilus_core::Params;
30use nautilus_model::{
31 identifiers::{InstrumentId, Symbol},
32 instruments::betting::BettingInstrument,
33 types::{money::Money, price::Price, quantity::Quantity},
34};
35use rust_decimal::Decimal;
36use ustr::Ustr;
37
38use super::KEY_CLASS;
39use crate::arrow::{
40 ArrowSchemaProvider, EncodeToRecordBatch, EncodingError, KEY_INSTRUMENT_ID,
41 KEY_PRICE_PRECISION, KEY_SIZE_PRECISION, extract_column, extract_column_by_name_or_index,
42 extract_optional_string_column_by_name, optional_ustr_value,
43};
44
45impl ArrowSchemaProvider for BettingInstrument {
46 fn get_schema(metadata: Option<HashMap<String, String>>) -> Schema {
47 let fields = vec![
48 Field::new("id", DataType::Utf8, false),
49 Field::new("raw_symbol", DataType::Utf8, false),
50 Field::new("venue_name", DataType::Utf8, false),
51 Field::new("currency", DataType::Utf8, false),
52 Field::new("event_type_id", DataType::UInt64, false),
53 Field::new("event_type_name", DataType::Utf8, false),
54 Field::new("competition_id", DataType::UInt64, false),
55 Field::new("competition_name", DataType::Utf8, false),
56 Field::new("event_id", DataType::UInt64, false),
57 Field::new("event_name", DataType::Utf8, false),
58 Field::new("event_country_code", DataType::Utf8, false),
59 Field::new("event_open_date", DataType::UInt64, false),
60 Field::new("betting_type", DataType::Utf8, false),
61 Field::new("market_id", DataType::Utf8, false),
62 Field::new("market_name", DataType::Utf8, false),
63 Field::new("market_type", DataType::Utf8, false),
64 Field::new("market_start_time", DataType::UInt64, false),
65 Field::new("selection_id", DataType::UInt64, false),
66 Field::new("selection_name", DataType::Utf8, false),
67 Field::new("selection_handicap", DataType::Float64, false),
68 Field::new("price_precision", DataType::UInt8, false),
69 Field::new("size_precision", DataType::UInt8, false),
70 Field::new("price_increment", DataType::Utf8, false),
71 Field::new("size_increment", DataType::Utf8, false),
72 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),
79 Field::new("margin_maint", DataType::Utf8, false),
80 Field::new("maker_fee", DataType::Utf8, false),
81 Field::new("taker_fee", DataType::Utf8, false),
82 Field::new("tick_scheme", DataType::Utf8, true),
83 Field::new("info", DataType::Binary, true), Field::new("ts_event", DataType::UInt64, false),
85 Field::new("ts_init", DataType::UInt64, false),
86 ];
87
88 let mut final_metadata = HashMap::new();
89 final_metadata.insert(KEY_CLASS.to_string(), "BettingInstrument".to_string());
90
91 if let Some(meta) = metadata {
92 final_metadata.extend(meta);
93 }
94
95 Schema::new_with_metadata(fields, final_metadata)
96 }
97}
98
99impl EncodeToRecordBatch for BettingInstrument {
100 fn encode_batch(
101 #[allow(unused)] metadata: &HashMap<String, String>,
102 data: &[Self],
103 ) -> Result<RecordBatch, ArrowError> {
104 let mut id_builder = StringBuilder::new();
105 let mut raw_symbol_builder = StringBuilder::new();
106 let mut venue_name_builder = StringBuilder::new();
107 let mut currency_builder = StringBuilder::new();
108 let mut event_type_id_builder = UInt64Array::builder(data.len());
109 let mut event_type_name_builder = StringBuilder::new();
110 let mut competition_id_builder = UInt64Array::builder(data.len());
111 let mut competition_name_builder = StringBuilder::new();
112 let mut event_id_builder = UInt64Array::builder(data.len());
113 let mut event_name_builder = StringBuilder::new();
114 let mut event_country_code_builder = StringBuilder::new();
115 let mut event_open_date_builder = UInt64Array::builder(data.len());
116 let mut betting_type_builder = StringBuilder::new();
117 let mut market_id_builder = StringBuilder::new();
118 let mut market_name_builder = StringBuilder::new();
119 let mut market_type_builder = StringBuilder::new();
120 let mut market_start_time_builder = UInt64Array::builder(data.len());
121 let mut selection_id_builder = UInt64Array::builder(data.len());
122 let mut selection_name_builder = StringBuilder::new();
123 let mut selection_handicap_builder = Float64Array::builder(data.len());
124 let mut price_precision_builder = UInt8Array::builder(data.len());
125 let mut size_precision_builder = UInt8Array::builder(data.len());
126 let mut price_increment_builder = StringBuilder::new();
127 let mut size_increment_builder = StringBuilder::new();
128 let mut max_quantity_builder = StringBuilder::new();
129 let mut min_quantity_builder = StringBuilder::new();
130 let mut max_notional_builder = StringBuilder::new();
131 let mut min_notional_builder = StringBuilder::new();
132 let mut max_price_builder = StringBuilder::new();
133 let mut min_price_builder = StringBuilder::new();
134 let mut margin_init_builder = StringBuilder::new();
135 let mut margin_maint_builder = StringBuilder::new();
136 let mut maker_fee_builder = StringBuilder::new();
137 let mut taker_fee_builder = StringBuilder::new();
138 let mut tick_scheme_builder = StringBuilder::new();
139 let mut info_builder = BinaryBuilder::new();
140 let mut ts_event_builder = UInt64Array::builder(data.len());
141 let mut ts_init_builder = UInt64Array::builder(data.len());
142
143 for bi in data {
144 id_builder.append_value(bi.id.to_string());
145 raw_symbol_builder.append_value(bi.raw_symbol);
146 let venue_name = bi.id.venue.to_string();
148 venue_name_builder.append_value(venue_name);
149 currency_builder.append_value(bi.currency.to_string());
150 event_type_id_builder.append_value(bi.event_type_id);
151 event_type_name_builder.append_value(bi.event_type_name);
152 competition_id_builder.append_value(bi.competition_id);
153 competition_name_builder.append_value(bi.competition_name);
154 event_id_builder.append_value(bi.event_id);
155 event_name_builder.append_value(bi.event_name);
156 event_country_code_builder.append_value(bi.event_country_code);
157 event_open_date_builder.append_value(bi.event_open_date.as_u64());
158 betting_type_builder.append_value(bi.betting_type);
159 market_id_builder.append_value(bi.market_id);
160 market_name_builder.append_value(bi.market_name);
161 market_type_builder.append_value(bi.market_type);
162 market_start_time_builder.append_value(bi.market_start_time.as_u64());
163 selection_id_builder.append_value(bi.selection_id);
164 selection_name_builder.append_value(bi.selection_name);
165 selection_handicap_builder.append_value(bi.selection_handicap);
166 price_precision_builder.append_value(bi.price_precision);
167 size_precision_builder.append_value(bi.size_precision);
168 price_increment_builder.append_value(bi.price_increment.to_string());
169 size_increment_builder.append_value(bi.size_increment.to_string());
170
171 if let Some(max_quantity) = bi.max_quantity {
172 max_quantity_builder.append_value(max_quantity.to_string());
173 } else {
174 max_quantity_builder.append_null();
175 }
176
177 if let Some(min_quantity) = bi.min_quantity {
178 min_quantity_builder.append_value(min_quantity.to_string());
179 } else {
180 min_quantity_builder.append_null();
181 }
182
183 if let Some(max_notional) = bi.max_notional {
184 max_notional_builder.append_value(max_notional.to_string());
185 } else {
186 max_notional_builder.append_null();
187 }
188
189 if let Some(min_notional) = bi.min_notional {
190 min_notional_builder.append_value(min_notional.to_string());
191 } else {
192 min_notional_builder.append_null();
193 }
194
195 if let Some(max_price) = bi.max_price {
196 max_price_builder.append_value(max_price.to_string());
197 } else {
198 max_price_builder.append_null();
199 }
200
201 if let Some(min_price) = bi.min_price {
202 min_price_builder.append_value(min_price.to_string());
203 } else {
204 min_price_builder.append_null();
205 }
206
207 margin_init_builder.append_value(bi.margin_init.to_string());
208 margin_maint_builder.append_value(bi.margin_maint.to_string());
209 maker_fee_builder.append_value(bi.maker_fee.to_string());
210 taker_fee_builder.append_value(bi.taker_fee.to_string());
211
212 if let Some(tick_scheme) = bi.tick_scheme {
213 tick_scheme_builder.append_value(tick_scheme);
214 } else {
215 tick_scheme_builder.append_null();
216 }
217
218 if let Some(ref info) = bi.info {
220 match serde_json::to_vec(info) {
221 Ok(json_bytes) => {
222 info_builder.append_value(json_bytes);
223 }
224 Err(e) => {
225 return Err(ArrowError::InvalidArgumentError(format!(
226 "Failed to serialize info dict to JSON: {e}"
227 )));
228 }
229 }
230 } else {
231 info_builder.append_null();
232 }
233
234 ts_event_builder.append_value(bi.ts_event.as_u64());
235 ts_init_builder.append_value(bi.ts_init.as_u64());
236 }
237
238 let mut final_metadata = metadata.clone();
239 final_metadata.insert(KEY_CLASS.to_string(), "BettingInstrument".to_string());
240
241 RecordBatch::try_new(
242 Self::get_schema(Some(final_metadata)).into(),
243 vec![
244 Arc::new(id_builder.finish()),
245 Arc::new(raw_symbol_builder.finish()),
246 Arc::new(venue_name_builder.finish()),
247 Arc::new(currency_builder.finish()),
248 Arc::new(event_type_id_builder.finish()),
249 Arc::new(event_type_name_builder.finish()),
250 Arc::new(competition_id_builder.finish()),
251 Arc::new(competition_name_builder.finish()),
252 Arc::new(event_id_builder.finish()),
253 Arc::new(event_name_builder.finish()),
254 Arc::new(event_country_code_builder.finish()),
255 Arc::new(event_open_date_builder.finish()),
256 Arc::new(betting_type_builder.finish()),
257 Arc::new(market_id_builder.finish()),
258 Arc::new(market_name_builder.finish()),
259 Arc::new(market_type_builder.finish()),
260 Arc::new(market_start_time_builder.finish()),
261 Arc::new(selection_id_builder.finish()),
262 Arc::new(selection_name_builder.finish()),
263 Arc::new(selection_handicap_builder.finish()),
264 Arc::new(price_precision_builder.finish()),
265 Arc::new(size_precision_builder.finish()),
266 Arc::new(price_increment_builder.finish()),
267 Arc::new(size_increment_builder.finish()),
268 Arc::new(max_quantity_builder.finish()),
269 Arc::new(min_quantity_builder.finish()),
270 Arc::new(max_notional_builder.finish()),
271 Arc::new(min_notional_builder.finish()),
272 Arc::new(max_price_builder.finish()),
273 Arc::new(min_price_builder.finish()),
274 Arc::new(margin_init_builder.finish()),
275 Arc::new(margin_maint_builder.finish()),
276 Arc::new(maker_fee_builder.finish()),
277 Arc::new(taker_fee_builder.finish()),
278 Arc::new(tick_scheme_builder.finish()),
279 Arc::new(info_builder.finish()),
280 Arc::new(ts_event_builder.finish()),
281 Arc::new(ts_init_builder.finish()),
282 ],
283 )
284 }
285
286 fn metadata(&self) -> HashMap<String, String> {
287 let mut metadata = HashMap::new();
288 metadata.insert(KEY_INSTRUMENT_ID.to_string(), self.id.to_string());
289 metadata.insert(
290 KEY_PRICE_PRECISION.to_string(),
291 self.price_precision.to_string(),
292 );
293 metadata.insert(
294 KEY_SIZE_PRECISION.to_string(),
295 self.size_precision.to_string(),
296 );
297 metadata
298 }
299}
300
301pub fn decode_betting_instrument_batch(
311 #[allow(unused)] metadata: &HashMap<String, String>,
312 record_batch: &RecordBatch,
313) -> Result<Vec<BettingInstrument>, EncodingError> {
314 let cols = record_batch.columns();
315 let num_rows = record_batch.num_rows();
316
317 let id_values = extract_column::<StringArray>(cols, "id", 0, DataType::Utf8)?;
318 let raw_symbol_values = extract_column::<StringArray>(cols, "raw_symbol", 1, DataType::Utf8)?;
319 let _venue_name_values = extract_column::<StringArray>(cols, "venue_name", 2, DataType::Utf8)?; let currency_values = extract_column::<StringArray>(cols, "currency", 3, DataType::Utf8)?;
321 let event_type_id_values =
322 extract_column::<UInt64Array>(cols, "event_type_id", 4, DataType::UInt64)?;
323 let event_type_name_values =
324 extract_column::<StringArray>(cols, "event_type_name", 5, DataType::Utf8)?;
325 let competition_id_values =
326 extract_column::<UInt64Array>(cols, "competition_id", 6, DataType::UInt64)?;
327 let competition_name_values =
328 extract_column::<StringArray>(cols, "competition_name", 7, DataType::Utf8)?;
329 let event_id_values = extract_column::<UInt64Array>(cols, "event_id", 8, DataType::UInt64)?;
330 let event_name_values = extract_column::<StringArray>(cols, "event_name", 9, DataType::Utf8)?;
331 let event_country_code_values =
332 extract_column::<StringArray>(cols, "event_country_code", 10, DataType::Utf8)?;
333 let event_open_date_values =
334 extract_column::<UInt64Array>(cols, "event_open_date", 11, DataType::UInt64)?;
335 let betting_type_values =
336 extract_column::<StringArray>(cols, "betting_type", 12, DataType::Utf8)?;
337 let market_id_values = extract_column::<StringArray>(cols, "market_id", 13, DataType::Utf8)?;
338 let market_name_values =
339 extract_column::<StringArray>(cols, "market_name", 14, DataType::Utf8)?;
340 let market_type_values =
341 extract_column::<StringArray>(cols, "market_type", 15, DataType::Utf8)?;
342 let market_start_time_values =
343 extract_column::<UInt64Array>(cols, "market_start_time", 16, DataType::UInt64)?;
344 let selection_id_values =
345 extract_column::<UInt64Array>(cols, "selection_id", 17, DataType::UInt64)?;
346 let selection_name_values =
347 extract_column::<StringArray>(cols, "selection_name", 18, DataType::Utf8)?;
348 let selection_handicap_values =
349 extract_column::<Float64Array>(cols, "selection_handicap", 19, DataType::Float64)?;
350 let price_precision_values =
351 extract_column::<UInt8Array>(cols, "price_precision", 20, DataType::UInt8)?;
352 let size_precision_values =
353 extract_column::<UInt8Array>(cols, "size_precision", 21, DataType::UInt8)?;
354 let price_increment_values =
355 extract_column::<StringArray>(cols, "price_increment", 22, DataType::Utf8)?;
356 let size_increment_values =
357 extract_column::<StringArray>(cols, "size_increment", 23, DataType::Utf8)?;
358 let max_quantity_values = extract_optional_string_column_by_name(record_batch, "max_quantity")?;
359 let min_quantity_values = extract_optional_string_column_by_name(record_batch, "min_quantity")?;
360 let max_notional_values = extract_optional_string_column_by_name(record_batch, "max_notional")?;
361 let min_notional_values = extract_optional_string_column_by_name(record_batch, "min_notional")?;
362 let max_price_values = extract_optional_string_column_by_name(record_batch, "max_price")?;
363 let min_price_values = extract_optional_string_column_by_name(record_batch, "min_price")?;
364 let margin_init_values =
365 extract_column::<StringArray>(cols, "margin_init", 30, DataType::Utf8)?;
366 let margin_maint_values =
367 extract_column::<StringArray>(cols, "margin_maint", 31, DataType::Utf8)?;
368 let maker_fee_values = extract_column::<StringArray>(cols, "maker_fee", 32, DataType::Utf8)?;
369 let taker_fee_values = extract_column::<StringArray>(cols, "taker_fee", 33, DataType::Utf8)?;
370 let tick_scheme_values = extract_optional_string_column_by_name(record_batch, "tick_scheme")?;
371 let info_values =
372 extract_column_by_name_or_index::<BinaryArray>(record_batch, "info", 35, DataType::Binary)?;
373 let ts_event_values = extract_column_by_name_or_index::<UInt64Array>(
374 record_batch,
375 "ts_event",
376 36,
377 DataType::UInt64,
378 )?;
379 let ts_init_values = extract_column_by_name_or_index::<UInt64Array>(
380 record_batch,
381 "ts_init",
382 37,
383 DataType::UInt64,
384 )?;
385
386 let mut result = Vec::with_capacity(num_rows);
387
388 for i in 0..num_rows {
389 let id = InstrumentId::from_str(id_values.value(i))
390 .map_err(|e| EncodingError::ParseError("id", format!("row {i}: {e}")))?;
391 let raw_symbol = Symbol::from(raw_symbol_values.value(i));
392 let currency = super::decode_currency(
393 currency_values.value(i),
394 "currency",
395 "betting_instrument.currency",
396 i,
397 )?;
398 let event_type_id = event_type_id_values.value(i);
399 let event_type_name = Ustr::from(event_type_name_values.value(i));
400 let competition_id = competition_id_values.value(i);
401 let competition_name = Ustr::from(competition_name_values.value(i));
402 let event_id = event_id_values.value(i);
403 let event_name = Ustr::from(event_name_values.value(i));
404 let event_country_code = Ustr::from(event_country_code_values.value(i));
405 let event_open_date = nautilus_core::UnixNanos::from(event_open_date_values.value(i));
406 let betting_type = Ustr::from(betting_type_values.value(i));
407 let market_id = Ustr::from(market_id_values.value(i));
408 let market_name = Ustr::from(market_name_values.value(i));
409 let market_type = Ustr::from(market_type_values.value(i));
410 let market_start_time = nautilus_core::UnixNanos::from(market_start_time_values.value(i));
411 let selection_id = selection_id_values.value(i);
412 let selection_name = Ustr::from(selection_name_values.value(i));
413 let selection_handicap = selection_handicap_values.value(i);
414 let price_prec = price_precision_values.value(i);
415 let size_prec = size_precision_values.value(i);
416
417 let price_increment = Price::from_str(price_increment_values.value(i))
418 .map_err(|e| EncodingError::ParseError("price_increment", format!("row {i}: {e}")))?;
419 let size_increment = Quantity::from_str(size_increment_values.value(i))
420 .map_err(|e| EncodingError::ParseError("size_increment", format!("row {i}: {e}")))?;
421
422 let margin_init = Decimal::from_str(margin_init_values.value(i))
423 .map_err(|e| EncodingError::ParseError("margin_init", format!("row {i}: {e}")))?;
424 let margin_maint = Decimal::from_str(margin_maint_values.value(i))
425 .map_err(|e| EncodingError::ParseError("margin_maint", format!("row {i}: {e}")))?;
426 let maker_fee = Decimal::from_str(maker_fee_values.value(i))
427 .map_err(|e| EncodingError::ParseError("maker_fee", format!("row {i}: {e}")))?;
428 let taker_fee = Decimal::from_str(taker_fee_values.value(i))
429 .map_err(|e| EncodingError::ParseError("taker_fee", format!("row {i}: {e}")))?;
430
431 let info = if info_values.is_null(i) {
433 None
434 } else {
435 let info_bytes = info_values
436 .as_any()
437 .downcast_ref::<BinaryArray>()
438 .ok_or_else(|| EncodingError::ParseError("info", format!("row {i}: invalid type")))?
439 .value(i);
440
441 match serde_json::from_slice::<Params>(info_bytes) {
442 Ok(info_dict) => Some(info_dict),
443 Err(e) => {
444 return Err(EncodingError::ParseError(
445 "info",
446 format!("row {i}: failed to deserialize JSON: {e}"),
447 ));
448 }
449 }
450 };
451
452 let ts_event = nautilus_core::UnixNanos::from(ts_event_values.value(i));
453 let ts_init = nautilus_core::UnixNanos::from(ts_init_values.value(i));
454
455 let tick_scheme = optional_ustr_value(tick_scheme_values, i);
456
457 let max_notional = match max_notional_values {
458 Some(column) if !column.is_null(i) => {
459 Some(Money::from_str(column.value(i)).map_err(|e| {
460 EncodingError::ParseError("max_notional", format!("row {i}: {e}"))
461 })?)
462 }
463 _ => None,
464 };
465
466 let min_notional = match min_notional_values {
467 Some(column) if !column.is_null(i) => {
468 Some(Money::from_str(column.value(i)).map_err(|e| {
469 EncodingError::ParseError("min_notional", format!("row {i}: {e}"))
470 })?)
471 }
472 _ => None,
473 };
474
475 let betting_instrument = BettingInstrument::builder()
476 .instrument_id(id)
477 .raw_symbol(raw_symbol)
478 .event_type_id(event_type_id)
479 .event_type_name(event_type_name)
480 .competition_id(competition_id)
481 .competition_name(competition_name)
482 .event_id(event_id)
483 .event_name(event_name)
484 .event_country_code(event_country_code)
485 .event_open_date(event_open_date)
486 .betting_type(betting_type)
487 .market_id(market_id)
488 .market_name(market_name)
489 .market_type(market_type)
490 .market_start_time(market_start_time)
491 .selection_id(selection_id)
492 .selection_name(selection_name)
493 .selection_handicap(selection_handicap)
494 .currency(currency)
495 .price_precision(price_prec)
496 .size_precision(size_prec)
497 .price_increment(price_increment)
498 .size_increment(size_increment)
499 .maybe_max_quantity(super::optional_quantity_value(
500 max_quantity_values,
501 "max_quantity",
502 i,
503 )?)
504 .maybe_min_quantity(super::optional_quantity_value(
505 min_quantity_values,
506 "min_quantity",
507 i,
508 )?)
509 .maybe_max_notional(max_notional)
510 .maybe_min_notional(min_notional)
511 .maybe_max_price(super::optional_price_value(
512 max_price_values,
513 "max_price",
514 i,
515 )?)
516 .maybe_min_price(super::optional_price_value(
517 min_price_values,
518 "min_price",
519 i,
520 )?)
521 .margin_init(margin_init)
522 .margin_maint(margin_maint)
523 .maker_fee(maker_fee)
524 .taker_fee(taker_fee)
525 .maybe_tick_scheme(tick_scheme)
526 .maybe_info(info)
527 .ts_event(ts_event)
528 .ts_init(ts_init)
529 .build()
530 .map_err(|e| super::instrument_validation_error::<BettingInstrument>(i, e))?;
531
532 result.push(betting_instrument);
533 }
534
535 Ok(result)
536}
537
538#[cfg(test)]
539mod tests {
540 use std::{collections::HashMap, sync::Arc};
541
542 use arrow::{array::UInt8Array, record_batch::RecordBatch};
543 use nautilus_model::instruments::stubs::betting;
544 use rstest::rstest;
545
546 use super::*;
547 use crate::arrow::EncodeToRecordBatch;
548
549 const PRICE_PRECISION_COLUMN: usize = 20;
550 const SIZE_PRECISION_COLUMN: usize = 21;
551
552 fn betting_batch_with_precision(column_index: usize, precision: u8) -> RecordBatch {
553 betting_batch_with_precision_values(column_index, &[precision])
554 }
555
556 fn betting_batch_with_precision_values(column_index: usize, precisions: &[u8]) -> RecordBatch {
557 let instruments = vec![betting(); precisions.len()];
558 let batch = BettingInstrument::encode_batch(&HashMap::new(), &instruments).unwrap();
559 let mut columns = batch.columns().to_vec();
560 columns[column_index] = Arc::new(UInt8Array::from(precisions.to_vec()));
561
562 RecordBatch::try_new(batch.schema(), columns).unwrap()
563 }
564
565 #[rstest]
566 fn decode_betting_instrument_invalid_price_precision_returns_error() {
567 let batch = betting_batch_with_precision(PRICE_PRECISION_COLUMN, u8::MAX);
568 let error = decode_betting_instrument_batch(&HashMap::new(), &batch).unwrap_err();
569
570 match error {
571 EncodingError::ParseError(field, message) => {
572 assert_eq!(field, super::super::INSTRUMENT_VALIDATION_FIELD);
573 assert!(message.starts_with("row 0:"));
574 assert!(message.contains("price_increment"));
575 assert!(message.contains("precision"));
576 }
577 _ => panic!("Expected instrument parse error, was: {error}"),
578 }
579 }
580
581 #[rstest]
582 fn decode_betting_instrument_invalid_second_row_precision_reports_row_index() {
583 let batch = betting_batch_with_precision_values(PRICE_PRECISION_COLUMN, &[2, u8::MAX]);
584 let error = decode_betting_instrument_batch(&HashMap::new(), &batch).unwrap_err();
585
586 match error {
587 EncodingError::ParseError(field, message) => {
588 assert_eq!(field, super::super::INSTRUMENT_VALIDATION_FIELD);
589 assert!(message.starts_with("row 1:"));
590 assert!(message.contains("price_increment"));
591 assert!(message.contains("precision"));
592 }
593 _ => panic!("Expected instrument parse error, was: {error}"),
594 }
595 }
596
597 #[rstest]
598 fn decode_betting_instrument_invalid_size_precision_returns_error() {
599 let batch = betting_batch_with_precision(SIZE_PRECISION_COLUMN, u8::MAX);
600 let error = decode_betting_instrument_batch(&HashMap::new(), &batch).unwrap_err();
601
602 match error {
603 EncodingError::ParseError(field, message) => {
604 assert_eq!(field, super::super::INSTRUMENT_VALIDATION_FIELD);
605 assert!(message.starts_with("row 0:"));
606 assert!(message.contains("size_increment"));
607 assert!(message.contains("precision"));
608 }
609 _ => panic!("Expected instrument parse error, was: {error}"),
610 }
611 }
612
613 #[rstest]
614 fn decode_betting_instrument_invalid_default_price_increment_returns_error() {
615 let batch = betting_batch_with_precision(PRICE_PRECISION_COLUMN, 1);
616 let error = decode_betting_instrument_batch(&HashMap::new(), &batch).unwrap_err();
617
618 match error {
619 EncodingError::ParseError(field, message) => {
620 assert_eq!(field, super::super::INSTRUMENT_VALIDATION_FIELD);
621 assert!(message.starts_with("row 0:"));
622 assert!(message.contains("BettingInstrument"));
623 assert!(message.contains("price_increment"));
624 }
625 _ => panic!("Expected instrument parse error, was: {error}"),
626 }
627 }
628}