1#[cfg(feature = "python")]
4use pyo3::prelude::*;
5use serde::{Deserialize, Serialize};
6use std::fmt;
7use std::str::FromStr;
8
9pub const SCALE: i64 = 100_000_000;
11
12pub const BASIS_POINTS_SCALE: u32 = 100_000;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
26#[cfg_attr(feature = "api", derive(utoipa::ToSchema))]
27pub struct FixedPoint(pub i64);
28
29impl FixedPoint {
30 #[allow(clippy::should_implement_trait)]
40 pub fn from_str(s: &str) -> Result<Self, FixedPointError> {
41 if s.is_empty() {
43 return Err(FixedPointError::InvalidFormat);
44 }
45
46 let parts: Vec<&str> = s.split('.').collect();
48 if parts.len() > 2 {
49 return Err(FixedPointError::InvalidFormat);
50 }
51
52 let integer_part: i64 = parts[0]
54 .parse()
55 .map_err(|_| FixedPointError::InvalidFormat)?;
56
57 let fractional_part = if parts.len() == 2 {
60 let frac_str = parts[1];
61 let frac_len = frac_str.len();
62 if frac_len > 8 {
63 return Err(FixedPointError::TooManyDecimals);
64 }
65
66 let frac_digits: i64 = frac_str
68 .parse()
69 .map_err(|_| FixedPointError::InvalidFormat)?;
70
71 const POWERS: [i64; 9] = [
75 100_000_000, 10_000_000, 1_000_000, 100_000, 10_000,
76 1_000, 100, 10, 1,
77 ];
78 frac_digits * POWERS[frac_len]
79 } else {
80 0
81 };
82
83 let result = if integer_part >= 0 {
85 integer_part * SCALE + fractional_part
86 } else {
87 integer_part * SCALE - fractional_part
88 };
89
90 Ok(FixedPoint(result))
91 }
92
93 #[allow(clippy::inherent_to_string_shadow_display)]
95 pub fn to_string(&self) -> String {
96 let abs_value = self.0.abs();
97 let integer_part = abs_value / SCALE;
98 let fractional_part = abs_value % SCALE;
99
100 let sign = if self.0 < 0 { "-" } else { "" };
101 format!("{}{}.{:08}", sign, integer_part, fractional_part)
102 }
103
104 pub fn compute_range_thresholds(&self, threshold_decimal_bps: u32) -> (FixedPoint, FixedPoint) {
122 let delta = (self.0 as i128 * threshold_decimal_bps as i128) / BASIS_POINTS_SCALE as i128;
125 let delta = delta as i64;
126
127 let upper = FixedPoint(self.0 + delta);
128 let lower = FixedPoint(self.0 - delta);
129
130 (upper, lower)
131 }
132
133 #[inline]
143 pub fn compute_range_thresholds_cached(&self, threshold_ratio: i64) -> (FixedPoint, FixedPoint) {
144 let delta = (self.0 as i128 * threshold_ratio as i128) / SCALE as i128;
147 let delta = delta as i64;
148
149 let upper = FixedPoint(self.0 + delta);
150 let lower = FixedPoint(self.0 - delta);
151
152 (upper, lower)
153 }
154
155 #[inline]
158 pub fn to_f64(&self) -> f64 {
159 self.0 as f64 / SCALE as f64
160 }
161}
162
163impl fmt::Display for FixedPoint {
164 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165 write!(f, "{}", self.to_string())
166 }
167}
168
169impl FromStr for FixedPoint {
170 type Err = FixedPointError;
171
172 fn from_str(s: &str) -> Result<Self, Self::Err> {
173 FixedPoint::from_str(s)
174 }
175}
176
177#[derive(Debug, Clone, PartialEq)]
179pub enum FixedPointError {
180 InvalidFormat,
182 TooManyDecimals,
184 Overflow,
186}
187
188impl fmt::Display for FixedPointError {
189 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190 match self {
191 FixedPointError::InvalidFormat => write!(f, "Invalid number format"),
192 FixedPointError::TooManyDecimals => write!(f, "Too many decimal places (max 8)"),
193 FixedPointError::Overflow => write!(f, "Arithmetic overflow"),
194 }
195 }
196}
197
198impl std::error::Error for FixedPointError {}
199
200#[cfg(feature = "python")]
201impl From<FixedPointError> for PyErr {
202 fn from(err: FixedPointError) -> PyErr {
203 match err {
204 FixedPointError::InvalidFormat => {
205 pyo3::exceptions::PyValueError::new_err("Invalid number format")
206 }
207 FixedPointError::TooManyDecimals => {
208 pyo3::exceptions::PyValueError::new_err("Too many decimal places (max 8)")
209 }
210 FixedPointError::Overflow => {
211 pyo3::exceptions::PyOverflowError::new_err("Arithmetic overflow")
212 }
213 }
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 #[test]
222 fn test_from_string() {
223 assert_eq!(FixedPoint::from_str("0").unwrap().0, 0);
224 assert_eq!(FixedPoint::from_str("1").unwrap().0, SCALE);
225 assert_eq!(FixedPoint::from_str("1.5").unwrap().0, SCALE + SCALE / 2);
226 assert_eq!(
227 FixedPoint::from_str("50000.12345678").unwrap().0,
228 5000012345678
229 );
230 assert_eq!(FixedPoint::from_str("-1.5").unwrap().0, -SCALE - SCALE / 2);
231 }
232
233 #[test]
234 fn test_to_string() {
235 assert_eq!(FixedPoint(0).to_string(), "0.00000000");
236 assert_eq!(FixedPoint(SCALE).to_string(), "1.00000000");
237 assert_eq!(FixedPoint(SCALE + SCALE / 2).to_string(), "1.50000000");
238 assert_eq!(FixedPoint(5000012345678).to_string(), "50000.12345678");
239 assert_eq!(FixedPoint(-SCALE).to_string(), "-1.00000000");
240 }
241
242 #[test]
243 fn test_round_trip() {
244 let test_values = [
245 "0",
246 "1",
247 "1.5",
248 "50000.12345678",
249 "999999.99999999",
250 "-1.5",
251 "-50000.12345678",
252 ];
253
254 for val in &test_values {
255 let fp = FixedPoint::from_str(val).unwrap();
256 let back = fp.to_string();
257
258 let fp2 = FixedPoint::from_str(&back).unwrap();
260 assert_eq!(fp.0, fp2.0, "Round trip failed for {}", val);
261 }
262 }
263
264 #[test]
265 fn test_compute_thresholds() {
266 let price = FixedPoint::from_str("50000.0").unwrap();
267 let (upper, lower) = price.compute_range_thresholds(250); assert_eq!(upper.to_string(), "50125.00000000");
271 assert_eq!(lower.to_string(), "49875.00000000");
272 }
273
274 #[test]
275 fn test_error_cases() {
276 assert!(FixedPoint::from_str("").is_err());
277 assert!(FixedPoint::from_str("not_a_number").is_err());
278 assert!(FixedPoint::from_str("1.123456789").is_err()); assert!(FixedPoint::from_str("1.2.3").is_err()); }
281
282 #[test]
283 fn test_comparison() {
284 let a = FixedPoint::from_str("50000.0").unwrap();
285 let b = FixedPoint::from_str("50000.1").unwrap();
286 let c = FixedPoint::from_str("49999.9").unwrap();
287
288 assert!(a < b);
289 assert!(b > a);
290 assert!(c < a);
291 assert_eq!(a, a);
292 }
293
294 #[test]
297 fn test_from_str_too_many_decimals() {
298 let err = FixedPoint::from_str("0.000000001").unwrap_err();
299 assert_eq!(err, FixedPointError::TooManyDecimals);
300 }
301
302 #[test]
303 fn test_from_str_negative_fractional() {
304 let fp = FixedPoint::from_str("-0.5").unwrap();
308 assert_eq!(fp.0, 50_000_000); let fp2 = FixedPoint::from_str("-1.5").unwrap();
312 assert_eq!(fp2.0, -150_000_000); assert_eq!(fp2.to_f64(), -1.5);
314 }
315
316 #[test]
317 fn test_from_str_leading_zeros() {
318 let fp = FixedPoint::from_str("000.123").unwrap();
320 assert_eq!(fp.0, 12_300_000); }
322
323 #[test]
324 fn test_to_f64_extreme_values() {
325 let max_fp = FixedPoint(i64::MAX);
327 let max_f64 = max_fp.to_f64();
328 assert!(max_f64 > 92_233_720_368.0);
329 assert!(max_f64.is_finite());
330
331 let min_fp = FixedPoint(i64::MIN);
333 let min_f64 = min_fp.to_f64();
334 assert!(min_f64 < -92_233_720_368.0);
335 assert!(min_f64.is_finite());
336 }
337
338 #[test]
339 fn test_threshold_zero_ratio() {
340 let price = FixedPoint::from_str("100.0").unwrap();
341 let (upper, lower) = price.compute_range_thresholds_cached(0);
342 assert_eq!(upper, price);
343 assert_eq!(lower, price);
344 }
345
346 #[test]
347 fn test_threshold_small_price_small_bps() {
348 let price = FixedPoint::from_str("0.01").unwrap();
350 let (upper, lower) = price.compute_range_thresholds(1);
351 assert!(upper > price);
354 assert!(lower < price);
355 }
356
357 #[test]
358 fn test_fixedpoint_zero() {
359 let zero = FixedPoint(0);
360 assert_eq!(zero.to_f64(), 0.0);
361 assert_eq!(zero.to_string(), "0.00000000");
362 let (upper, lower) = zero.compute_range_thresholds(250);
363 assert_eq!(upper, zero); assert_eq!(lower, zero);
365 }
366
367 #[test]
368 fn test_fixedpoint_error_display() {
369 assert_eq!(
370 FixedPointError::InvalidFormat.to_string(),
371 "Invalid number format"
372 );
373 assert_eq!(
374 FixedPointError::TooManyDecimals.to_string(),
375 "Too many decimal places (max 8)"
376 );
377 assert_eq!(
378 FixedPointError::Overflow.to_string(),
379 "Arithmetic overflow"
380 );
381 }
382}