1use crate::expiry_times::{expiry_times, ExpiryTime};
4use crate::OptionType;
5use chrono::NaiveDate;
6use rust_decimal::Decimal;
7use rust_decimal_macros::dec;
8
9pub const CONTRACT_UNIT_MULTIPLIER: f64 = 1_000_000.0;
11
12pub const CONTRACT_UNIT_MULTIPLIER_DECIMAL: Decimal = dec!(1000000);
14
15pub const MAX_PRICE_SIGNIFICANT_FIGURES: usize = 5;
17
18pub const STRIKE_SCALE_1E8: i128 = 100_000_000;
20
21pub const STRIKE_SCALE_1E8_DECIMAL: Decimal = dec!(100000000);
23
24const MIN_YYYYMMDD: u64 = 10_000_101; const MAX_YYYYMMDD: u64 = 99_991_231; #[derive(Debug, Clone, PartialEq, Eq)]
29pub enum ExpiryDateConversionError {
30 InvalidExpiryCode { expiry: u64 },
32 InvalidExpiryDate { year: i32, month: u32, day: u32 },
34 InvalidExpiryTimestamp { expiry: u64, timestamp: i64 },
36}
37
38impl std::fmt::Display for ExpiryDateConversionError {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 match self {
41 Self::InvalidExpiryCode { expiry } => {
42 write!(f, "Invalid expiry code (expected YYYYMMDD): {}", expiry)
43 }
44 Self::InvalidExpiryDate { year, month, day } => write!(
45 f,
46 "Invalid expiry date components: {}-{}-{}",
47 year, month, day
48 ),
49 Self::InvalidExpiryTimestamp { expiry, timestamp } => write!(
50 f,
51 "Expiry timestamp must be non-negative unix time, got {} for code {}",
52 timestamp, expiry
53 ),
54 }
55 }
56}
57
58impl std::error::Error for ExpiryDateConversionError {}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum StrikeScaleError {
63 NonPositive { strike: Decimal },
65 Overflow { strike: Decimal },
67 PrecisionExceedsE8 { strike: Decimal },
69 IntegerConversion { strike: Decimal, scaled: Decimal },
71 ZeroScaled { strike: Decimal },
73}
74
75impl std::fmt::Display for StrikeScaleError {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 match self {
78 Self::NonPositive { strike } => write!(f, "Invalid strike {}: must be > 0", strike),
79 Self::Overflow { strike } => write!(f, "Strike scaling overflow for {}", strike),
80 Self::PrecisionExceedsE8 { strike } => write!(
81 f,
82 "Invalid strike {}: precision exceeds 8 decimals, cannot convert exactly to e8",
83 strike
84 ),
85 Self::IntegerConversion { strike, scaled } => write!(
86 f,
87 "Invalid strike {}: cannot convert scaled e8 value {} to unsigned integer",
88 strike, scaled
89 ),
90 Self::ZeroScaled { strike } => {
91 write!(f, "Invalid strike {}: strike_e8 resolved to zero", strike)
92 }
93 }
94 }
95}
96
97impl std::error::Error for StrikeScaleError {}
98
99pub fn expiry_date_to_timestamp_at_time_checked(
107 expiry: u64,
108 time: ExpiryTime,
109) -> Result<u64, ExpiryDateConversionError> {
110 if !(MIN_YYYYMMDD..=MAX_YYYYMMDD).contains(&expiry) {
111 return Err(ExpiryDateConversionError::InvalidExpiryCode { expiry });
112 }
113
114 let year = (expiry / 10_000) as i32;
115 let month = ((expiry % 10_000) / 100) as u32;
116 let day = (expiry % 100) as u32;
117
118 let date = NaiveDate::from_ymd_opt(year, month, day)
119 .ok_or(ExpiryDateConversionError::InvalidExpiryDate { year, month, day })?;
120 let timestamp = date
121 .and_hms_opt(time.hour, time.minute, 0)
122 .expect("ExpiryTime is validated at construction")
123 .and_utc()
124 .timestamp();
125
126 u64::try_from(timestamp)
127 .map_err(|_| ExpiryDateConversionError::InvalidExpiryTimestamp { expiry, timestamp })
128}
129
130pub fn expiry_date_to_timestamp_checked(
133 underlying: &str,
134 expiry: u64,
135) -> Result<u64, ExpiryDateConversionError> {
136 expiry_date_to_timestamp_at_time_checked(expiry, expiry_times().for_underlying(underlying))
137}
138
139pub fn strike_to_e8(strike: Decimal) -> Result<u128, StrikeScaleError> {
141 use rust_decimal::prelude::ToPrimitive;
142
143 if strike <= Decimal::ZERO {
144 return Err(StrikeScaleError::NonPositive { strike });
145 }
146
147 let scaled = strike
148 .checked_mul(STRIKE_SCALE_1E8_DECIMAL)
149 .ok_or(StrikeScaleError::Overflow { strike })?;
150
151 if !scaled.fract().is_zero() {
152 return Err(StrikeScaleError::PrecisionExceedsE8 { strike });
153 }
154
155 let strike_e8 = scaled
156 .to_u128()
157 .ok_or(StrikeScaleError::IntegerConversion { strike, scaled })?;
158 if strike_e8 == 0 {
159 return Err(StrikeScaleError::ZeroScaled { strike });
160 }
161
162 Ok(strike_e8)
163}
164
165pub fn expiry_date_to_timestamp(underlying: &str, expiry: u64) -> i64 {
176 expiry_date_to_timestamp_checked(underlying, expiry)
177 .ok()
178 .and_then(|timestamp| i64::try_from(timestamp).ok())
179 .unwrap_or(0)
180}
181
182pub fn to_human_readable(_symbol: &str, size: u64) -> f64 {
191 (size as f64) / CONTRACT_UNIT_MULTIPLIER
194}
195
196pub fn to_contract_units(_symbol: &str, size: f64) -> u64 {
205 (size * CONTRACT_UNIT_MULTIPLIER) as u64
208}
209
210pub fn to_human_readable_decimal(_symbol: &str, size: Decimal) -> Decimal {
219 size / CONTRACT_UNIT_MULTIPLIER_DECIMAL
220}
221
222pub fn to_contract_units_decimal(_symbol: &str, size: Decimal) -> Decimal {
231 size * CONTRACT_UNIT_MULTIPLIER_DECIMAL
232}
233
234#[derive(Debug, Clone)]
236pub struct DecimalConversionError {
237 pub value: Decimal,
239 pub context: &'static str,
241}
242
243impl std::fmt::Display for DecimalConversionError {
244 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245 write!(
246 f,
247 "Failed to convert Decimal {} to f64 in context: {}",
248 self.value, self.context
249 )
250 }
251}
252
253impl std::error::Error for DecimalConversionError {}
254
255pub fn decimal_to_f64_checked(
269 value: Decimal,
270 context: &'static str,
271) -> Result<f64, DecimalConversionError> {
272 use rust_decimal::prelude::ToPrimitive;
273 value
274 .to_f64()
275 .ok_or(DecimalConversionError { value, context })
276}
277
278pub fn count_significant_figures(price_str: &str) -> Result<usize, String> {
286 let trimmed = price_str.trim();
287
288 if trimmed.is_empty() {
290 return Err("Empty price string".to_string());
291 }
292
293 let unsigned = trimmed.trim_start_matches('+').trim_start_matches('-');
295
296 let valid_chars = unsigned.chars().all(|c| c.is_ascii_digit() || c == '.');
298 if !valid_chars {
299 return Err(format!("Invalid price string: {}", price_str));
300 }
301
302 if unsigned == "0" || unsigned == "0." || unsigned == "0.0" {
304 return Ok(1);
305 }
306
307 if unsigned.contains('.') {
309 let parts: Vec<&str> = unsigned.split('.').collect();
311 if parts.len() != 2 {
312 return Err(format!("Invalid decimal format: {}", price_str));
313 }
314
315 let integer_part = parts[0];
316 let decimal_part = parts[1];
317
318 let integer_trimmed = integer_part.trim_start_matches('0');
320
321 if integer_trimmed.is_empty() {
322 let first_nonzero = decimal_part.find(|c: char| c != '0');
324 match first_nonzero {
325 Some(pos) => {
326 Ok(decimal_part[pos..].len())
328 }
329 None => {
330 Ok(1)
332 }
333 }
334 } else {
335 Ok(integer_trimmed.len() + decimal_part.len())
337 }
338 } else {
339 let trimmed_leading = unsigned.trim_start_matches('0');
341
342 if trimmed_leading.is_empty() {
343 return Ok(1);
345 }
346
347 let trimmed_both = trimmed_leading.trim_end_matches('0');
349
350 if trimmed_both.is_empty() {
351 return Ok(1);
353 }
354
355 Ok(trimmed_both.len())
356 }
357}
358
359pub fn validate_price_precision(price: f64, max_sig_figs: usize) -> Result<(), String> {
368 let abs_price = price.abs();
373
374 if abs_price == 0.0 {
375 return Ok(()); }
377
378 let log10 = abs_price.log10().floor();
381 let decimal_places = if log10 >= 0.0 {
382 ((max_sig_figs as f64 - log10).max(0.0) + 3.0) as usize
385 } else {
386 ((-log10) as usize + max_sig_figs + 3).min(15)
389 };
390
391 let price_str = format!("{:.prec$}", price, prec = decimal_places)
393 .trim_end_matches('0')
394 .trim_end_matches('.')
395 .to_string();
396
397 let sig_figs = count_significant_figures(&price_str)?;
398
399 if sig_figs > max_sig_figs {
400 Err(format!(
401 "Price {} has {} significant figures, maximum allowed is {}",
402 price, sig_figs, max_sig_figs
403 ))
404 } else {
405 Ok(())
406 }
407}
408
409pub fn round_to_sig_figs(value: f64, sig_figs: usize) -> f64 {
418 if value == 0.0 || !value.is_finite() {
419 return value;
420 }
421
422 if sig_figs == 0 {
423 return 0.0;
424 }
425
426 let sign = value.signum();
428 let abs_value = value.abs();
429
430 let magnitude = abs_value.log10().floor();
432
433 let scale = 10_f64.powf((sig_figs as f64 - 1.0) - magnitude);
436
437 let scaled = abs_value * scale;
439 let rounded = scaled.round();
440
441 sign * (rounded / scale)
443}
444
445#[derive(Debug, Clone)]
447pub struct ParsedSymbol {
448 pub underlying: String,
449 pub expiry: u64,
450 pub strike: f64,
451 pub option_type: OptionType,
452}
453
454impl ParsedSymbol {
455 pub fn from_symbol(symbol: &str) -> Result<Self, String> {
459 let parts: Vec<&str> = symbol.split('-').collect();
461 if parts.len() != 4 {
462 return Err(format!("Invalid symbol format: {}", symbol));
463 }
464
465 let underlying = parts[0].to_string();
466
467 let expiry = if parts[1].chars().all(|c| c.is_ascii_digit()) {
469 parts[1]
471 .parse::<u64>()
472 .map_err(|_| format!("Invalid expiry: {}", parts[1]))?
473 } else {
474 parse_deribit_expiry(parts[1])
476 .ok_or_else(|| format!("Invalid expiry format: {}", parts[1]))?
477 };
478
479 let strike = parts[2]
480 .parse::<f64>()
481 .map_err(|_| format!("Invalid strike: {}", parts[2]))?;
482
483 let option_type = match parts[3] {
484 "C" | "c" => OptionType::Call,
485 "P" | "p" => OptionType::Put,
486 _ => return Err(format!("Invalid option type: {}", parts[3])),
487 };
488
489 Ok(ParsedSymbol {
490 underlying,
491 expiry,
492 strike,
493 option_type,
494 })
495 }
496}
497
498#[derive(Debug, Clone, Copy, PartialEq, Eq)]
500pub enum InstrumentKind {
501 Option,
502 Perp,
503}
504
505pub fn classify_instrument_symbol(symbol: &str) -> Result<InstrumentKind, String> {
506 if is_perp_symbol(symbol) {
507 return Ok(InstrumentKind::Perp);
508 }
509
510 if ParsedSymbol::from_symbol(symbol).is_ok() {
511 return Ok(InstrumentKind::Option);
512 }
513
514 Err(format!(
515 "cannot classify symbol {} as option or perp",
516 symbol
517 ))
518}
519
520pub fn is_option_symbol(symbol: &str) -> bool {
521 ParsedSymbol::from_symbol(symbol).is_ok()
522}
523
524pub fn is_perp_symbol(symbol: &str) -> bool {
525 symbol.ends_with("-PERP")
526}
527
528fn parse_deribit_expiry(expiry_str: &str) -> Option<u64> {
530 if expiry_str.len() < 5 {
532 return None;
533 }
534
535 let day_end = expiry_str.chars().position(|c| !c.is_ascii_digit())?;
537 let day = expiry_str[..day_end].parse::<u32>().ok()?;
538
539 let month_str = &expiry_str[day_end..];
541 if month_str.len() < 5 {
542 return None;
544 }
545 let month_abbr = &month_str[..3];
546 let month = match month_abbr {
547 "JAN" => 1,
548 "FEB" => 2,
549 "MAR" => 3,
550 "APR" => 4,
551 "MAY" => 5,
552 "JUN" => 6,
553 "JUL" => 7,
554 "AUG" => 8,
555 "SEP" => 9,
556 "OCT" => 10,
557 "NOV" => 11,
558 "DEC" => 12,
559 _ => return None,
560 };
561
562 let year_str = &month_str[3..];
564 let year = year_str.parse::<u32>().ok()?;
565 let full_year = 2000 + year;
566
567 Some(full_year as u64 * 10000 + month as u64 * 100 + day as u64)
569}
570
571pub fn get_timestamp_millis() -> u64 {
573 std::time::SystemTime::now()
574 .duration_since(std::time::SystemTime::UNIX_EPOCH)
575 .unwrap()
576 .as_millis() as u64
577}
578
579#[cfg(test)]
580mod tests {
581 use super::*;
582
583 #[test]
584 fn test_to_human_readable() {
585 assert_eq!(to_human_readable("BTC-PERP", 1_000_000), 1.0);
586 assert_eq!(to_human_readable("BTC-PERP", 500_000), 0.5);
587 assert_eq!(to_human_readable("BTC-PERP", 2_500_000), 2.5);
588 assert_eq!(to_human_readable("ETH-PERP", 1_000_000), 1.0);
589 }
590
591 #[test]
592 fn test_to_contract_units() {
593 assert_eq!(to_contract_units("BTC-PERP", 1.0), 1_000_000);
594 assert_eq!(to_contract_units("BTC-PERP", 0.5), 500_000);
595 assert_eq!(to_contract_units("BTC-PERP", 2.5), 2_500_000);
596 assert_eq!(to_contract_units("ETH-PERP", 1.0), 1_000_000);
597 }
598
599 #[test]
600 fn test_round_trip_conversion() {
601 let original = 1.5;
602 let contract = to_contract_units("BTC-PERP", original);
603 let human = to_human_readable("BTC-PERP", contract);
604 assert!((human - original).abs() < 0.0001);
605 }
606
607 #[test]
608 fn test_count_sig_figs_basic() {
609 assert_eq!(count_significant_figures("12345").unwrap(), 5);
610 assert_eq!(count_significant_figures("1234").unwrap(), 4);
611 assert_eq!(count_significant_figures("123").unwrap(), 3);
612 assert_eq!(count_significant_figures("12").unwrap(), 2);
613 assert_eq!(count_significant_figures("1").unwrap(), 1);
614 }
615
616 #[test]
617 fn test_count_sig_figs_decimals() {
618 assert_eq!(count_significant_figures("1.2345").unwrap(), 5);
619 assert_eq!(count_significant_figures("12.345").unwrap(), 5);
620 assert_eq!(count_significant_figures("123.45").unwrap(), 5);
621 assert_eq!(count_significant_figures("1234.5").unwrap(), 5);
622 }
623
624 #[test]
625 fn test_count_sig_figs_leading_zeros() {
626 assert_eq!(count_significant_figures("0.12345").unwrap(), 5);
627 assert_eq!(count_significant_figures("0.012345").unwrap(), 5);
628 assert_eq!(count_significant_figures("0.0012345").unwrap(), 5);
629 assert_eq!(count_significant_figures("0.00012345").unwrap(), 5);
630 assert_eq!(count_significant_figures("0.000012345").unwrap(), 5);
631 }
632
633 #[test]
634 fn test_count_sig_figs_trailing_zeros_before_decimal() {
635 assert_eq!(count_significant_figures("12300").unwrap(), 3);
636 assert_eq!(count_significant_figures("55500").unwrap(), 3);
637 assert_eq!(count_significant_figures("10000").unwrap(), 1);
638 assert_eq!(count_significant_figures("100").unwrap(), 1);
639 }
640
641 #[test]
642 fn test_count_sig_figs_trailing_zeros_after_decimal() {
643 assert_eq!(count_significant_figures("123.00").unwrap(), 5);
644 assert_eq!(count_significant_figures("100.00").unwrap(), 5);
645 assert_eq!(count_significant_figures("1.0000").unwrap(), 5);
646 assert_eq!(count_significant_figures("10.000").unwrap(), 5);
647 }
648
649 #[test]
650 fn test_validate_price_precision_valid() {
651 assert!(validate_price_precision(12345.0, 5).is_ok());
652 assert!(validate_price_precision(1234.5, 5).is_ok());
653 assert!(validate_price_precision(123.45, 5).is_ok());
654 assert!(validate_price_precision(12.345, 5).is_ok());
655 assert!(validate_price_precision(1.2345, 5).is_ok());
656 }
657
658 #[test]
659 fn test_validate_price_precision_invalid() {
660 assert!(validate_price_precision(123456.0, 5).is_err());
661 assert!(validate_price_precision(12345.6, 5).is_err());
662 assert!(validate_price_precision(1234.56, 5).is_err());
663 assert!(validate_price_precision(123.456, 5).is_err());
664 assert!(validate_price_precision(12.3456, 5).is_err());
665 assert!(validate_price_precision(1.23456, 5).is_err());
666 }
667
668 #[test]
669 fn test_round_to_sig_figs_basic() {
670 assert!((round_to_sig_figs(12345.6, 5) - 12346.0).abs() < 0.1);
671 assert!((round_to_sig_figs(12345.4, 5) - 12345.0).abs() < 0.1);
672 assert!((round_to_sig_figs(123456.0, 5) - 123460.0).abs() < 1.0);
673 assert!((round_to_sig_figs(123456.0, 4) - 123500.0).abs() < 1.0);
674 assert!((round_to_sig_figs(123456.0, 3) - 123000.0).abs() < 1.0);
675 }
676
677 #[test]
678 fn test_expiry_date_to_timestamp_checked_known_value() {
679 assert_eq!(
680 expiry_date_to_timestamp_checked("BTC", 20250101).unwrap(),
681 1_735_718_400
682 );
683 }
684
685 #[test]
686 fn test_expiry_date_to_timestamp_checked_defaults_to_0800_utc() {
687 assert_eq!(
688 expiry_date_to_timestamp_checked("BTC", 20260605).unwrap(),
689 1_780_646_400
690 );
691 }
692
693 #[test]
696 fn test_2000_utc_override_matches_retired_spcx_flag_timestamps() {
697 let spcx_time = ExpiryTime::parse("20:00").unwrap();
698 assert_eq!(
699 expiry_date_to_timestamp_at_time_checked(20260605, spcx_time).unwrap(),
700 1_780_689_600
701 );
702 }
703
704 #[test]
705 fn test_expiry_date_to_timestamp_checked_rejects_invalid_date() {
706 let err = expiry_date_to_timestamp_checked("BTC", 20230229)
707 .unwrap_err()
708 .to_string();
709 assert!(err.contains("Invalid expiry date components"));
710 }
711
712 #[test]
713 fn test_expiry_date_to_timestamp_preserves_legacy_zero_fallback() {
714 assert_eq!(expiry_date_to_timestamp("BTC", 20230229), 0);
715 }
716
717 #[test]
718 fn test_strike_to_e8_exact_scaling() {
719 assert_eq!(strike_to_e8(dec!(100.5)).unwrap(), 10_050_000_000);
720 assert_eq!(strike_to_e8(dec!(0.05)).unwrap(), 5_000_000);
721 }
722
723 #[test]
724 fn test_strike_to_e8_rejects_precision_overflow_and_zero() {
725 assert!(strike_to_e8(dec!(100.000000001)).is_err());
726 assert!(strike_to_e8(Decimal::ZERO).is_err());
727 }
728
729 #[test]
730 fn test_parse_symbol_valid() {
731 let symbol = "BTC-20240131-100000-C";
732 let parsed = ParsedSymbol::from_symbol(symbol).unwrap();
733 assert_eq!(parsed.underlying, "BTC");
734 assert_eq!(parsed.expiry, 20240131);
735 assert_eq!(parsed.strike, 100000.0);
736 assert_eq!(parsed.option_type, OptionType::Call);
737 }
738
739 #[test]
740 fn test_parse_symbol_deribit_format() {
741 let symbol = "BTC-30AUG25-95000-C";
742 let parsed = ParsedSymbol::from_symbol(symbol).unwrap();
743 assert_eq!(parsed.underlying, "BTC");
744 assert_eq!(parsed.expiry, 20250830);
745 assert_eq!(parsed.strike, 95000.0);
746 assert_eq!(parsed.option_type, OptionType::Call);
747 }
748
749 #[test]
750 fn test_parse_symbol_put() {
751 let symbol = "ETH-20240228-3000-P";
752 let parsed = ParsedSymbol::from_symbol(symbol).unwrap();
753 assert_eq!(parsed.underlying, "ETH");
754 assert_eq!(parsed.expiry, 20240228);
755 assert_eq!(parsed.strike, 3000.0);
756 assert_eq!(parsed.option_type, OptionType::Put);
757 }
758
759 #[test]
760 fn test_classify_instrument_symbol_option_perp() {
761 assert_eq!(
762 classify_instrument_symbol("BTC-20240131-100000-C").unwrap(),
763 InstrumentKind::Option
764 );
765 assert_eq!(
766 classify_instrument_symbol("BTC-PERP").unwrap(),
767 InstrumentKind::Perp
768 );
769 }
770
771 #[test]
772 fn test_classify_instrument_symbol_rejects_unknown_symbol() {
773 let err = classify_instrument_symbol("BTC-BROKEN").unwrap_err();
774 assert!(err.contains("cannot classify symbol"));
775 }
776
777 #[test]
782 fn test_contract_unit_round_trip_f64() {
783 let test_cases = [1.0, 0.5, 0.000001, 100.0, 1234.567890];
785 let symbol = "BTC-20240131-100000-C";
786
787 for human_readable in test_cases {
788 let contract_units = to_contract_units(symbol, human_readable);
789 let back = to_human_readable(symbol, contract_units);
790 assert!(
792 (back - human_readable).abs() < 0.0000001,
793 "Round-trip failed for {}: got {}",
794 human_readable,
795 back
796 );
797 }
798 }
799
800 #[test]
801 fn test_contract_unit_round_trip_decimal() {
802 let test_cases = [
805 dec!(1.0),
806 dec!(0.5),
807 dec!(0.000001),
808 dec!(100.0),
809 dec!(1234.567890),
810 dec!(60000.0), ];
812 let symbol = "BTC-20240131-100000-C";
813
814 for human_readable in test_cases {
815 let contract_units = to_contract_units_decimal(symbol, human_readable);
816 let back = to_human_readable_decimal(symbol, contract_units);
817 assert_eq!(
818 back, human_readable,
819 "Decimal round-trip failed for {}: got {}",
820 human_readable, back
821 );
822 }
823 }
824
825 #[test]
826 fn test_contract_unit_multiplier_values() {
827 assert_eq!(CONTRACT_UNIT_MULTIPLIER, 1_000_000.0);
829 assert_eq!(CONTRACT_UNIT_MULTIPLIER_DECIMAL, dec!(1000000));
830 }
831
832 #[test]
833 fn test_human_readable_to_contract_units() {
834 let symbol = "BTC-20240131-100000-C";
835
836 assert_eq!(to_contract_units(symbol, 1.0), 1_000_000);
838 assert_eq!(to_contract_units_decimal(symbol, dec!(1.0)), dec!(1000000));
839
840 assert_eq!(to_contract_units(symbol, 0.00001), 10);
842 assert_eq!(to_contract_units_decimal(symbol, dec!(0.00001)), dec!(10));
843 }
844
845 #[test]
846 fn test_contract_units_to_human_readable() {
847 let symbol = "BTC-20240131-100000-C";
848
849 assert_eq!(to_human_readable(symbol, 1_000_000), 1.0);
851 assert_eq!(to_human_readable_decimal(symbol, dec!(1000000)), dec!(1.0));
852
853 assert_eq!(to_human_readable(symbol, 10), 0.00001);
855 assert_eq!(to_human_readable_decimal(symbol, dec!(10)), dec!(0.00001));
856 }
857
858 #[test]
859 fn test_no_double_conversion_bug() {
860 let symbol = "BTC-20240131-100000-C";
864 let price = dec!(60000.0); let size_human_readable = dec!(1.0); let correct_premium = price * size_human_readable;
869 assert_eq!(correct_premium, dec!(60000.0));
870
871 let wrong_size = to_human_readable_decimal(symbol, size_human_readable);
873 let wrong_premium = price * wrong_size;
874 assert_eq!(wrong_premium, dec!(0.06)); assert_eq!(
878 correct_premium / wrong_premium,
879 CONTRACT_UNIT_MULTIPLIER_DECIMAL
880 );
881 }
882}