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}