Skip to main content

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}