rangebar_core/
fixed_point.rs1#[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, 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 {
59 let frac_str = parts[1];
60 if frac_str.len() > 8 {
61 return Err(FixedPointError::TooManyDecimals);
62 }
63
64 let padded = format!("{:0<8}", frac_str);
66 padded
67 .parse::<i64>()
68 .map_err(|_| FixedPointError::InvalidFormat)?
69 } else {
70 0
71 };
72
73 let result = if integer_part >= 0 {
75 integer_part * SCALE + fractional_part
76 } else {
77 integer_part * SCALE - fractional_part
78 };
79
80 Ok(FixedPoint(result))
81 }
82
83 #[allow(clippy::inherent_to_string_shadow_display)]
85 pub fn to_string(&self) -> String {
86 let abs_value = self.0.abs();
87 let integer_part = abs_value / SCALE;
88 let fractional_part = abs_value % SCALE;
89
90 let sign = if self.0 < 0 { "-" } else { "" };
91 format!("{}{}.{:08}", sign, integer_part, fractional_part)
92 }
93
94 pub fn compute_range_thresholds(&self, threshold_bps: u32) -> (FixedPoint, FixedPoint) {
112 let delta = (self.0 as i128 * threshold_bps as i128) / BASIS_POINTS_SCALE as i128;
115 let delta = delta as i64;
116
117 let upper = FixedPoint(self.0 + delta);
118 let lower = FixedPoint(self.0 - delta);
119
120 (upper, lower)
121 }
122
123 pub fn to_f64(&self) -> f64 {
125 self.0 as f64 / SCALE as f64
126 }
127}
128
129impl fmt::Display for FixedPoint {
130 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131 write!(f, "{}", self.to_string())
132 }
133}
134
135impl FromStr for FixedPoint {
136 type Err = FixedPointError;
137
138 fn from_str(s: &str) -> Result<Self, Self::Err> {
139 FixedPoint::from_str(s)
140 }
141}
142
143#[derive(Debug, Clone, PartialEq)]
145pub enum FixedPointError {
146 InvalidFormat,
148 TooManyDecimals,
150 Overflow,
152}
153
154impl fmt::Display for FixedPointError {
155 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156 match self {
157 FixedPointError::InvalidFormat => write!(f, "Invalid number format"),
158 FixedPointError::TooManyDecimals => write!(f, "Too many decimal places (max 8)"),
159 FixedPointError::Overflow => write!(f, "Arithmetic overflow"),
160 }
161 }
162}
163
164impl std::error::Error for FixedPointError {}
165
166#[cfg(feature = "python")]
167impl From<FixedPointError> for PyErr {
168 fn from(err: FixedPointError) -> PyErr {
169 match err {
170 FixedPointError::InvalidFormat => {
171 pyo3::exceptions::PyValueError::new_err("Invalid number format")
172 }
173 FixedPointError::TooManyDecimals => {
174 pyo3::exceptions::PyValueError::new_err("Too many decimal places (max 8)")
175 }
176 FixedPointError::Overflow => {
177 pyo3::exceptions::PyOverflowError::new_err("Arithmetic overflow")
178 }
179 }
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn test_from_string() {
189 assert_eq!(FixedPoint::from_str("0").unwrap().0, 0);
190 assert_eq!(FixedPoint::from_str("1").unwrap().0, SCALE);
191 assert_eq!(FixedPoint::from_str("1.5").unwrap().0, SCALE + SCALE / 2);
192 assert_eq!(
193 FixedPoint::from_str("50000.12345678").unwrap().0,
194 5000012345678
195 );
196 assert_eq!(FixedPoint::from_str("-1.5").unwrap().0, -SCALE - SCALE / 2);
197 }
198
199 #[test]
200 fn test_to_string() {
201 assert_eq!(FixedPoint(0).to_string(), "0.00000000");
202 assert_eq!(FixedPoint(SCALE).to_string(), "1.00000000");
203 assert_eq!(FixedPoint(SCALE + SCALE / 2).to_string(), "1.50000000");
204 assert_eq!(FixedPoint(5000012345678).to_string(), "50000.12345678");
205 assert_eq!(FixedPoint(-SCALE).to_string(), "-1.00000000");
206 }
207
208 #[test]
209 fn test_round_trip() {
210 let test_values = [
211 "0",
212 "1",
213 "1.5",
214 "50000.12345678",
215 "999999.99999999",
216 "-1.5",
217 "-50000.12345678",
218 ];
219
220 for val in &test_values {
221 let fp = FixedPoint::from_str(val).unwrap();
222 let back = fp.to_string();
223
224 let fp2 = FixedPoint::from_str(&back).unwrap();
226 assert_eq!(fp.0, fp2.0, "Round trip failed for {}", val);
227 }
228 }
229
230 #[test]
231 fn test_compute_thresholds() {
232 let price = FixedPoint::from_str("50000.0").unwrap();
233 let (upper, lower) = price.compute_range_thresholds(250); assert_eq!(upper.to_string(), "50125.00000000");
237 assert_eq!(lower.to_string(), "49875.00000000");
238 }
239
240 #[test]
241 fn test_error_cases() {
242 assert!(FixedPoint::from_str("").is_err());
243 assert!(FixedPoint::from_str("not_a_number").is_err());
244 assert!(FixedPoint::from_str("1.123456789").is_err()); assert!(FixedPoint::from_str("1.2.3").is_err()); }
247
248 #[test]
249 fn test_comparison() {
250 let a = FixedPoint::from_str("50000.0").unwrap();
251 let b = FixedPoint::from_str("50000.1").unwrap();
252 let c = FixedPoint::from_str("49999.9").unwrap();
253
254 assert!(a < b);
255 assert!(b > a);
256 assert!(c < a);
257 assert_eq!(a, a);
258 }
259}