optionstratlib-core 0.23.0

Core domain model of OptionStratLib: options, positions, legs, trades, checked Decimal helpers and their errors. Re-exported by the optionstratlib facade on every feature set.
Documentation
/******************************************************************************
   Author: Joaquín Béjar García
   Email: jb@taunais.com
   Date: 27/9/24
******************************************************************************/
//! Randomness helpers owned by `core`.
//!
//! Deterministic seeding for reproducible Monte-Carlo and simulation tests
//! (`rules/global_rules.md`, "Numerical Discipline"), plus the generic
//! `BTreeSet` and `Decimal` sampling helpers every layer above uses. They
//! depend on nothing but `core` types, so `core` is their defining owner
//! even though `market`, `pricing` and `simulation` are the callers.

use crate::error::DecimalError;
use num_traits::FromPrimitive;
use rand::rngs::StdRng;
use rand::{Rng, RngExt, SeedableRng, rng};
use rust_decimal::Decimal;
use std::collections::BTreeSet;

/// Fixed seed used by `deterministic_rng()` when no seed is supplied.
/// Chosen as an unremarkable prime so test authors can copy/paste the
/// same seed elsewhere without having to guess what magic number is
/// in play.
pub const DETERMINISTIC_RNG_DEFAULT_SEED: u64 = 0x0B_AD_C0_FF_EE_15_DE_AD;

/// Returns a seeded [`StdRng`] for reproducible Monte-Carlo / simulation
/// tests.
///
/// Per `rules/global_rules.md` §Numerical Discipline, every Monte-Carlo
/// and simulation test must seed deterministically so that an upstream
/// arithmetic-precision shift cannot flip an assertion by luck. Use this
/// helper instead of `rand::rng()` / `rand::random()` inside any
/// `#[cfg(test)]` block whose expected values depend on the sample
/// stream.
///
/// ```rust
/// use optionstratlib_core::utils::deterministic_rng;
/// use rand::RngExt;
/// let mut rng = deterministic_rng(42);
/// let _ = rng.random::<u64>();
/// ```
#[must_use]
#[inline]
pub fn deterministic_rng(seed: u64) -> StdRng {
    StdRng::seed_from_u64(seed)
}

/// Gets a random element from a BTreeSet, drawing from the thread-local RNG.
///
/// This is the unseeded path: every call reads [`rand::rng()`], so two runs
/// pick different elements. Reproducible callers use
/// [`get_random_element_with`] with a seeded generator such as
/// [`deterministic_rng`]; this function is exactly that helper applied to the
/// thread-local RNG.
///
/// # Type Parameters
///
/// * `T` - The type of elements in the BTreeSet
///
/// # Arguments
///
/// * `set` - A reference to a BTreeSet containing elements of type T
///
/// # Returns
///
/// * `Option<&T>` - A reference to a random element from the set, or None if the set is empty
///
#[must_use]
pub fn get_random_element<T>(set: &BTreeSet<T>) -> Option<&T> {
    get_random_element_with(set, &mut rng())
}

/// Gets a uniformly chosen element from a BTreeSet, drawing from `rng`.
///
/// One index is drawn with [`RngExt::random_range`] over `0..set.len()`, so
/// a seeded `rng` picks the same element on every run. An empty set draws
/// nothing and returns `None`.
///
/// # Arguments
///
/// * `set` - The set to pick from.
/// * `rng` - The generator the index is drawn from.
///
/// # Returns
///
/// A reference to the chosen element, or `None` if the set is empty.
///
/// # Examples
///
/// ```rust
/// use optionstratlib_core::utils::{deterministic_rng, get_random_element_with};
/// use std::collections::BTreeSet;
///
/// let set: BTreeSet<u32> = (0..10).collect();
/// let first = get_random_element_with(&set, &mut deterministic_rng(7));
/// let second = get_random_element_with(&set, &mut deterministic_rng(7));
/// assert_eq!(first, second);
/// ```
#[must_use]
pub fn get_random_element_with<'a, T, R: Rng + ?Sized>(
    set: &'a BTreeSet<T>,
    rng: &mut R,
) -> Option<&'a T> {
    if set.is_empty() {
        return None;
    }
    let random_index = rng.random_range(0..set.len());
    set.iter().nth(random_index)
}

