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