optionstratlib_core/model/utils.rs
1/******************************************************************************
2 Author: Joaquín Béjar García
3 Email: jb@taunais.com
4 Date: 21/8/24
5******************************************************************************/
6use crate::error::DecimalError;
7use crate::model::Position;
8use crate::model::decimal::{d_add, d_div, d_mul, d_sub, decimal_to_f64};
9use crate::model::types::{OptionStyle, OptionType, Side};
10use crate::model::{ExpirationDate, Options};
11use chrono::{NaiveDateTime, TimeZone, Utc};
12use positive::{Positive, PositiveError};
13use rust_decimal::Decimal;
14use rust_decimal_macros::dec;
15
16/// Build a `Positive` from a compile-time non-negative `Decimal` literal.
17///
18/// All call sites in this module pass `dec!(X.X)` with a statically
19/// non-negative value, so the checked constructor is total; the
20/// `Positive::ZERO` branch is unreachable and only exists to keep the
21/// call site free of `.unwrap()`/`.expect()`.
22#[inline]
23fn pos_lit(value: Decimal) -> Positive {
24 Positive::new_decimal(value).unwrap_or(Positive::ZERO)
25}
26
27/// Subtracts `rhs` from `lhs`, flooring the result at zero.
28///
29/// `Positive::sub_or_zero` is deprecated in positive 0.6 because saturating
30/// arithmetic hides underflow. Every OptionStratLib call site genuinely wants
31/// the floor (a quoted price or a strike offset never goes negative), so the
32/// decision is made once, here, on top of the checked `sub_or_none`: `None`
33/// (the difference would be negative) becomes zero, and no saturating
34/// arithmetic is used on the `Decimal` itself. Use it only where a negative
35/// difference has no meaning; anywhere else it hides an underflow, and the
36/// checked `Positive::sub_or_none` is the right call.
37#[doc(hidden)]
38#[must_use]
39#[inline]
40pub fn sub_floor_zero(lhs: Positive, rhs: &Decimal) -> Positive {
41 lhs.sub_or_none(rhs).unwrap_or(Positive::ZERO)
42}
43
44/// Converts a vector of `Positive` values to a vector of `f64` values.
45///
46/// This utility function transforms a collection of `Positive` type values
47/// to standard floating-point values by applying the `to_f64()` method to each element.
48/// The function consumes the input vector and returns a new vector containing the converted values.
49///
50/// # Parameters
51///
52/// * `vec` - A vector of `Positive` values to be converted.
53///
54/// # Returns
55///
56/// A vector of `f64` values corresponding to the input `Positive` values,
57/// each the `f64` nearest to it ([`decimal_to_f64`], #828).
58///
59/// # Errors
60///
61/// Returns the [`DecimalError`] of [`decimal_to_f64`] for a value it cannot
62/// convert.
63pub fn positive_f64_to_f64(vec: Vec<Positive>) -> Result<Vec<f64>, DecimalError> {
64 vec.into_iter()
65 .map(|value| decimal_to_f64(value.to_dec()))
66 .collect()
67}
68
69/// Creates a sample option contract with predefined parameters for testing or demonstration purposes.
70///
71/// This utility function simplifies the creation of option contracts by providing a standard
72/// configuration with reasonable defaults. It creates a European-style option with a 30-day
73/// expiration and a fixed risk-free rate of 5%.
74///
75/// # Parameters
76///
77/// * `option_style` - Specifies whether the option is a Call or Put.
78/// * `side` - Determines if the position is Long or Short.
79/// * `underlying_price` - The current market price of the underlying asset.
80/// * `quantity` - The number of contracts in the position.
81/// * `strike_price` - The price at which the option holder can exercise the option.
82/// * `volatility` - The implied volatility used for pricing the option.
83///
84/// # Returns
85///
86/// An `Options` struct configured with the specified parameters and sensible defaults.
87///
88/// # Examples
89///
90/// ```rust
91/// use optionstratlib_core::model::{OptionStyle, Side};
92/// use positive::pos_or_panic;
93/// use optionstratlib_core::model::utils::create_sample_option;
94/// let option = create_sample_option(
95/// OptionStyle::Call,
96/// Side::Long,
97/// pos_or_panic!(150.0), // underlying price
98/// pos_or_panic!(10.0), // quantity
99/// pos_or_panic!(155.0), // strike price
100/// pos_or_panic!(0.25), // volatility (25%)
101/// );
102/// ```
103#[allow(dead_code)]
104#[must_use]
105pub fn create_sample_option(
106 option_style: OptionStyle,
107 side: Side,
108 underlying_price: Positive,
109 quantity: Positive,
110 strike_price: Positive,
111 volatility: Positive,
112) -> Options {
113 Options::new(
114 OptionType::European,
115 side,
116 "AAPL".to_string(),
117 strike_price,
118 ExpirationDate::Days(pos_lit(dec!(30.0))),
119 volatility,
120 quantity,
121 underlying_price,
122 dec!(0.05),
123 option_style,
124 pos_lit(dec!(0.01)),
125 None,
126 )
127}
128
129/// Creates a sample position for testing and demonstration purposes.
130///
131/// This function generates a `Position` instance with predefined values for some fields
132/// while allowing customization of key option parameters. It's useful for creating test
133/// scenarios, examples, or sample data for option position analysis.
134///
135/// # Parameters
136///
137/// * `option_style` - The style of the option (Call or Put)
138/// * `side` - Whether the position is Long or Short
139/// * `underlying_price` - The current price of the underlying asset
140/// * `quantity` - The number of option contracts in the position
141/// * `strike_price` - The price at which the option can be exercised
142/// * `implied_volatility` - The market's forecast of likely movement in the underlying asset
143///
144/// # Returns
145///
146/// A `Position` instance with the specified parameters and these default values:
147/// * European-style option
148/// * "AAPL" as the underlying symbol
149/// * 30-day expiration
150/// * 5% risk-free rate
151/// * 1% dividend yield
152/// * Premium of $5.00
153/// * Open and close fees of $0.50 each
154/// * Current date and time
155///
156/// # Example
157///
158/// ```rust
159/// use optionstratlib_core::model::utils::create_sample_position;
160/// use optionstratlib_core::model::{OptionStyle, Side};
161/// use positive::{pos_or_panic, Positive};
162/// let sample_call = create_sample_position(
163/// OptionStyle::Call,
164/// Side::Long,
165/// pos_or_panic!(150.0), // underlying price
166/// Positive::ONE, // quantity
167/// pos_or_panic!(155.0), // strike price
168/// pos_or_panic!(0.25) // implied volatility
169/// );
170/// ```
171#[allow(dead_code)]
172#[must_use]
173pub fn create_sample_position(
174 option_style: OptionStyle,
175 side: Side,
176 underlying_price: Positive,
177 quantity: Positive,
178 strike_price: Positive,
179 implied_volatility: Positive,
180) -> Position {
181 Position {
182 option: Options {
183 option_type: OptionType::European,
184 side,
185 underlying_symbol: "AAPL".to_string(),
186 strike_price,
187 expiration_date: ExpirationDate::Days(pos_lit(dec!(30.0))),
188 implied_volatility,
189 quantity,
190 contract_size: Positive::ONE,
191 underlying_price,
192 risk_free_rate: dec!(0.05),
193 option_style,
194 dividend_yield: pos_lit(dec!(0.01)),
195 exotic_params: None,
196 },
197 premium: pos_lit(dec!(5.0)),
198 date: Utc::now(),
199 open_fee: pos_lit(dec!(0.5)),
200 close_fee: pos_lit(dec!(0.5)),
201 epic: Some("Epic123".to_string()),
202 extra_fields: None,
203 }
204}
205
206/// Creates a sample Options object with a specific expiration date.
207///
208/// This utility function simplifies the creation of option contracts for testing
209/// or demonstration purposes by providing a specific expiration date using a NaiveDateTime.
210/// It creates a European-style option with predefined parameters and a fixed risk-free rate of 5%
211/// and dividend yield of 1%.
212///
213/// # Parameters
214///
215/// * `option_style` - The style of the option (Call or Put)
216/// * `side` - The position side (Long or Short)
217/// * `underlying_price` - The current price of the underlying asset
218/// * `quantity` - The number of option contracts
219/// * `strike_price` - The strike price of the option
220/// * `volatility` - The implied volatility for pricing the option
221/// * `naive_date` - The expiration date and time in naive format (will be converted to UTC)
222///
223/// # Returns
224///
225/// Returns a fully configured Options instance with "AAPL" as the underlying symbol.
226#[must_use]
227pub fn create_sample_option_with_date(
228 option_style: OptionStyle,
229 side: Side,
230 underlying_price: Positive,
231 quantity: Positive,
232 strike_price: Positive,
233 volatility: Positive,
234 naive_date: NaiveDateTime,
235) -> Options {
236 Options::new(
237 OptionType::European,
238 side,
239 "AAPL".to_string(),
240 strike_price,
241 ExpirationDate::DateTime(Utc.from_utc_datetime(&naive_date)),
242 volatility,
243 quantity,
244 underlying_price,
245 dec!(0.05),
246 option_style,
247 pos_lit(dec!(0.01)),
248 None,
249 )
250}
251
252/// Creates a sample Options object with a specific expiration date in days.
253///
254/// This utility function simplifies the creation of option contracts for testing
255/// or demonstration purposes by providing a specific expiration date expressed
256/// in days.
257/// It creates a European-style option with predefined parameters and a fixed
258/// risk-free rate of 5% and dividend yield of 1%.
259///
260/// # Parameters
261///
262/// * `option_style` - The style of the option (Call or Put)
263/// * `side` - The position side (Long or Short)
264/// * `underlying_price` - The current price of the underlying asset
265/// * `quantity` - The number of option contracts
266/// * `strike_price` - The strike price of the option
267/// * `volatility` - The implied volatility for pricing the option
268/// * `expiration_days` - The expiration days
269///
270/// # Returns
271///
272/// Returns a fully configured Options instance with "AAPL" as the underlying symbol.
273#[must_use]
274pub fn create_sample_option_with_days(
275 option_style: OptionStyle,
276 side: Side,
277 underlying_price: Positive,
278 quantity: Positive,
279 strike_price: Positive,
280 volatility: Positive,
281 expiration_days: Positive,
282) -> Options {
283 Options::new(
284 OptionType::European,
285 side,
286 "AAPL".to_string(),
287 strike_price,
288 ExpirationDate::Days(expiration_days),
289 volatility,
290 quantity,
291 underlying_price,
292 dec!(0.05),
293 option_style,
294 pos_lit(dec!(0.01)),
295 None,
296 )
297}
298
299/// Creates a simplified sample option contract for testing or demonstration purposes.
300///
301/// This function generates an Options instance with pre-defined values, requiring only
302/// the specification of the option style (Call or Put) and market position (Long or Short).
303/// It uses Apple Inc. (AAPL) as the underlying security with standard parameters suitable
304/// for basic examples or testing scenarios.
305///
306/// # Parameters
307///
308/// * `option_style` - Specifies whether the option is a Call or Put
309/// * `side` - Indicates whether the position is Long or Short
310///
311/// # Returns
312///
313/// Returns an `Options` instance with the following predefined values:
314/// - European-style option
315/// - AAPL as the underlying symbol
316/// - Strike price of $100.0
317/// - 30 days until expiration
318/// - 20% implied volatility (0.2)
319/// - Quantity of 1.0 contracts
320/// - Underlying price of $100.0
321/// - 5% risk-free rate
322/// - 1% dividend yield
323/// - No exotic parameters
324///
325/// # Examples
326///
327/// ```
328/// use optionstratlib_core::model::utils::create_sample_option_simplest;
329/// use optionstratlib_core::model::{OptionStyle, Side};
330/// let long_call = create_sample_option_simplest(OptionStyle::Call, Side::Long);
331/// let short_put = create_sample_option_simplest(OptionStyle::Put, Side::Short);
332/// ```
333#[must_use]
334pub fn create_sample_option_simplest(option_style: OptionStyle, side: Side) -> Options {
335 Options::new(
336 OptionType::European,
337 side,
338 "AAPL".to_string(),
339 Positive::HUNDRED,
340 ExpirationDate::Days(pos_lit(dec!(30.0))),
341 pos_lit(dec!(0.2)),
342 Positive::ONE,
343 Positive::HUNDRED,
344 dec!(0.05),
345 option_style,
346 pos_lit(dec!(0.01)),
347 None,
348 )
349}
350
351/// Creates a sample option with specified parameters and default values.
352///
353/// This function provides a convenient way to create an `Options` instance with common default values
354/// while allowing customization of the most important parameters: side, option style, and strike price.
355/// All other parameters are set to reasonable defaults for testing or demonstration purposes.
356///
357/// # Parameters
358/// * `side` - The position side (Long or Short) for the option.
359/// * `option_style` - The style of option (Call or Put).
360/// * `strike` - The strike price of the option as a `Positive` value.
361///
362/// # Returns
363/// An `Options` instance representing a European option on AAPL stock with:
364/// * 30 days until expiration
365/// * 20% implied volatility
366/// * Quantity of 1.0
367/// * Underlying price of $100.0
368/// * 5% risk-free rate
369/// * 1% dividend yield
370/// * No exotic parameters
371///
372/// # Examples
373/// ```
374/// use optionstratlib_core::model::utils::create_sample_option_simplest_strike;
375/// use optionstratlib_core::model::{OptionStyle, Side};
376/// use positive::pos_or_panic;
377/// let long_call = create_sample_option_simplest_strike(
378/// Side::Long,
379/// OptionStyle::Call,
380/// pos_or_panic!(105.0)
381/// );
382/// ```
383#[must_use]
384pub fn create_sample_option_simplest_strike(
385 side: Side,
386 option_style: OptionStyle,
387 strike: Positive,
388) -> Options {
389 Options::new(
390 OptionType::European,
391 side,
392 "AAPL".to_string(),
393 strike,
394 ExpirationDate::Days(pos_lit(dec!(30.0))),
395 pos_lit(dec!(0.2)),
396 Positive::ONE,
397 Positive::HUNDRED,
398 dec!(0.05),
399 option_style,
400 pos_lit(dec!(0.01)),
401 None,
402 )
403}
404
405/// Computes the mean and standard deviation of a vector containing `Positive` values.
406///
407/// # Arguments
408///
409/// * `vec` - A non-empty `Vec<Positive>` containing the data for which the mean and standard
410/// deviation are to be calculated.
411///
412/// # Returns
413///
414/// A tuple containing:
415/// - `Positive` - The mean of the provided vector.
416/// - `Positive` - The standard deviation of the provided vector.
417///
418/// # Errors
419///
420/// Returns [`PositiveError::ArithmeticError`] when the sample is empty — no
421/// mean is defined for it — and when accumulating the values or the squared
422/// deviations overflows the `Positive` range. Returns
423/// [`PositiveError::InvalidValue`] when a squared deviation is not
424/// representable as a finite `f64`. The raw `Positive` operators abort the
425/// process in all of those cases, so every step is taken through its checked
426/// counterpart.
427///
428/// # Example
429///
430/// ```rust
431/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
432/// use positive::Positive;
433/// use optionstratlib_core::model::utils::mean_and_std;
434///
435/// let data = vec![
436/// Positive::new(2.0)?, Positive::new(4.0)?, Positive::new(4.0)?, Positive::new(4.0)?,
437/// Positive::new(5.0)?, Positive::new(5.0)?, Positive::new(7.0)?, Positive::new(9.0)?,
438/// ];
439/// let (mean, std) = mean_and_std(data)?;
440///
441/// assert_eq!(mean.to_f64(), 5.0);
442/// assert_eq!(std.to_f64(), 4.0_f64.sqrt());
443/// # Ok(())
444/// # }
445/// ```
446///
447/// # Details
448///
449/// - The mean is computed by summing the `Positive` values and dividing by the count of elements.
450/// - The standard deviation is derived from the variance, which is the average of the squared differences
451/// from the mean. The variance is then converted into standard deviation by taking its square root.
452///
453/// Note: The `Positive` type and associated operations are defined in the `crate::model::types` module.
454pub fn mean_and_std(vec: Vec<Positive>) -> Result<(Positive, Positive), PositiveError> {
455 if vec.is_empty() {
456 return Err(PositiveError::ArithmeticError {
457 operation: "mean_and_std".to_string(),
458 reason: "an empty sample has no mean".to_string(),
459 });
460 }
461 // `Decimal::from(usize)` is exact for every length a `Vec` can hold, so
462 // the divisor introduces no rounding of its own.
463 let count = Decimal::from(vec.len());
464
465 let mut total = Positive::ZERO;
466 for value in &vec {
467 total = total.checked_add(value)?;
468 }
469 let mean = total.checked_div_dec(count)?;
470
471 // The squared deviations are taken in `f64`, as they always have been, so
472 // the returned figures are unchanged for every sample the panicking form
473 // accepted. What changes is the reporting: a deviation that overflows
474 // `f64` now surfaces instead of collapsing to zero.
475 // Each `Decimal` reaches `f64` through `decimal_to_f64` (#828); its error
476 // is reported as the arithmetic error this function returns.
477 let to_f64 = |value: Positive| {
478 decimal_to_f64(value.to_dec()).map_err(|e| PositiveError::ArithmeticError {
479 operation: "mean_and_std".to_string(),
480 reason: e.to_string(),
481 })
482 };
483 let mean_f64 = to_f64(mean)?;
484 let mut squared_deviations = Positive::ZERO;
485 for value in &vec {
486 let deviation = Positive::new((to_f64(*value)? - mean_f64).powi(2))?;
487 squared_deviations = squared_deviations.checked_add(&deviation)?;
488 }
489 let variance = squared_deviations.checked_div_dec(count)?;
490 let std = Positive::new(to_f64(variance)?.sqrt())?; // scan-banned: allow -- f64 `sqrt`: returns NaN for negative input, it does not abort; the non-finite value is rejected at the `Decimal` boundary
491
492 Ok((mean, std))
493}
494
495/// Trait for rounding operations on numeric types, specifically for financial calculations.
496///
497/// This trait provides methods to round a number to the nearest integer and to a specified
498/// number of decimal places, ensuring precision and accuracy in financial computations.
499///
500pub trait ToRound {
501 /// Rounds the number to the nearest integer.
502 ///
503 /// This method rounds the number to the nearest whole number, removing any fractional part.
504 fn round(&self) -> Decimal;
505
506 /// Rounds the number to a specified number of decimal places.
507 ///
508 /// This method rounds the number to the specified number of digits after the decimal point,
509 /// providing control over the precision of the rounded value.
510 ///
511 /// # Arguments
512 ///
513 /// * `decimal_places` - The number of decimal places to round to.
514 fn round_to(&self, decimal_places: u32) -> Decimal;
515}
516
517/// Generates a price vector for the payoff graph: `num_points` prices evenly
518/// spaced from `min_price` to `max_price`, both ends included.
519///
520/// # Errors
521///
522/// Returns [`DecimalError::InvalidValue`] when `num_points` is below two,
523/// so there is no spacing between the ends (one point divided by zero and
524/// zero points underflowed `num_points - 1`, #788), or too large to
525/// allocate, and
526/// [`DecimalError::Overflow`] when the range, the step or a price leaves the
527/// `Decimal` range.
528pub fn generate_price_points(
529 min_price: Decimal,
530 max_price: Decimal,
531 num_points: usize,
532) -> Result<Vec<Decimal>, DecimalError> {
533 let Some(intervals) = num_points.checked_sub(1).filter(|n| *n > 0) else {
534 return Err(DecimalError::invalid_value(
535 num_points as f64,
536 "a price grid needs at least two points",
537 ));
538 };
539 let range = d_sub(
540 max_price,
541 min_price,
542 "model::utils::generate_price_points/range",
543 )?;
544 let step = d_div(
545 range,
546 Decimal::from(intervals),
547 "model::utils::generate_price_points/step",
548 )?;
549 // Reserved fallibly: `Vec::with_capacity` aborts with `capacity overflow`
550 // on a count wider than the address space.
551 let mut prices = Vec::new();
552 prices.try_reserve_exact(num_points).map_err(|_| {
553 DecimalError::invalid_value(
554 num_points as f64,
555 "a price grid this large cannot be allocated",
556 )
557 })?;
558
559 for i in 0..num_points {
560 let offset = d_mul(
561 step,
562 Decimal::from(i),
563 "model::utils::generate_price_points/offset",
564 )?;
565 prices.push(d_add(
566 min_price,
567 offset,
568 "model::utils::generate_price_points/price",
569 )?);
570 }
571
572 Ok(prices)
573}
574
575#[cfg(test)]
576mod tests_generate_price_points {
577 use super::*;
578
579 #[test]
580 fn test_generate_price_points_spans_both_ends() {
581 let prices = generate_price_points(dec!(90), dec!(110), 5);
582 assert_eq!(
583 prices.ok(),
584 Some(vec![dec!(90), dec!(95), dec!(100), dec!(105), dec!(110)])
585 );
586 }
587
588 // One point divided by zero (`Division by zero`) and zero points
589 // underflowed `num_points - 1` (#788).
590 #[test]
591 fn test_generate_price_points_below_two_points_is_error() {
592 for num_points in [0, 1] {
593 assert!(matches!(
594 generate_price_points(dec!(90), dec!(110), num_points),
595 Err(DecimalError::InvalidValue { .. })
596 ));
597 }
598 }
599
600 // `Vec::with_capacity(usize::MAX)` aborted with `capacity overflow`.
601 #[test]
602 fn test_generate_price_points_unallocatable_is_error() {
603 assert!(matches!(
604 generate_price_points(dec!(90), dec!(110), usize::MAX),
605 Err(DecimalError::InvalidValue { .. })
606 ));
607 }
608
609 #[test]
610 fn test_generate_price_points_overflow_is_error() {
611 assert!(generate_price_points(Decimal::MIN, Decimal::MAX, 3).is_err());
612 }
613}
614
615#[cfg(test)]
616mod tests_positive_f64_to_f64 {
617 use super::*;
618
619 #[test]
620 fn test_positive_f64_to_f64_non_empty() {
621 let positive_vec = vec![
622 Positive::new(10.0).unwrap(),
623 Positive::new(20.0).unwrap(),
624 Positive::new(30.0).unwrap(),
625 ];
626
627 let f64_vec = positive_f64_to_f64(positive_vec);
628
629 assert!(matches!(f64_vec, Ok(v) if v == vec![10.0, 20.0, 30.0]));
630 }
631
632 #[test]
633 fn test_positive_f64_to_f64_single_element() {
634 let positive_vec = vec![Positive::new(42.0).unwrap()];
635
636 let f64_vec = positive_f64_to_f64(positive_vec);
637
638 assert!(matches!(f64_vec, Ok(v) if v == vec![42.0]));
639 }
640
641 #[test]
642 #[should_panic]
643 fn test_positive_f64_to_f64_invalid_positivef64() {
644 Positive::new(-10.0).unwrap();
645 }
646}
647
648#[cfg(test)]
649mod tests_mean_and_std {
650 use super::*;
651
652 use approx::assert_relative_eq;
653 use positive::pos_or_panic;
654
655 #[test]
656 fn test_basic_mean_and_std() {
657 let values = vec![
658 Positive::TWO,
659 pos_or_panic!(4.0),
660 pos_or_panic!(4.0),
661 pos_or_panic!(4.0),
662 pos_or_panic!(5.0),
663 pos_or_panic!(5.0),
664 pos_or_panic!(7.0),
665 pos_or_panic!(9.0),
666 ];
667 let (mean, std) = mean_and_std(values).expect("the sample is non-empty and finite");
668
669 assert_relative_eq!(mean.to_f64(), 5.0, epsilon = 0.0001);
670 assert_relative_eq!(std.to_f64(), 2.0, epsilon = 0.0001);
671 }
672
673 #[test]
674 fn test_identical_values() {
675 let values = vec![
676 pos_or_panic!(5.0),
677 pos_or_panic!(5.0),
678 pos_or_panic!(5.0),
679 pos_or_panic!(5.0),
680 ];
681 let (mean, std) = mean_and_std(values).expect("the sample is non-empty and finite");
682
683 assert_relative_eq!(mean.to_f64(), 5.0, epsilon = 0.0001);
684 assert_relative_eq!(std.to_f64(), 0.0, epsilon = 0.0001);
685 }
686
687 #[test]
688 fn test_single_value() {
689 let values = vec![pos_or_panic!(3.0)];
690 let (mean, std) = mean_and_std(values).expect("the sample is non-empty and finite");
691
692 assert_relative_eq!(mean.to_f64(), 3.0, epsilon = 0.0001);
693 assert_relative_eq!(std.to_f64(), 0.0, epsilon = 0.0001);
694 }
695
696 #[test]
697 fn test_small_numbers() {
698 let values = vec![pos_or_panic!(0.1), pos_or_panic!(0.2), pos_or_panic!(0.3)];
699 let (mean, std) = mean_and_std(values).expect("the sample is non-empty and finite");
700
701 assert_relative_eq!(mean.to_f64(), 0.2, epsilon = 0.0001);
702 assert_relative_eq!(std.to_f64(), 0.08164966, epsilon = 0.0001);
703 }
704
705 #[test]
706 fn test_large_numbers() {
707 let values = vec![
708 pos_or_panic!(1000.0),
709 pos_or_panic!(2000.0),
710 pos_or_panic!(3000.0),
711 ];
712 let (mean, std) = mean_and_std(values).expect("the sample is non-empty and finite");
713
714 assert_relative_eq!(mean.to_f64(), 2000.0, epsilon = 0.0001);
715 assert_relative_eq!(std.to_f64(), 816.4966, epsilon = 0.1);
716 }
717
718 #[test]
719 fn test_mixed_range() {
720 let values = vec![
721 pos_or_panic!(0.5),
722 pos_or_panic!(5.0),
723 pos_or_panic!(50.0),
724 pos_or_panic!(500.0),
725 ];
726 let (mean, std) = mean_and_std(values).expect("the sample is non-empty and finite");
727
728 assert_relative_eq!(mean.to_f64(), 138.875, epsilon = 0.001);
729 assert_relative_eq!(std.to_f64(), 209.392, epsilon = 0.001);
730 }
731
732 #[test]
733 fn test_empty_vector() {
734 let values: Vec<Positive> = vec![];
735 let error = mean_and_std(values).expect_err("an empty sample has no mean");
736 assert!(matches!(error, PositiveError::ArithmeticError { .. }));
737 }
738
739 #[test]
740 fn test_sum_overflow_returns_error() {
741 let values = vec![Positive::MAX, Positive::MAX];
742 let error = mean_and_std(values).expect_err("two maxima overflow the sum");
743 assert!(matches!(error, PositiveError::ArithmeticError { .. }));
744 }
745
746 #[test]
747 fn test_symmetric_distribution() {
748 let values = vec![
749 Positive::ONE,
750 Positive::TWO,
751 pos_or_panic!(3.0),
752 pos_or_panic!(4.0),
753 pos_or_panic!(5.0),
754 ];
755 let (mean, std) = mean_and_std(values).expect("the sample is non-empty and finite");
756
757 assert_relative_eq!(mean.to_f64(), 3.0, epsilon = 0.0001);
758 assert_relative_eq!(std.to_f64(), std::f64::consts::SQRT_2, epsilon = 0.0001);
759 }
760
761 #[test]
762 fn test_result_is_positive() {
763 let values = vec![Positive::ONE, Positive::TWO, pos_or_panic!(3.0)];
764 let (mean, std) = mean_and_std(values).expect("the sample is non-empty and finite");
765
766 assert!(mean > Positive::ZERO);
767 assert!(std > Positive::ZERO);
768 }
769
770 #[test]
771 fn test_precision() {
772 let values = vec![
773 pos_or_panic!(1.23456789),
774 pos_or_panic!(2.34567890),
775 pos_or_panic!(3.45678901),
776 ];
777 let (mean, std) = mean_and_std(values).expect("the sample is non-empty and finite");
778
779 assert_relative_eq!(mean.to_f64(), 2.34567860, epsilon = 0.00000001);
780 assert_relative_eq!(std.to_f64(), 0.90721797, epsilon = 0.00000001);
781 }
782
783 #[test]
784 fn test_precision_bis() {
785 let values = vec![
786 pos_or_panic!(0.123456789),
787 pos_or_panic!(0.134567890),
788 pos_or_panic!(0.145678901),
789 ];
790 let (mean, std) = mean_and_std(values).expect("the sample is non-empty and finite");
791
792 assert_relative_eq!(mean.to_f64(), 0.13456786, epsilon = 0.00000001);
793 assert_relative_eq!(std.to_f64(), 0.00907213, epsilon = 0.00000001);
794 }
795}