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