/// Generates a random `Decimal` value using the provided random number generator.
///
/// This function takes a mutable reference to a random number generator (`rand::Rng`)
/// and uses it to generate a random `f64` value, which is then converted to a `Decimal`.
///
/// # Arguments
///
/// * `rng` - A mutable reference to a random number generator. This allows the function
///   to generate different random numbers on each call.
///
/// # Returns
///
/// A `Result` containing either the generated `Decimal` or a `DecimalError` if the
/// conversion from `f64` to `Decimal` fails.
///
/// # Errors
///
/// Returns a `DecimalError::ConversionError` if the `f64` value generated by the random
/// number generator cannot be converted to a `Decimal`. This can occur if the `f64`
/// value is NaN or infinite.
///
pub fn random_decimal<R: Rng + ?Sized>(rng: &mut R) -> Result<Decimal, DecimalError> {
    Decimal::from_f64(rng.random::<f64>()).ok_or_else(|| DecimalError::ConversionError {
        // The source type being converted from
        from_type: "f64".to_string(),
        // The destination type being converted to
        to_type: "Decimal".to_string(),
        // Detailed explanation of why the conversion failed
        reason: "Failed to convert f64 to Decimal".to_string(),
    })
}

#[cfg(test)]
mod tests_get_random_element {
    use super::*;

    use std::collections::BTreeSet;

    #[test]
    fn test_get_random_element_empty_set() {
        let set: BTreeSet<i32> = BTreeSet::new();
        assert!(get_random_element(&set).is_none());
    }

    #[test]
    fn test_get_random_element_single_element() {
        let mut set = BTreeSet::new();
        set.insert(42);
        assert_eq!(get_random_element(&set), Some(&42));
    }

    #[test]
    fn test_get_random_element_multiple_elements() {
        let mut set = BTreeSet::new();
        for i in 0..5 {
            set.insert(i);
        }
        let random_element = get_random_element_with(&set, &mut deterministic_rng(3));
        assert!(random_element.is_some());
        assert!((0..5).contains(random_element.unwrap()));
    }

    #[test]
    fn test_get_random_element_distribution() {
        // Test that the distribution is somewhat uniform
        let mut set = BTreeSet::new();
        for i in 0..3 {
            set.insert(i);
        }

        let mut counts = vec![0; 3];
        let mut rng = deterministic_rng(DETERMINISTIC_RNG_DEFAULT_SEED);
        for _ in 0..1000 {
            if let Some(&value) = get_random_element_with(&set, &mut rng) {
                counts[value as usize] += 1;
            }
        }

        // Check that each element was selected at least some times
        // (allowing for some random variation)
        for count in counts {
            assert!(count > 200); // Should be around 333 for uniform distribution
        }
    }
}

#[cfg(test)]
mod tests_get_random_element_with {
    use super::*;

    #[test]
    fn test_get_random_element_with_empty_set_returns_none() {
        let set: BTreeSet<i32> = BTreeSet::new();
        assert!(get_random_element_with(&set, &mut deterministic_rng(1)).is_none());
    }

    #[test]
    fn test_get_random_element_with_same_seed_same_sequence() {
        let set: BTreeSet<u32> = (0..100).collect();
        let mut a = deterministic_rng(DETERMINISTIC_RNG_DEFAULT_SEED);
        let mut b = deterministic_rng(DETERMINISTIC_RNG_DEFAULT_SEED);
        for _ in 0..50 {
            assert_eq!(
                get_random_element_with(&set, &mut a),
                get_random_element_with(&set, &mut b)
            );
        }
    }

    #[test]
    fn test_get_random_element_with_seeded_distribution_is_uniform() {
        let set: BTreeSet<usize> = (0..3).collect();
        let mut rng = deterministic_rng(42);
        let mut counts = [0_u32; 3];
        let n = 30_000_u32;
        for _ in 0..n {
            let value = get_random_element_with(&set, &mut rng).copied();
            assert!(value.is_some());
            if let Some(v) = value {
                counts[v] += 1;
            }
        }
        // Each count is Binomial(n, 1/3): sd = sqrt(n * 1/3 * 2/3) ~ 81.6.
        // A 5 sd band around n / 3 = 10_000.
        for count in counts {
            assert!((9_592..=10_408).contains(&count), "count {count}");
        }
    }
}

