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