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}