#[cfg(test)]
mod tests_random_decimal {
    use super::*;
    use rand::SeedableRng;
    use rand::rngs::SmallRng;
    // use rand::rngs::mock::StepRng; // Deprecated - replaced with SmallRng

    use tracing::info;

    #[test]
    fn test_random_decimal_generates_valid_value() {
        // Create a deterministic RNG for testing
        let mut t_rng = SmallRng::seed_from_u64(42);

        // Call the function
        let result = random_decimal(&mut t_rng);

        // Check that it succeeded
        assert!(result.is_ok());

        // Verify the value is within expected range [0.0, 1.0)
        let decimal = result.unwrap();
        assert!(decimal >= Decimal::ZERO);
        assert!(decimal < Decimal::ONE);
    }

    #[test]
    fn test_random_decimal_different_calls_different_values() {
        // Use SmallRng instead of StepRng to ensure different values
        let mut t_rng = SmallRng::seed_from_u64(42);

        // Generate two decimal values
        let decimal1 = random_decimal(&mut t_rng).unwrap();
        let decimal2 = random_decimal(&mut t_rng).unwrap();

        // Values should be different
        assert_ne!(decimal1, decimal2);
    }

    #[test]
    fn test_random_decimal_reproduces_expected_values() {
        // Using a known seed should produce predictable outputs
        let mut t_rng = SmallRng::seed_from_u64(12345);

        // Generate the value once
        let decimal = random_decimal(&mut t_rng).unwrap();

        // Store this value for future runs
        info!("Generated decimal: {}", decimal);

        // Reset RNG with same seed
        let mut rng2 = SmallRng::seed_from_u64(12345);

        // The second generation should match the first
        let decimal2 = random_decimal(&mut rng2).unwrap();
        assert_eq!(decimal, decimal2);
    }

    #[test]
    fn test_random_decimal_with_multiple_rng_types() {
        // Test with different RNG implementations

        // Small RNG with deterministic seed (replaces deprecated StepRng)
        {
            let mut t_rng = SmallRng::seed_from_u64(1);
            assert!(random_decimal(&mut t_rng).is_ok());
        }

        // Thread-local RNG
        {
            let mut t_rng = rng();
            assert!(random_decimal(&mut t_rng).is_ok());
        }

        // Small RNG with seed
        {
            let mut t_rng = SmallRng::seed_from_u64(42);
            assert!(random_decimal(&mut t_rng).is_ok());
        }
    }

    // This test verifies we can create multiple random decimals
    #[test]
    fn test_multiple_random_decimals() {
        let mut t_rng = SmallRng::seed_from_u64(42);
        let decimals: Vec<Decimal> = (0..10)
            .map(|_| random_decimal(&mut t_rng).unwrap())
            .collect();

        // Check we have 10 values
        assert_eq!(decimals.len(), 10);

        // Check they're all different
        for i in 0..9 {
            assert_ne!(decimals[i], decimals[i + 1]);
        }
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests_deterministic_rng {
    use super::*;

    #[test]
    fn same_seed_yields_identical_stream() {
        let mut a = deterministic_rng(12345);
        let mut b = deterministic_rng(12345);
        for _ in 0..16 {
            assert_eq!(a.random::<u64>(), b.random::<u64>());
        }
    }

    #[test]
    fn different_seeds_diverge() {
        let mut a = deterministic_rng(1);
        let mut b = deterministic_rng(2);
        // Not a strict guarantee for two draws, but vanishingly unlikely
        // to collide on StdRng for the default seed-mixing routine.
        let mut collisions = 0;
        for _ in 0..32 {
            if a.random::<u64>() == b.random::<u64>() {
                collisions += 1;
            }
        }
        assert!(
            collisions <= 1,
            "expected diverging streams, got {collisions} collisions"
        );
    }

    #[test]
    fn default_seed_is_stable_across_calls() {
        let mut a = deterministic_rng(DETERMINISTIC_RNG_DEFAULT_SEED);
        let mut b = deterministic_rng(DETERMINISTIC_RNG_DEFAULT_SEED);
        assert_eq!(a.random::<u64>(), b.random::<u64>());
    }
}