Skip to main content

optionstratlib_core/utils/
rng.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 27/9/24
5******************************************************************************/
6//! Randomness helpers owned by `core`.
7//!
8//! Deterministic seeding for reproducible Monte-Carlo and simulation tests
9//! (`rules/global_rules.md`, "Numerical Discipline"), plus the generic
10//! `BTreeSet` and `Decimal` sampling helpers every layer above uses. They
11//! depend on nothing but `core` types, so `core` is their defining owner
12//! even though `market`, `pricing` and `simulation` are the callers.
13
14use crate::error::DecimalError;
15use num_traits::FromPrimitive;
16use rand::rngs::StdRng;
17use rand::{Rng, RngExt, SeedableRng, rng};
18use rust_decimal::Decimal;
19use std::collections::BTreeSet;
20
21/// Fixed seed used by `deterministic_rng()` when no seed is supplied.
22/// Chosen as an unremarkable prime so test authors can copy/paste the
23/// same seed elsewhere without having to guess what magic number is
24/// in play.
25pub const DETERMINISTIC_RNG_DEFAULT_SEED: u64 = 0x0B_AD_C0_FF_EE_15_DE_AD;
26
27/// Returns a seeded [`StdRng`] for reproducible Monte-Carlo / simulation
28/// tests.
29///
30/// Per `rules/global_rules.md` §Numerical Discipline, every Monte-Carlo
31/// and simulation test must seed deterministically so that an upstream
32/// arithmetic-precision shift cannot flip an assertion by luck. Use this
33/// helper instead of `rand::rng()` / `rand::random()` inside any
34/// `#[cfg(test)]` block whose expected values depend on the sample
35/// stream.
36///
37/// ```rust
38/// use optionstratlib_core::utils::deterministic_rng;
39/// use rand::RngExt;
40/// let mut rng = deterministic_rng(42);
41/// let _ = rng.random::<u64>();
42/// ```
43#[must_use]
44#[inline]
45pub fn deterministic_rng(seed: u64) -> StdRng {
46    StdRng::seed_from_u64(seed)
47}
48
49/// Gets a random element from a BTreeSet, drawing from the thread-local RNG.
50///
51/// This is the unseeded path: every call reads [`rand::rng()`], so two runs
52/// pick different elements. Reproducible callers use
53/// [`get_random_element_with`] with a seeded generator such as
54/// [`deterministic_rng`]; this function is exactly that helper applied to the
55/// thread-local RNG.
56///
57/// # Type Parameters
58///
59/// * `T` - The type of elements in the BTreeSet
60///
61/// # Arguments
62///
63/// * `set` - A reference to a BTreeSet containing elements of type T
64///
65/// # Returns
66///
67/// * `Option<&T>` - A reference to a random element from the set, or None if the set is empty
68///
69#[must_use]
70pub fn get_random_element<T>(set: &BTreeSet<T>) -> Option<&T> {
71    get_random_element_with(set, &mut rng())
72}
73
74/// Gets a uniformly chosen element from a BTreeSet, drawing from `rng`.
75///
76/// One index is drawn with [`RngExt::random_range`] over `0..set.len()`, so
77/// a seeded `rng` picks the same element on every run. An empty set draws
78/// nothing and returns `None`.
79///
80/// # Arguments
81///
82/// * `set` - The set to pick from.
83/// * `rng` - The generator the index is drawn from.
84///
85/// # Returns
86///
87/// A reference to the chosen element, or `None` if the set is empty.
88///
89/// # Examples
90///
91/// ```rust
92/// use optionstratlib_core::utils::{deterministic_rng, get_random_element_with};
93/// use std::collections::BTreeSet;
94///
95/// let set: BTreeSet<u32> = (0..10).collect();
96/// let first = get_random_element_with(&set, &mut deterministic_rng(7));
97/// let second = get_random_element_with(&set, &mut deterministic_rng(7));
98/// assert_eq!(first, second);
99/// ```
100#[must_use]
101pub fn get_random_element_with<'a, T, R: Rng + ?Sized>(
102    set: &'a BTreeSet<T>,
103    rng: &mut R,
104) -> Option<&'a T> {
105    if set.is_empty() {
106        return None;
107    }
108    let random_index = rng.random_range(0..set.len());
109    set.iter().nth(random_index)
110}
111
112/// Generates a random `Decimal` value using the provided random number generator.
113///
114/// This function takes a mutable reference to a random number generator (`rand::Rng`)
115/// and uses it to generate a random `f64` value, which is then converted to a `Decimal`.
116///
117/// # Arguments
118///
119/// * `rng` - A mutable reference to a random number generator. This allows the function
120///   to generate different random numbers on each call.
121///
122/// # Returns
123///
124/// A `Result` containing either the generated `Decimal` or a `DecimalError` if the
125/// conversion from `f64` to `Decimal` fails.
126///
127/// # Errors
128///
129/// Returns a `DecimalError::ConversionError` if the `f64` value generated by the random
130/// number generator cannot be converted to a `Decimal`. This can occur if the `f64`
131/// value is NaN or infinite.
132///
133pub fn random_decimal<R: Rng + ?Sized>(rng: &mut R) -> Result<Decimal, DecimalError> {
134    Decimal::from_f64(rng.random::<f64>()).ok_or_else(|| DecimalError::ConversionError {
135        // The source type being converted from
136        from_type: "f64".to_string(),
137        // The destination type being converted to
138        to_type: "Decimal".to_string(),
139        // Detailed explanation of why the conversion failed
140        reason: "Failed to convert f64 to Decimal".to_string(),
141    })
142}
143
144#[cfg(test)]
145mod tests_get_random_element {
146    use super::*;
147
148    use std::collections::BTreeSet;
149
150    #[test]
151    fn test_get_random_element_empty_set() {
152        let set: BTreeSet<i32> = BTreeSet::new();
153        assert!(get_random_element(&set).is_none());
154    }
155
156    #[test]
157    fn test_get_random_element_single_element() {
158        let mut set = BTreeSet::new();
159        set.insert(42);
160        assert_eq!(get_random_element(&set), Some(&42));
161    }
162
163    #[test]
164    fn test_get_random_element_multiple_elements() {
165        let mut set = BTreeSet::new();
166        for i in 0..5 {
167            set.insert(i);
168        }
169        let random_element = get_random_element_with(&set, &mut deterministic_rng(3));
170        assert!(random_element.is_some());
171        assert!((0..5).contains(random_element.unwrap()));
172    }
173
174    #[test]
175    fn test_get_random_element_distribution() {
176        // Test that the distribution is somewhat uniform
177        let mut set = BTreeSet::new();
178        for i in 0..3 {
179            set.insert(i);
180        }
181
182        let mut counts = vec![0; 3];
183        let mut rng = deterministic_rng(DETERMINISTIC_RNG_DEFAULT_SEED);
184        for _ in 0..1000 {
185            if let Some(&value) = get_random_element_with(&set, &mut rng) {
186                counts[value as usize] += 1;
187            }
188        }
189
190        // Check that each element was selected at least some times
191        // (allowing for some random variation)
192        for count in counts {
193            assert!(count > 200); // Should be around 333 for uniform distribution
194        }
195    }
196}
197
198#[cfg(test)]
199mod tests_get_random_element_with {
200    use super::*;
201
202    #[test]
203    fn test_get_random_element_with_empty_set_returns_none() {
204        let set: BTreeSet<i32> = BTreeSet::new();
205        assert!(get_random_element_with(&set, &mut deterministic_rng(1)).is_none());
206    }
207
208    #[test]
209    fn test_get_random_element_with_same_seed_same_sequence() {
210        let set: BTreeSet<u32> = (0..100).collect();
211        let mut a = deterministic_rng(DETERMINISTIC_RNG_DEFAULT_SEED);
212        let mut b = deterministic_rng(DETERMINISTIC_RNG_DEFAULT_SEED);
213        for _ in 0..50 {
214            assert_eq!(
215                get_random_element_with(&set, &mut a),
216                get_random_element_with(&set, &mut b)
217            );
218        }
219    }
220
221    #[test]
222    fn test_get_random_element_with_seeded_distribution_is_uniform() {
223        let set: BTreeSet<usize> = (0..3).collect();
224        let mut rng = deterministic_rng(42);
225        let mut counts = [0_u32; 3];
226        let n = 30_000_u32;
227        for _ in 0..n {
228            let value = get_random_element_with(&set, &mut rng).copied();
229            assert!(value.is_some());
230            if let Some(v) = value {
231                counts[v] += 1;
232            }
233        }
234        // Each count is Binomial(n, 1/3): sd = sqrt(n * 1/3 * 2/3) ~ 81.6.
235        // A 5 sd band around n / 3 = 10_000.
236        for count in counts {
237            assert!((9_592..=10_408).contains(&count), "count {count}");
238        }
239    }
240}
241
242#[cfg(test)]
243mod tests_random_decimal {
244    use super::*;
245    use rand::SeedableRng;
246    use rand::rngs::SmallRng;
247    // use rand::rngs::mock::StepRng; // Deprecated - replaced with SmallRng
248
249    use tracing::info;
250
251    #[test]
252    fn test_random_decimal_generates_valid_value() {
253        // Create a deterministic RNG for testing
254        let mut t_rng = SmallRng::seed_from_u64(42);
255
256        // Call the function
257        let result = random_decimal(&mut t_rng);
258
259        // Check that it succeeded
260        assert!(result.is_ok());
261
262        // Verify the value is within expected range [0.0, 1.0)
263        let decimal = result.unwrap();
264        assert!(decimal >= Decimal::ZERO);
265        assert!(decimal < Decimal::ONE);
266    }
267
268    #[test]
269    fn test_random_decimal_different_calls_different_values() {
270        // Use SmallRng instead of StepRng to ensure different values
271        let mut t_rng = SmallRng::seed_from_u64(42);
272
273        // Generate two decimal values
274        let decimal1 = random_decimal(&mut t_rng).unwrap();
275        let decimal2 = random_decimal(&mut t_rng).unwrap();
276
277        // Values should be different
278        assert_ne!(decimal1, decimal2);
279    }
280
281    #[test]
282    fn test_random_decimal_reproduces_expected_values() {
283        // Using a known seed should produce predictable outputs
284        let mut t_rng = SmallRng::seed_from_u64(12345);
285
286        // Generate the value once
287        let decimal = random_decimal(&mut t_rng).unwrap();
288
289        // Store this value for future runs
290        info!("Generated decimal: {}", decimal);
291
292        // Reset RNG with same seed
293        let mut rng2 = SmallRng::seed_from_u64(12345);
294
295        // The second generation should match the first
296        let decimal2 = random_decimal(&mut rng2).unwrap();
297        assert_eq!(decimal, decimal2);
298    }
299
300    #[test]
301    fn test_random_decimal_with_multiple_rng_types() {
302        // Test with different RNG implementations
303
304        // Small RNG with deterministic seed (replaces deprecated StepRng)
305        {
306            let mut t_rng = SmallRng::seed_from_u64(1);
307            assert!(random_decimal(&mut t_rng).is_ok());
308        }
309
310        // Thread-local RNG
311        {
312            let mut t_rng = rng();
313            assert!(random_decimal(&mut t_rng).is_ok());
314        }
315
316        // Small RNG with seed
317        {
318            let mut t_rng = SmallRng::seed_from_u64(42);
319            assert!(random_decimal(&mut t_rng).is_ok());
320        }
321    }
322
323    // This test verifies we can create multiple random decimals
324    #[test]
325    fn test_multiple_random_decimals() {
326        let mut t_rng = SmallRng::seed_from_u64(42);
327        let decimals: Vec<Decimal> = (0..10)
328            .map(|_| random_decimal(&mut t_rng).unwrap())
329            .collect();
330
331        // Check we have 10 values
332        assert_eq!(decimals.len(), 10);
333
334        // Check they're all different
335        for i in 0..9 {
336            assert_ne!(decimals[i], decimals[i + 1]);
337        }
338    }
339}
340
341#[cfg(test)]
342#[allow(clippy::unwrap_used)]
343mod tests_deterministic_rng {
344    use super::*;
345
346    #[test]
347    fn same_seed_yields_identical_stream() {
348        let mut a = deterministic_rng(12345);
349        let mut b = deterministic_rng(12345);
350        for _ in 0..16 {
351            assert_eq!(a.random::<u64>(), b.random::<u64>());
352        }
353    }
354
355    #[test]
356    fn different_seeds_diverge() {
357        let mut a = deterministic_rng(1);
358        let mut b = deterministic_rng(2);
359        // Not a strict guarantee for two draws, but vanishingly unlikely
360        // to collide on StdRng for the default seed-mixing routine.
361        let mut collisions = 0;
362        for _ in 0..32 {
363            if a.random::<u64>() == b.random::<u64>() {
364                collisions += 1;
365            }
366        }
367        assert!(
368            collisions <= 1,
369            "expected diverging streams, got {collisions} collisions"
370        );
371    }
372
373    #[test]
374    fn default_seed_is_stable_across_calls() {
375        let mut a = deterministic_rng(DETERMINISTIC_RNG_DEFAULT_SEED);
376        let mut b = deterministic_rng(DETERMINISTIC_RNG_DEFAULT_SEED);
377        assert_eq!(a.random::<u64>(), b.random::<u64>());
378    }
379}