Skip to main content

optionstratlib/model/
decimal.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 25/12/24
5******************************************************************************/
6use crate::error::decimal::DecimalError;
7use crate::geometrics::HasX;
8use num_traits::{FromPrimitive, ToPrimitive};
9use rand::distr::Distribution;
10use rand_distr::Normal;
11use rust_decimal::{Decimal, MathematicalOps};
12use rust_decimal_macros::dec;
13
14/// Represents the daily interest rate factor used for financial calculations,
15/// approximately equivalent to 1/252 (a standard value for the number of trading days in a year).
16///
17/// This constant converts annual interest rates to daily rates by providing a division factor.
18/// The value 0.00396825397 corresponds to 1/252, where 252 is the typical number of trading
19/// days in a financial year.
20///
21/// # Usage
22///
23/// This constant is commonly used in financial calculations such as:
24/// - Converting annual interest rates to daily rates
25/// - Time value calculations for options pricing
26/// - Discounting cash flows on a daily basis
27/// - Interest accrual calculations
28pub const ONE_DAY: Decimal = dec!(0.00396825397);
29
30/// Asserts that two Decimal values are approximately equal within a given epsilon
31#[macro_export]
32macro_rules! assert_decimal_eq {
33    ($left:expr, $right:expr, $epsilon:expr) => {
34        let diff = ($left - $right).abs();
35        assert!(
36            diff <= $epsilon,
37            "assertion failed: `(left == right)`\n  left: `{}`\n right: `{}`\n  diff: `{}`\n epsilon: `{}`",
38            $left,
39            $right,
40            diff,
41            $epsilon
42        );
43    };
44}
45
46/// Defines statistical operations for collections of decimal values.
47///
48/// This trait provides methods to calculate common statistical measures
49/// for sequences or collections of `Decimal` values. It allows implementing
50/// types to offer standardized statistical analysis capabilities.
51///
52/// ## Key Features
53///
54/// * Basic statistical calculations for `Decimal` collections
55/// * Consistent interface for various collection types
56/// * Precision-preserving operations using the `Decimal` type
57///
58/// ## Available Statistics
59///
60/// * `mean`: Calculates the arithmetic mean (average) of the values
61/// * `std_dev`: Calculates the standard deviation, measuring the dispersion from the mean
62///
63/// ## Example
64///
65/// ```rust
66/// use rust_decimal::Decimal;
67/// use rust_decimal_macros::dec;
68/// use optionstratlib::model::decimal::DecimalStats;
69///
70/// struct DecimalSeries(Vec<Decimal>);
71///
72/// impl DecimalStats for DecimalSeries {
73///     fn mean(&self) -> Decimal {
74///         let sum: Decimal = self.0.iter().sum();
75///         if self.0.is_empty() {
76///             dec!(0)
77///         } else {
78///             sum / Decimal::from(self.0.len())
79///         }
80///     }
81///     
82///     fn std_dev(&self) -> Decimal {
83///         // Implementation of standard deviation calculation
84///         // ...
85///         dec!(0) // Placeholder return
86///     }
87/// }
88/// ```
89pub trait DecimalStats {
90    /// Calculates the arithmetic mean (average) of the collection.
91    ///
92    /// The mean is the sum of all values divided by the count of values.
93    /// This method should handle empty collections appropriately.
94    fn mean(&self) -> Decimal;
95
96    /// Calculates the standard deviation of the collection.
97    ///
98    /// The standard deviation measures the amount of variation or dispersion
99    /// from the mean. A low standard deviation indicates that values tend to be
100    /// close to the mean, while a high standard deviation indicates values are
101    /// spread out over a wider range.
102    fn std_dev(&self) -> Decimal;
103}
104
105impl DecimalStats for Vec<Decimal> {
106    fn mean(&self) -> Decimal {
107        if self.is_empty() {
108            return Decimal::ZERO;
109        }
110        let sum: Decimal = self.iter().sum();
111        sum / Decimal::from(self.len())
112    }
113
114    fn std_dev(&self) -> Decimal {
115        // Population variance of a single value is zero
116        if self.len() < 2usize {
117            return Decimal::ZERO;
118        }
119        let mean = self.mean();
120        let variance: Decimal = self.iter().map(|x| (x - mean).powd(Decimal::TWO)).sum();
121        (variance / Decimal::from(self.len() - 1))
122            .sqrt()
123            .unwrap_or(Decimal::ZERO)
124    }
125}
126
127/// Converts a Decimal value to an f64.
128///
129/// This function attempts to convert a Decimal value to an f64 floating-point number.
130/// If the conversion fails, it returns a DecimalError with detailed information about
131/// the failure.
132///
133/// # Parameters
134///
135/// * `value` - The Decimal value to convert
136///
137/// # Returns
138///
139/// * `Result<f64, DecimalError>` - The converted f64 value if successful, or a DecimalError
140///   if the conversion fails
141///
142/// # Example
143///
144/// ```rust
145/// use rust_decimal::Decimal;
146/// use rust_decimal_macros::dec;
147/// use tracing::info;
148/// use optionstratlib::model::decimal::decimal_to_f64;
149///
150/// let decimal = dec!(3.14159);
151/// match decimal_to_f64(decimal) {
152///     Ok(float) => info!("Converted to f64: {}", float),
153///     Err(e) => info!("Conversion error: {:?}", e)
154/// }
155/// ```
156pub fn decimal_to_f64(value: Decimal) -> Result<f64, DecimalError> {
157    value.to_f64().ok_or(DecimalError::ConversionError {
158        from_type: format!("Decimal: {value}"),
159        to_type: "f64".to_string(),
160        reason: "Failed to convert Decimal to f64".to_string(),
161    })
162}
163
164/// Converts an f64 floating-point number to a Decimal.
165///
166/// This function attempts to convert an f64 floating-point number to a Decimal value.
167/// If the conversion fails (for example, if the f64 represents NaN, infinity, or is otherwise
168/// not representable as a Decimal), it returns a DecimalError with detailed information about
169/// the failure.
170///
171/// # Parameters
172///
173/// * `value` - The f64 value to convert
174///
175/// # Returns
176///
177/// * `Result<Decimal, DecimalError>` - The converted Decimal value if successful, or a DecimalError
178///   if the conversion fails
179///
180/// # Example
181///
182/// ```rust
183/// use rust_decimal::Decimal;
184/// use tracing::info;
185/// use optionstratlib::model::decimal::f64_to_decimal;
186///
187/// let float = std::f64::consts::PI;
188/// match f64_to_decimal(float) {
189///     Ok(decimal) => info!("Converted to Decimal: {}", decimal),
190///     Err(e) => info!("Conversion error: {:?}", e)
191/// }
192/// ```
193pub fn f64_to_decimal(value: f64) -> Result<Decimal, DecimalError> {
194    Decimal::from_f64(value).ok_or(DecimalError::ConversionError {
195        from_type: format!("f64: {value}"),
196        to_type: "Decimal".to_string(),
197        reason: "Failed to convert f64 to Decimal".to_string(),
198    })
199}
200
201/// Generates a random positive value from a standard normal distribution.
202///
203/// This function samples from a normal distribution with mean 0.0 and standard
204/// deviation 1.0, and returns the value as a `Positive` type. Since the normal
205/// distribution can produce negative values, the function uses the `pos!` macro
206/// to convert the sample to a `Positive` value, which will handle the conversion
207/// according to the `Positive` type's implementation.
208///
209/// # Returns
210///
211/// A `Positive` value sampled from a standard normal distribution.
212///
213/// # Examples
214///
215/// ```rust
216/// use optionstratlib::model::decimal::decimal_normal_sample;
217/// use positive::Positive;
218/// let normal = decimal_normal_sample();
219/// ```
220pub fn decimal_normal_sample() -> Decimal {
221    let mut t_rng = rand::rng();
222    // SAFETY: Normal::new(0.0, 1.0) is always valid (mean=0, std=1 are valid parameters)
223    let normal =
224        Normal::new(0.0, 1.0).expect("standard normal distribution parameters are always valid");
225    Decimal::from_f64(normal.sample(&mut t_rng)).unwrap_or(Decimal::ZERO)
226}
227
228impl HasX for Decimal {
229    fn get_x(&self) -> Decimal {
230        *self
231    }
232}
233
234/// Converts a Decimal value to f64 without error checking.
235///
236/// This macro converts a Decimal type to an f64 floating-point value.
237/// It's an "unchecked" version that doesn't handle potential conversion errors.
238///
239/// # Parameters
240/// * `$val` - A Decimal value to be converted to f64
241///
242/// # Example
243/// ```rust
244/// use rust_decimal_macros::dec;
245/// use optionstratlib::d2fu;
246/// let decimal_value = dec!(10.5);
247/// let float_value = d2fu!(decimal_value);
248/// ```
249#[macro_export]
250macro_rules! d2fu {
251    ($val:expr) => {
252        $crate::model::decimal::decimal_to_f64($val)
253    };
254}
255
256/// Converts a Decimal value to f64 with error propagation.
257///
258/// This macro converts a Decimal type to an f64 floating-point value.
259/// It propagates any errors that might occur during conversion using the `?` operator.
260///
261/// # Parameters
262/// * `$val` - A Decimal value to be converted to f64
263///
264#[macro_export]
265macro_rules! d2f {
266    ($val:expr) => {
267        $crate::model::decimal::decimal_to_f64($val)?
268    };
269}
270
271/// Converts an f64 value to Decimal without error checking.
272///
273/// This macro converts an f64 floating-point value to a Decimal type.
274/// It's an "unchecked" version that doesn't handle potential conversion errors.
275///
276/// # Parameters
277/// * `$val` - An f64 value to be converted to Decimal
278///
279/// # Example
280/// ```rust
281/// use optionstratlib::f2du;
282/// let float_value = 10.5;
283/// let decimal_value = f2du!(float_value);
284/// ```
285#[macro_export]
286macro_rules! f2du {
287    ($val:expr) => {
288        $crate::model::decimal::f64_to_decimal($val)
289    };
290}
291
292/// Converts an f64 value to Decimal with error propagation.
293///
294/// This macro converts an f64 floating-point value to a Decimal type.
295/// It propagates any errors that might occur during conversion using the `?` operator.
296///
297/// # Parameters
298/// * `$val` - An f64 value to be converted to Decimal
299///
300#[macro_export]
301macro_rules! f2d {
302    ($val:expr) => {
303        $crate::model::decimal::f64_to_decimal($val)?
304    };
305}
306
307#[cfg(test)]
308pub mod tests {
309    use super::*;
310    use std::str::FromStr;
311
312    #[test]
313    fn test_f64_to_decimal_valid() {
314        let value = 42.42;
315        let result = f64_to_decimal(value);
316        assert!(result.is_ok());
317        assert_eq!(result.unwrap(), Decimal::from_str("42.42").unwrap());
318    }
319
320    #[test]
321    fn test_f64_to_decimal_zero() {
322        let value = 0.0;
323        let result = f64_to_decimal(value);
324        assert!(result.is_ok());
325        assert_eq!(result.unwrap(), Decimal::from_str("0").unwrap());
326    }
327
328    #[test]
329    fn test_decimal_to_f64_valid() {
330        let decimal = Decimal::from_str("42.42").unwrap();
331        let result = decimal_to_f64(decimal);
332        assert!(result.is_ok());
333        assert_eq!(result.unwrap(), 42.42);
334    }
335
336    #[test]
337    fn test_decimal_to_f64_zero() {
338        let decimal = Decimal::from_str("0").unwrap();
339        let result = decimal_to_f64(decimal);
340        assert!(result.is_ok());
341        assert_eq!(result.unwrap(), 0.0);
342    }
343}
344
345#[cfg(test)]
346mod tests_random_generation {
347    use super::*;
348    use approx::assert_relative_eq;
349    use rand::distr::Distribution;
350    use std::collections::HashMap;
351
352    #[test]
353    fn test_normal_sample_returns() {
354        // Run the function multiple times to ensure it always returns a positive value
355        for _ in 0..1000 {
356            let sample = decimal_normal_sample();
357            assert!(sample <= Decimal::TEN);
358            assert!(sample >= -Decimal::TEN);
359        }
360    }
361
362    #[test]
363    fn test_normal_sample_distribution() {
364        // Generate a large number of samples to check distribution characteristics
365        const NUM_SAMPLES: usize = 10000;
366        let mut samples = Vec::with_capacity(NUM_SAMPLES);
367
368        for _ in 0..NUM_SAMPLES {
369            samples.push(decimal_normal_sample().to_f64().unwrap());
370        }
371
372        // Calculate mean and standard deviation
373        let sum: f64 = samples.iter().sum();
374        let mean = sum / NUM_SAMPLES as f64;
375
376        let variance_sum: f64 = samples.iter().map(|&x| (x - mean).powi(2)).sum();
377        let std_dev = (variance_sum / NUM_SAMPLES as f64).sqrt();
378
379        // Check if the distribution approximately matches a standard normal
380        // Note: These tests use wide tolerances since we're working with random samples
381        assert_relative_eq!(mean, 0.0, epsilon = 0.04);
382        assert_relative_eq!(std_dev, 1.0, epsilon = 0.03);
383    }
384
385    #[test]
386    fn test_normal_distribution_transformation() {
387        let mut t_rng = rand::rng();
388        let normal = Normal::new(-1.0, 0.5).unwrap(); // Deliberately using a distribution with negative mean
389
390        // Count occurrences of values after transformation
391        let mut value_counts: HashMap<i32, usize> = HashMap::new();
392        const SAMPLES: usize = 5000;
393
394        for _ in 0..SAMPLES {
395            let raw_sample = normal.sample(&mut t_rng);
396            let positive_sample = raw_sample.to_f64().unwrap();
397
398            // Bucket values to the nearest integer for counting
399            let bucket = (positive_sample.round() as i32).max(0);
400            *value_counts.entry(bucket).or_insert(0) += 1;
401        }
402
403        // Verify that zero values appear frequently (due to negative values being transformed)
404        assert!(value_counts.get(&0).unwrap_or(&0) > &(SAMPLES / 10));
405
406        // Verify that we have a range of positive values
407        let max_bucket = value_counts.keys().max().unwrap_or(&0);
408        assert!(*max_bucket > 0);
409    }
410
411    #[test]
412    fn test_normal_sample_consistency() {
413        // This test ensures that multiple calls in sequence produce different values
414        let sample1 = decimal_normal_sample();
415        let sample2 = decimal_normal_sample();
416        let sample3 = decimal_normal_sample();
417
418        // It's statistically extremely unlikely to get the same value three times in a row
419        // This verifies that the RNG is properly producing different values
420        assert!(sample1 != sample2 || sample2 != sample3);
421    }
422}