1use std::marker::PhantomData;
2use nalgebra::SVector;
3use crate::state::{Diffusion, NoiseIncrement, State};
4
5pub trait Drift<S: State>: Fn(&S, f64) -> S + Send + Sync {}
6impl<S: State, F: Fn(&S, f64) -> S + Send + Sync> Drift<S> for F {}
7
8pub struct SDE<S: State + NoiseIncrement, D: Drift<S>, G: Diffusion<S, S>> {
9 pub drift: D,
10 pub diffusion: G,
11 _s: PhantomData<S>,
12}
13
14impl<S: State + NoiseIncrement, D: Drift<S>, G: Diffusion<S, S>> SDE<S, D, G> {
15 pub fn new(drift: D, diffusion: G) -> Self {
16 Self {
17 drift,
18 diffusion,
19 _s: PhantomData,
20 }
21 }
22
23 pub fn eval_drift(&self, x: &S, t: f64) -> S {
24 (self.drift)(x, t)
25 }
26}
27
28pub fn bm() -> SDE<f64, impl Drift<f64>, impl Diffusion<f64, f64>> {
51 SDE::new(|_x: &f64, _t: f64| 0.0_f64, |_x: f64, _t: f64| 1.0_f64)
52}
53
54pub fn gbm(mu: f64, sigma: f64) -> SDE<f64, impl Drift<f64>, impl Diffusion<f64, f64>> {
71 SDE::new(
72 move |x: &f64, _t: f64| mu * x,
73 move |x: f64, _t: f64| sigma * x,
74 )
75}
76
77pub fn ou(theta: f64, mu: f64, sigma: f64) -> SDE<f64, impl Drift<f64>, impl Diffusion<f64, f64>> {
91 SDE::new(
92 move |x: &f64, _t: f64| theta * (mu - x),
93 move |_x: f64, _t: f64| sigma,
94 )
95}
96
97pub fn cir(
103 kappa: f64,
104 theta: f64,
105 sigma: f64,
106) -> Result<SDE<f64, impl Drift<f64>, impl Fn(f64, f64) -> f64 + Send + Sync>, crate::error::PathwiseError> {
107 if kappa <= 0.0 || theta <= 0.0 || sigma <= 0.0 {
108 return Err(crate::error::PathwiseError::InvalidParameters(
109 format!("CIR requires kappa, theta, sigma > 0; got kappa={}, theta={}, sigma={}", kappa, theta, sigma)
110 ));
111 }
112 if 2.0 * kappa * theta <= sigma * sigma {
113 return Err(crate::error::PathwiseError::FellerViolation(
114 format!("2*kappa*theta = {:.4} <= sigma^2 = {:.4}; boundary is reflecting in continuous time but clipping may introduce bias under discretization",
115 2.0 * kappa * theta, sigma * sigma)
116 ));
117 }
118 Ok(SDE::new(
119 move |x: &f64, _t: f64| kappa * (theta - x),
120 move |x: f64, _t: f64| sigma * x.max(0.0).sqrt(),
121 ))
122}
123
124pub struct NdSDE<const N: usize, D, G> {
126 pub drift: D,
127 pub diffusion: G,
128}
129
130impl<const N: usize, D, G> NdSDE<N, D, G>
131where
132 D: Fn(&SVector<f64, N>, f64) -> SVector<f64, N> + Send + Sync,
133 G: Diffusion<SVector<f64, N>, SVector<f64, N>>,
134{
135 pub fn new(drift: D, diffusion: G) -> Self {
136 Self { drift, diffusion }
137 }
138}
139
140pub struct HestonDiffusion {
149 xi: f64,
150 rho: f64,
151 rho_perp: f64, }
153
154impl HestonDiffusion {
155 pub fn new(xi: f64, rho: f64) -> Self {
156 Self { xi, rho, rho_perp: (1.0 - rho * rho).sqrt() }
157 }
158}
159
160impl Diffusion<SVector<f64, 2>, SVector<f64, 2>> for HestonDiffusion {
161 fn apply(&self, x: &SVector<f64, 2>, _t: f64, dw: &SVector<f64, 2>) -> SVector<f64, 2> {
162 let v = x[1].max(0.0); let sv = v.sqrt();
164 SVector::from([
165 sv * dw[0],
166 sv * self.xi * (self.rho * dw[0] + self.rho_perp * dw[1]),
167 ])
168 }
169}
170
171pub fn heston(
184 mu: f64,
185 kappa: f64,
186 theta: f64,
187 xi: f64,
188 rho: f64,
189) -> NdSDE<2, impl Fn(&SVector<f64, 2>, f64) -> SVector<f64, 2> + Send + Sync, HestonDiffusion> {
190 NdSDE::new(
191 move |x: &SVector<f64, 2>, _t: f64| -> SVector<f64, 2> {
192 let v = x[1].max(0.0);
193 SVector::from([mu - v / 2.0, kappa * (theta - x[1])])
194 },
195 HestonDiffusion::new(xi, rho),
196 )
197}
198
199pub struct CorrOuDiffusion<const N: usize> {
202 l: nalgebra::SMatrix<f64, N, N>,
203}
204
205impl<const N: usize> crate::state::Diffusion<nalgebra::SVector<f64, N>, nalgebra::SVector<f64, N>> for CorrOuDiffusion<N> {
206 fn apply(&self, _x: &nalgebra::SVector<f64, N>, _t: f64, dw: &nalgebra::SVector<f64, N>) -> nalgebra::SVector<f64, N> {
207 self.l * dw
208 }
209}
210
211pub fn corr_ou<const N: usize>(
216 theta: f64,
217 mu: nalgebra::SVector<f64, N>,
218 sigma_mat: nalgebra::SMatrix<f64, N, N>,
219) -> Result<
220 NdSDE<N, impl Fn(&nalgebra::SVector<f64, N>, f64) -> nalgebra::SVector<f64, N> + Send + Sync, CorrOuDiffusion<N>>,
221 crate::error::PathwiseError,
222> {
223 let chol = nalgebra::Cholesky::new(sigma_mat).ok_or_else(|| {
224 crate::error::PathwiseError::DimensionMismatch(
225 "sigma_mat is not positive-definite (Cholesky failed)".into()
226 )
227 })?;
228 let l = chol.l();
229 Ok(NdSDE::new(
230 move |x: &nalgebra::SVector<f64, N>, _t: f64| -> nalgebra::SVector<f64, N> {
231 (mu - x) * theta
232 },
233 CorrOuDiffusion { l },
234 ))
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240 use crate::state::Increment;
241
242 #[test]
243 fn sde_evaluates_drift_and_diffusion() {
244 let sde = SDE::new(|x: &f64, _t: f64| -0.5 * x, |_x: f64, _t: f64| 1.0_f64);
245 assert_eq!(sde.eval_drift(&2.0, 0.0), -1.0);
246 assert_eq!(sde.diffusion.apply(&2.0, 0.0, &1.0_f64), 1.0);
248 }
249
250 #[test]
251 fn sde_closures_capture_parameters() {
252 let theta = 0.7_f64;
253 let sde = SDE::new(
254 move |x: &f64, _t: f64| -theta * x,
255 |_x: f64, _t: f64| 1.0_f64,
256 );
257 assert!((sde.eval_drift(&1.0, 0.0) - (-0.7)).abs() < 1e-12);
258 }
259
260 #[test]
261 fn bm_has_zero_drift_unit_diffusion() {
262 let b = bm();
263 assert_eq!(b.eval_drift(&1.5, 0.5), 0.0);
264 assert_eq!(b.diffusion.apply(&1.5, 0.5, &1.0_f64), 1.0);
266 }
267
268 #[test]
269 fn gbm_drift_and_diffusion() {
270 let g = gbm(0.05, 0.2);
271 assert!((g.eval_drift(&2.0, 0.0) - 0.1).abs() < 1e-12);
272 assert!((g.diffusion.apply(&2.0, 0.0, &1.0_f64) - 0.4).abs() < 1e-12);
274 }
275
276 #[test]
277 fn ou_drift_and_diffusion() {
278 let o = ou(1.0, 0.0, 0.5);
279 assert!((o.eval_drift(&1.0, 0.0) - (-1.0)).abs() < 1e-12);
280 assert!((o.diffusion.apply(&1.0, 0.0, &1.0_f64) - 0.5).abs() < 1e-12);
281 }
282
283 #[test]
284 fn increment_roundtrip() {
285 let inc = Increment { dw: 0.3_f64, dz: 0.0_f64 };
286 assert!((inc.dw - 0.3).abs() < 1e-12);
287 }
288}