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;
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}