1use std::fmt;
4
5use super::constants::QUOTE_LOT_DECIMALS;
6
7pub const MAX_UI_ORDER_SIZE_UNITS: f64 = 1_000_000_000.0;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum LotConversionError {
14 NotFinite,
15 NonPositive,
16 AboveUiLimit,
17 BelowMinimumLot,
18 TooLarge,
19}
20
21impl fmt::Display for LotConversionError {
22 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23 match self {
24 Self::NotFinite => f.write_str("size must be a finite number"),
25 Self::NonPositive => f.write_str("size must be greater than zero"),
26 Self::AboveUiLimit => f.write_str("size is above the release safety limit"),
27 Self::BelowMinimumLot => f.write_str("size is below one base lot for this market"),
28 Self::TooLarge => f.write_str("size is too large to encode as base lots"),
29 }
30 }
31}
32
33#[inline]
35pub fn ticks_to_price(ticks: u64, tick_size: u64, base_lot_decimals: i8) -> f64 {
36 ticks as f64 * tick_size as f64 * 10_f64.powi(base_lot_decimals as i32)
37 / 10_f64.powi(QUOTE_LOT_DECIMALS)
38}
39
40#[inline]
42pub fn base_lots_to_units(lots: u64, base_lot_decimals: i8) -> f64 {
43 lots as f64 / 10_f64.powi(base_lot_decimals as i32)
44}
45
46#[inline]
49pub fn pct_change_24h(mark: f64, prev_day: f64) -> f64 {
50 if prev_day != 0.0 {
51 ((mark - prev_day) / prev_day) * 100.0
52 } else {
53 0.0
54 }
55}
56
57#[inline]
62pub fn ui_size_to_num_base_lots(
63 size: f64,
64 base_lot_decimals: i8,
65) -> Result<u64, LotConversionError> {
66 if !size.is_finite() {
67 return Err(LotConversionError::NotFinite);
68 }
69 if size <= 0.0 {
70 return Err(LotConversionError::NonPositive);
71 }
72 if size > MAX_UI_ORDER_SIZE_UNITS {
73 return Err(LotConversionError::AboveUiLimit);
74 }
75
76 let lots = size * 10_f64.powi(base_lot_decimals as i32);
77 if !lots.is_finite() || lots > u64::MAX as f64 {
78 return Err(LotConversionError::TooLarge);
79 }
80 if lots < 1.0 {
81 return Err(LotConversionError::BelowMinimumLot);
82 }
83
84 Ok(lots.floor() as u64)
85}
86
87#[inline]
92pub fn phoenix_decimal_to_num_base_lots(
93 value: i64,
94 value_decimals: i8,
95 base_lot_decimals: i8,
96) -> Option<u64> {
97 let abs_val = value.unsigned_abs();
98 let exp = i32::from(base_lot_decimals) - i32::from(value_decimals);
99
100 match exp.cmp(&0) {
101 std::cmp::Ordering::Greater => {
102 let mult = 10_u64.checked_pow(exp as u32)?;
103 abs_val.checked_mul(mult)
104 }
105 std::cmp::Ordering::Less => {
106 let div = 10_u64.checked_pow((-exp) as u32)?;
107 Some(abs_val / div)
108 }
109 std::cmp::Ordering::Equal => Some(abs_val),
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 #[test]
118 fn num_base_lots_from_decimal_sol_two_decimals() {
119 assert_eq!(phoenix_decimal_to_num_base_lots(3470, 3, 2), Some(347));
121 }
122
123 #[test]
124 fn num_base_lots_scale_down_truncates() {
125 assert_eq!(phoenix_decimal_to_num_base_lots(1001, 3, 2), Some(100));
126 }
127
128 #[test]
129 fn num_base_lots_scale_up_multiplies() {
130 assert_eq!(phoenix_decimal_to_num_base_lots(5, 0, 3), Some(5_000));
132 }
133
134 #[test]
135 fn num_base_lots_negative_value_uses_absolute() {
136 assert_eq!(phoenix_decimal_to_num_base_lots(-3470, 3, 2), Some(347));
137 }
138
139 #[test]
140 fn num_base_lots_overflow_returns_none() {
141 assert_eq!(phoenix_decimal_to_num_base_lots(1, 0, 20), None);
143 }
144
145 #[test]
146 fn ui_size_to_num_base_lots_matches_existing_scale() {
147 assert_eq!(ui_size_to_num_base_lots(3.47, 2), Ok(347));
148 assert_eq!(ui_size_to_num_base_lots(50.0, -1), Ok(5));
149 }
150
151 #[test]
152 fn ui_size_to_num_base_lots_rejects_bad_inputs() {
153 assert_eq!(
154 ui_size_to_num_base_lots(f64::NAN, 2),
155 Err(LotConversionError::NotFinite)
156 );
157 assert_eq!(
158 ui_size_to_num_base_lots(0.0, 2),
159 Err(LotConversionError::NonPositive)
160 );
161 assert_eq!(
162 ui_size_to_num_base_lots(0.001, 2),
163 Err(LotConversionError::BelowMinimumLot)
164 );
165 assert_eq!(
166 ui_size_to_num_base_lots(MAX_UI_ORDER_SIZE_UNITS + 1.0, 2),
167 Err(LotConversionError::AboveUiLimit)
168 );
169 }
170
171 #[test]
172 fn pct_change_zero_prev() {
173 assert_eq!(pct_change_24h(100.0, 0.0), 0.0);
174 }
175
176 #[test]
177 fn pct_change_matches_formula() {
178 assert!((pct_change_24h(110.0, 100.0) - 10.0).abs() < 1e-9);
179 }
180
181 #[test]
182 fn pct_change_handles_negative_direction() {
183 assert!((pct_change_24h(90.0, 100.0) - -10.0).abs() < 1e-9);
184 }
185
186 #[test]
187 fn base_lots_to_units_divides_by_decimal_power() {
188 assert!((base_lots_to_units(1_000, 3) - 1.0).abs() < 1e-9);
190 }
191
192 #[test]
193 fn base_lots_to_units_handles_negative_decimals() {
194 assert!((base_lots_to_units(5, -1) - 50.0).abs() < 1e-9);
196 }
197
198 #[test]
199 fn ticks_to_price_matches_known_market() {
200 assert!((ticks_to_price(150_000_000, 1, 0) - 150.0).abs() < 1e-9);
202 }
203
204 #[test]
205 fn ticks_to_price_scales_with_base_lot_decimals() {
206 assert!((ticks_to_price(100, 10, 2) - 0.1).abs() < 1e-9);
208 }
209}