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}