shape_runtime/intrinsics/
stochastic.rs1use super::random;
19use crate::marshal::{
20 register_typed_fn_2, register_typed_fn_3, register_typed_fn_5, register_typed_fn_6,
21};
22use crate::module_exports::ModuleExports;
23use crate::typed_module_exports::{ConcreteReturn, ConcreteType, TypedReturn};
24use rand::Rng;
25
26pub fn create_stochastic_intrinsics_module() -> ModuleExports {
30 let mut module = ModuleExports::new("std::core::intrinsics::stochastic");
31 module.description = "Stochastic process intrinsics (Brownian motion, GBM, OU, random walk)"
32 .to_string();
33
34 register_typed_fn_3::<_, i64, f64, f64>(
35 &mut module,
36 "__intrinsic_brownian_motion",
37 "Brownian motion path of length n with timestep dt and volatility sigma",
38 [("n", "int"), ("dt", "number"), ("sigma", "number")],
39 ConcreteType::ArrayNumber,
40 |n, dt, sigma, _ctx| {
41 if n < 0 {
42 return Err("__intrinsic_brownian_motion: n must be non-negative".to_string());
43 }
44 if dt <= 0.0 {
45 return Err("__intrinsic_brownian_motion: dt must be positive".to_string());
46 }
47 if sigma < 0.0 {
48 return Err(
49 "__intrinsic_brownian_motion: sigma must be non-negative".to_string()
50 );
51 }
52 let n = n as usize;
53 let scale = sigma * dt.sqrt();
54 let path = random::with_rng(|rng| {
55 let mut path = Vec::with_capacity(n);
56 let mut x = 0.0;
57 for i in 0..n {
58 if i > 0 {
59 x += scale * standard_normal(rng);
60 }
61 path.push(x);
62 }
63 path
64 });
65 Ok(TypedReturn::Concrete(ConcreteReturn::ArrayF64(path)))
66 },
67 );
68
69 register_typed_fn_5::<_, i64, f64, f64, f64, f64>(
70 &mut module,
71 "__intrinsic_gbm",
72 "Geometric Brownian Motion path: s0 * exp((mu - 0.5*sigma^2)*dt + sigma*sqrt(dt)*Z)",
73 [
74 ("n", "int"),
75 ("dt", "number"),
76 ("mu", "number"),
77 ("sigma", "number"),
78 ("s0", "number"),
79 ],
80 ConcreteType::ArrayNumber,
81 |n, dt, mu, sigma, s0, _ctx| {
82 if n < 0 {
83 return Err("__intrinsic_gbm: n must be non-negative".to_string());
84 }
85 if dt <= 0.0 {
86 return Err("__intrinsic_gbm: dt must be positive".to_string());
87 }
88 if sigma < 0.0 {
89 return Err("__intrinsic_gbm: sigma must be non-negative".to_string());
90 }
91 if s0 <= 0.0 {
92 return Err("__intrinsic_gbm: s0 must be positive".to_string());
93 }
94 let n = n as usize;
95 let drift = (mu - 0.5 * sigma * sigma) * dt;
96 let diffusion_scale = sigma * dt.sqrt();
97 let path = random::with_rng(|rng| {
98 let mut path = Vec::with_capacity(n);
99 let mut s = s0;
100 for i in 0..n {
101 if i > 0 {
102 let z = standard_normal(rng);
103 s *= (drift + diffusion_scale * z).exp();
104 }
105 path.push(s);
106 }
107 path
108 });
109 Ok(TypedReturn::Concrete(ConcreteReturn::ArrayF64(path)))
110 },
111 );
112
113 register_typed_fn_6::<_, i64, f64, f64, f64, f64, f64>(
114 &mut module,
115 "__intrinsic_ou_process",
116 "Ornstein-Uhlenbeck mean-reverting process: dx = theta*(mu - x)*dt + sigma*sqrt(dt)*Z",
117 [
118 ("n", "int"),
119 ("dt", "number"),
120 ("theta", "number"),
121 ("mu", "number"),
122 ("sigma", "number"),
123 ("x0", "number"),
124 ],
125 ConcreteType::ArrayNumber,
126 |n, dt, theta, mu, sigma, x0, _ctx| {
127 if n < 0 {
128 return Err("__intrinsic_ou_process: n must be non-negative".to_string());
129 }
130 if dt <= 0.0 {
131 return Err("__intrinsic_ou_process: dt must be positive".to_string());
132 }
133 if theta < 0.0 {
134 return Err("__intrinsic_ou_process: theta must be non-negative".to_string());
135 }
136 if sigma < 0.0 {
137 return Err("__intrinsic_ou_process: sigma must be non-negative".to_string());
138 }
139 let n = n as usize;
140 let diffusion_scale = sigma * dt.sqrt();
141 let path = random::with_rng(|rng| {
142 let mut path = Vec::with_capacity(n);
143 let mut x = x0;
144 for i in 0..n {
145 if i > 0 {
146 let z = standard_normal(rng);
147 x += theta * (mu - x) * dt + diffusion_scale * z;
148 }
149 path.push(x);
150 }
151 path
152 });
153 Ok(TypedReturn::Concrete(ConcreteReturn::ArrayF64(path)))
154 },
155 );
156
157 register_typed_fn_2::<_, i64, f64>(
158 &mut module,
159 "__intrinsic_random_walk",
160 "Discrete +/- step_size random walk of length n",
161 [("n", "int"), ("step_size", "number")],
162 ConcreteType::ArrayNumber,
163 |n, step_size, _ctx| {
164 if n < 0 {
165 return Err("__intrinsic_random_walk: n must be non-negative".to_string());
166 }
167 if step_size <= 0.0 {
168 return Err("__intrinsic_random_walk: step_size must be positive".to_string());
169 }
170 let n = n as usize;
171 let path = random::with_rng(|rng| {
172 let mut path = Vec::with_capacity(n);
173 let mut x = 0.0;
174 for i in 0..n {
175 if i > 0 {
176 let step = if rng.r#gen::<f64>() < 0.5 {
177 -step_size
178 } else {
179 step_size
180 };
181 x += step;
182 }
183 path.push(x);
184 }
185 path
186 });
187 Ok(TypedReturn::Concrete(ConcreteReturn::ArrayF64(path)))
188 },
189 );
190
191 module
192}
193
194#[inline]
198fn standard_normal(rng: &mut rand_chacha::ChaCha8Rng) -> f64 {
199 let u1: f64 = rng.r#gen();
200 let u2: f64 = rng.r#gen();
201 (-2.0_f64 * u1.ln()).sqrt() * (2.0_f64 * std::f64::consts::PI * u2).cos()
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use crate::intrinsics::random as random_intrinsics;
208 use rand::SeedableRng;
209
210 fn mean_variance(samples: &[f64]) -> (f64, f64) {
211 let mean = samples.iter().sum::<f64>() / samples.len() as f64;
212 let var = samples.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / samples.len() as f64;
213 (mean, var)
214 }
215
216 #[test]
217 fn test_brownian_motion_increments_unit_variance() {
218 random_intrinsics::with_rng(|rng| {
219 *rng = rand_chacha::ChaCha8Rng::seed_from_u64(999);
220 });
221
222 let path = random_intrinsics::with_rng(|rng| {
223 let n = 10001usize;
224 let scale = 1.0_f64 * 1.0_f64.sqrt();
225 let mut path = Vec::with_capacity(n);
226 let mut x = 0.0;
227 for i in 0..n {
228 if i > 0 {
229 x += scale * standard_normal(rng);
230 }
231 path.push(x);
232 }
233 path
234 });
235
236 let increments: Vec<f64> = (1..path.len()).map(|i| path[i] - path[i - 1]).collect();
237 let (mean, var) = mean_variance(&increments);
238 assert!(mean.abs() < 0.05);
239 assert!((var - 1.0).abs() < 0.1);
240 }
241
242 #[test]
243 fn test_gbm_positive_path() {
244 random_intrinsics::with_rng(|rng| {
245 *rng = rand_chacha::ChaCha8Rng::seed_from_u64(42);
246 });
247
248 let n = 100usize;
249 let dt: f64 = 1.0 / 252.0;
250 let mu = 0.1;
251 let sigma = 0.2;
252 let s0 = 100.0;
253 let drift = (mu - 0.5 * sigma * sigma) * dt;
254 let diffusion_scale = sigma * dt.sqrt();
255 let path = random_intrinsics::with_rng(|rng| {
256 let mut path = Vec::with_capacity(n);
257 let mut s = s0;
258 for i in 0..n {
259 if i > 0 {
260 let z = standard_normal(rng);
261 s *= (drift + diffusion_scale * z).exp();
262 }
263 path.push(s);
264 }
265 path
266 });
267 for &v in &path {
268 assert!(v > 0.0);
269 }
270 }
271
272 #[test]
273 fn test_random_walk_step_size_invariant() {
274 random_intrinsics::with_rng(|rng| {
275 *rng = rand_chacha::ChaCha8Rng::seed_from_u64(11);
276 });
277
278 let n = 101usize;
279 let step = 2.0;
280 let path: Vec<f64> = random_intrinsics::with_rng(|rng| {
281 let mut path = Vec::with_capacity(n);
282 let mut x = 0.0;
283 for i in 0..n {
284 if i > 0 {
285 let s = if rng.r#gen::<f64>() < 0.5 {
286 -step
287 } else {
288 step
289 };
290 x += s;
291 }
292 path.push(x);
293 }
294 path
295 });
296
297 for i in 1..path.len() {
298 let diff = (path[i] - path[i - 1]).abs();
299 assert!((diff - step).abs() < 1e-9);
300 }
301 }
302}