1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
use ndarray::Array1;
use crate::error::{DigiFiError, ErrorTitle};
use crate::statistics::{discrete_distributions::PoissonDistribution};
use crate::random_generators::{
RandomGenerator, generator_algorithms::inverse_transform, uniform_generators::FibonacciGenerator, standard_normal_generators::StandardNormalBoxMuller,
};
pub trait CustomSDEComponent {
/// Custom SDE component function.
///
/// # Input
/// - `n_paths`: Number of simulation paths
/// - `stochastic_values`: Stochastic process values
/// - `t`: Current time step
/// - `dt`: Time step increment
fn comp_func(&self, n_paths: usize, stochastic_values: &Array1<f64>, t: f64, dt: f64) -> Result<Array1<f64>, DigiFiError>;
/// Error title associated with this custom SDE term.
///
/// Note: This title will be displayed in the case of validation error.
fn error_title(&self) -> String;
/// Validates a custom SDE component function ensuring it meets computational requirements.
///
/// # Input
/// - `n_paths`: Number of simulation paths
///
/// # Errors
/// - Returns an error if the custom function does not return an array of the same length as there are number of paths.
fn validate(&self, n_paths: usize) -> Result<(), DigiFiError> {
let sample: Array1<f64> = FibonacciGenerator::new_shuffle(2)?.generate()?;
if self.comp_func(n_paths, &Array1::from_vec(vec![1.0; n_paths]), sample[0], sample[1])?.len() != n_paths {
return Err(DigiFiError::CustomFunctionLengthVal { title: self.error_title() });
}
Ok(())
}
}
/// An SDE component that is used in either drift or diffusion terms of the SDE.
pub enum SDEComponent {
/// Generates a constant array representing a linear SDE component.
///
/// # LaTeX Formula
/// - f(t) = a \\quad a\\in\\mathbb(R)
Linear {
/// Constant linear coefficient (i.e., the gradient of the linear function)
a: f64,
},
/// Generates an array representing a quadratic time-dependent SDE component.
///
/// # LaTeX Formula
/// - f(t) = 2at + b
QuadraticTime {
/// Coefficient for the time-dependent term in quadratic gradient
a: f64,
/// Constant term in quadratic gradient
b: f64,
},
/// Generates an array representing a power-law stochastic SDE component.
///
/// # LaTeX Formula
/// - f(X_{t}) = aX^{power}_{t}
PowerStochastic {
/// Scaling coefficient
a: f64,
/// Power-law exponent
power: f64,
},
/// Generate a term that converges to a specific final value (e.g., Brownian Bridge Drift -\\frac{b - a}{T - t}).
///
/// # LaTeX Formula
/// - f(t) = \\frac{b - a}{T - t}
ConvergenceToValue {
/// Final time step
final_time: f64,
/// Initial process value
a: f64,
/// Final process value
b: f64,
},
/// Generates a term that regresses to a trend value (e.g., Ornstein-Uhlenbeck Process Drift - \\alpha(\\mu - S_{t})).
///
/// # LaTeX Formula
/// - f(X_{t}) = \\textit{scale} * (\\textit{trend} - X_{t})
RegressionToTrend {
/// Scaling coefficient
scale: f64,
/// Average trend value
trend: f64,
},
Custom { f: Box<dyn CustomSDEComponent> },
}
impl SDEComponent {
/// Validates the parameters of the SDE component type.
///
/// # Input
/// - `n_paths`: Number of simulation paths
///
/// # Errors
/// - Returns an error if in convergence-to-value component the argument `b` is smaller or equal to the argument `a`.
/// - Returns an error if in convergence-to-value component the argument `final_time` is non-positive.
/// - Returns an error if the custom function does not return an array of the same length as there are number of paths.
pub fn validate(&self, n_paths: usize) -> Result<(), DigiFiError> {
match &self {
Self::ConvergenceToValue { final_time, a, b } => {
if b <= a {
return Err(DigiFiError::ParameterConstraint {
title: Self::error_title(),
constraint: "The argument `b` must be larger that the argument `a`.".to_owned(),
});
}
if *final_time <= 0.0 {
return Err(DigiFiError::ParameterConstraint {
title: Self::error_title(),
constraint: "The argument `final_time` must be positive.".to_owned(),
});
}
},
Self::Custom { f } => { f.validate(n_paths)?; },
_ => (),
}
Ok(())
}
/// Computes SDE component values for a given component type using the specified parameters and stochastic values.
///
/// # Input
/// - `n_paths`: Number of simulation paths
/// - `stochastic_values`: Stochastic process values
/// - `t`: Current time step
/// - `dt`: Time step increment
///
/// # Output
/// - Array of computed SDE component values
///
/// # Errors
/// - Returns an error if the argument `stochastic_values` has length that is not same as the value of `n_paths`.
pub fn get_component_values(&self, n_paths: usize, stochastic_values: &Array1<f64>, t: f64, dt: f64) -> Result<Array1<f64>, DigiFiError> {
if stochastic_values.len() != n_paths {
return Err(DigiFiError::WrongLength { title: Self::error_title(), arg: "stochastic_values".to_owned(), len: n_paths, });
}
match &self {
Self::Linear { a } => {
Ok(Array1::from_vec(vec![*a; n_paths]))
},
Self::QuadraticTime { a, b } => {
Ok(Array1::from_vec(vec![2.0 * a + b; n_paths]))
},
Self::PowerStochastic { a, power } => {
Ok(*a * stochastic_values.map(|v| v.powf(*power) ))
},
Self::ConvergenceToValue { final_time, a, b } => {
Ok(Array1::from_vec(vec![(b - a) / (final_time - t); n_paths]))
},
Self::RegressionToTrend { scale, trend } => {
Ok(*scale * (*trend - stochastic_values))
},
Self::Custom { f } => {
f.comp_func(n_paths, stochastic_values, t, dt)
},
}
}
}
impl ErrorTitle for SDEComponent {
fn error_title() -> String {
String::from("SDE Component")
}
}
pub trait CustomNoise {
/// Custom noise function.
///
/// # Input
/// - `n_paths`: Number of simulation paths
/// - `dt`: Time step increment
fn noise_func(&self, n_paths: usize, dt: f64) -> Result<Array1<f64>, DigiFiError>;
/// Error title associated with this custom noise term.
///
/// Note: This title will be displayed in the case of validation error.
fn error_title(&self) -> String;
/// Validates a custom noise function ensuring it meets computational requirements.
///
/// # Errors
/// - Returns an error if the custom function does not return an array of the same length as there are number of paths.
fn validate(&self, n_paths: usize) -> Result<(), DigiFiError> {
let dt: f64 = FibonacciGenerator::new_shuffle(1)?.generate()?[0];
if self.noise_func(n_paths, dt)?.len() != n_paths {
return Err(DigiFiError::CustomFunctionLengthVal { title: self.error_title() });
}
Ok(())
}
}
/// Type of noise function to use in the diffusion term of the stochastic process.
pub enum Noise {
/// Generates standard Gaussian white noise with mean 0 and variance 1.
///
/// # LaTeX Formula
/// - \\mathcal{N}(0, 1)
///
/// # Links
/// - Wikipedia: <https://en.wikipedia.org/wiki/White_noise>
/// - Original Source: N/A
StandardWhiteNoise,
/// Generates increments of the Wiener process, suitable for simulating Brownian motion.
///
/// # Links
/// - Wikipedia: <https://en.wikipedia.org/wiki/Wiener_process>
/// - Original Source: N/A
WeinerProcess,
Custom { f: Box<dyn CustomNoise> },
}
impl Noise {
/// Validates the parameters of the noise type.
///
/// # Input
/// - `n_paths`: Number of simulation paths
///
/// # Errors
/// - Returns an error if the custom function does not return an array of the same length as there are number of paths.
pub fn validate(&self, n_paths: usize) -> Result<(), DigiFiError> {
if let Self::Custom { f } = &self { f.validate(n_paths)?; }
Ok(())
}
/// Computes noise for the diffusion term.
///
/// # Input
/// - `n_paths`: Number of simulation paths
/// - `dt`: Time step increment
///
/// # Output
/// - An array of noises for each path at a time step.
pub fn get_noise(&self, n_paths: usize, dt: f64) -> Result<Array1<f64>, DigiFiError> {
match &self {
Self::StandardWhiteNoise => {
StandardNormalBoxMuller::new_shuffle(n_paths)?.generate()
},
Self::WeinerProcess => {
Ok(dt.sqrt() * StandardNormalBoxMuller::new_shuffle(n_paths)?.generate()?)
},
Self::Custom { f } => { f.noise_func(n_paths, dt) }
}
}
}
pub trait CustomJump {
/// Custom jump function.
///
/// # Input
/// - `dt`:Time step increment
fn jump_func(&self, n_paths: usize, dt: f64) -> Result<Array1<f64>, DigiFiError>;
/// Error title associated with this custom jump term.
///
/// Note: This title will be displayed in the case of validation error.
fn error_title(&self) -> String;
/// Validates a custom jump function ensuring it meets computational requirements.
///
/// # Errors
/// - Returns an error if the custom function does not return an array of the same length as there are number of paths.
fn validate(&self, n_paths: usize) -> Result<(), DigiFiError> {
let dt: f64 = FibonacciGenerator::new_shuffle(1)?.generate()?[0];
if self.jump_func(n_paths, dt)?.len() != n_paths {
return Err(DigiFiError::CustomFunctionLengthVal { title: self.error_title() });
}
Ok(())
}
}
/// Represents the jump term in a discrete-time SDE. The jump term accounts for sudden, discontinuous changes in the value of the process.
pub enum Jump {
NoJumps,
/// Generates jumps according to a compound Poisson process with normally distributed jump sizes.
///
/// # LaTeX Formula
/// - \\textit{Jumps} = \\textit{Pois}(\\lambda \\cdot dt) \\cdot (\\mu_{j} + \\sigma_{j} \\cdot \\sqrt{\\text{Jumps}} \\cdot \\mathcal{N}(0,1))
/// where \\lambda is the jump rate, \\mu_{j} is the mean jump size, and \\sigma_{j} is the jump size volatility.
CompoundPoissonNormal {
/// Jump rate (0 < lambda_j)
lambda_j: f64,
/// Average jump magnitude
mu_j: f64,
/// Standard deviation of jump magnitude
sigma_j: f64
},
/// Generates jumps according to a compound Poisson process with bilateral exponential jump sizes.
///
/// # LaTeX Formula
/// - \\textit{Jumps Frequency} = Pois(\\lambda dt)
/// - \\textit{Assymetric Double Exponential RV} = \\mathbb{1}_{p\\leq U(0,1)}*(-\\frac{1}{\\eta_{u}} * ln(\\frac{1-U(0,1)}{p})) + \\mathbb{1}_{U(0,1)<p}*(\\frac{1}{\\eta_{d}} * ln(\\frac{U(0,1)}{1-p}))
/// - \\textit{Jumps Distribution} = (e^{\\textit{Assymetric Double Exponential RV}} - 1) * \\textit{Jumps Frequency}
CompoundPoissonBilateral {
/// Jump rate (0 < lambda_j)
lambda_j: f64,
/// Probability of a jump up (0<=p<=1)
p: f64,
/// Scaling of jump down
eta_d: f64,
/// Scaling of jump up
eta_u: f64,
},
CustomJump {f: Box<dyn CustomJump>},
}
impl Jump {
/// Validates the parameters of the jump function type.
///
/// # Input
/// - `n_paths`: Number of simulation paths
///
/// # Errors
/// - Returns an error if the argument `lambda_j` of the compound Poisson normal jump is negative.
/// - Returns an error if the argument `lambda_j` of the compound Poisson bilateral jump be negative.
/// - Returns an error if the rgument `p` of the compound Poisson bilateral jump is not in the range \[0, 1\].
pub fn validate(&self, n_paths: usize) -> Result<(), DigiFiError> {
match &self {
Self::NoJumps => Ok(()),
Self::CompoundPoissonNormal { lambda_j, .. } => {
if lambda_j <= &0.0 {
return Err(DigiFiError::ParameterConstraint {
title: Self::error_title(),
constraint: "The argument `lambda_j` of the compound Poisson normal jump must be positive.".to_owned(),
});
}
Ok(())
},
Self::CompoundPoissonBilateral { lambda_j, p, .. } => {
if lambda_j <= &0.0 {
return Err(DigiFiError::ParameterConstraint {
title: Self::error_title(),
constraint: "The argument `lambda_j` of the compound Poisson bilateral jump must be positive.".to_owned(),
});
}
if (p < &0.0) || (&1.0 < p) {
return Err(DigiFiError::ParameterConstraint {
title: Self::error_title(),
constraint: "The argument `p` of the compound Poisson bilateral jump must be in the range `[0, 1]`.".to_owned(),
});
}
Ok(())
},
Self::CustomJump { f } => { f.validate(n_paths) },
}
}
/// Computes the jump term for each path in the stochastic process at a given time.
///
/// # Input
/// - `n_paths`: Number of simulation paths
/// - `dt`: Time step increment
pub fn get_jumps(&self, n_paths: usize, dt: f64) -> Result<Array1<f64>, DigiFiError> {
match &self {
Self::NoJumps => { Ok(Array1::from_vec(vec![0.0; n_paths])) },
Self::CompoundPoissonNormal { lambda_j, mu_j, sigma_j } => {
let pois_dist: PoissonDistribution = PoissonDistribution::build(lambda_j * dt)?;
let dp: Array1<f64> = inverse_transform(&pois_dist, n_paths)?;
Ok(*mu_j * &dp + *sigma_j * dp.map(|v| v.sqrt() ) * StandardNormalBoxMuller::new_shuffle(n_paths)?.generate()?)
},
Self::CompoundPoissonBilateral { lambda_j, p, eta_d, eta_u } => {
let pois_dist: PoissonDistribution = PoissonDistribution::build(lambda_j * dt)?;
let dp: Array1<f64> = inverse_transform(&pois_dist, n_paths)?;
// Assymetric double exponential distribution
let u: Array1<f64> = FibonacciGenerator::new_shuffle(n_paths)?.generate()?;
let mut y: Array1<f64> = Array1::from_vec(vec![0.0; n_paths]);
let neg_one_over_eta_u: f64 = -1.0 / eta_u;
let one_over_eta_d: f64 = 1.0 / eta_d;
let q: f64 = 1.0 - p;
for i in 0..n_paths {
if p <= &u[i] {
y[i] = ((neg_one_over_eta_u * ((1.0 - u[i]) / p).ln()).exp() - 1.0) * dp[i]
} else {
y[i] = ((one_over_eta_d * (u[i] / q).ln()).exp() - 1.0) * dp[i]
}
}
Ok(y)
},
Self::CustomJump { f } => { f.jump_func(n_paths, dt) },
}
}
}
impl ErrorTitle for Jump {
fn error_title() -> String {
String::from("Jump Type")
}
}