@@ -21,6 +21,13 @@ const KAPPA_MIN: f64 = 1e-3;
2121const THETA_MIN : f64 = 1e-8 ;
2222const SIGMA_MIN : f64 = 1e-8 ;
2323
24+ // Use periodic linear extension mapping into these ranges
25+ const P_KAPPA : ( f64 , f64 ) = ( 0.1 , 20.0 ) ;
26+ const P_THETA : ( f64 , f64 ) = ( 0.001 , 0.4 ) ;
27+ const P_SIGMA : ( f64 , f64 ) = ( 0.01 , 0.6 ) ;
28+ const P_RHO : ( f64 , f64 ) = ( -1.0 , 1.0 ) ;
29+ const P_V0 : ( f64 , f64 ) = ( 0.005 , 0.25 ) ;
30+
2431#[ derive( Clone , Debug ) ]
2532pub struct HestonParams {
2633 /// Initial variance v0 (not volatility) in Heston model
@@ -36,25 +43,52 @@ pub struct HestonParams {
3643}
3744
3845impl HestonParams {
39- /// Project parameters to satisfy Heston admissibility constraints:
40- /// v0 ≥ 0, kappa > 0, theta > 0, sigma ≥ 0, −1 < rho < 1, and Feller 2*kappa*theta ≥ sigma^2.
46+ fn periodic_map ( x : f64 , c : f64 , d : f64 ) -> f64 {
47+ if c <= x && x <= d {
48+ x
49+ } else {
50+ let range = d - c;
51+ if range <= 0.0 {
52+ return c;
53+ }
54+ let n = ( ( x - c) / range) . floor ( ) ;
55+ let n_int = n as i64 ;
56+ if n_int % 2 == 0 {
57+ x - n * range
58+ } else {
59+ d + n * range - ( x - c)
60+ }
61+ }
62+ }
63+
64+ /// Project parameters to satisfy Heston admissibility constraints and periodic-range mapping.
65+ /// Steps:
66+ /// 1) Periodic mapping into fixed parameter ranges
67+ /// 2) Enforce basic positivity/box constraints
68+ /// 3) Enforce Feller by lowering sigma when needed (otherwise minimally bump theta)
4169 pub fn project_in_place ( & mut self ) {
42- // Basic bounds
70+ self . kappa = Self :: periodic_map ( self . kappa , P_KAPPA . 0 , P_KAPPA . 1 ) ;
71+ self . theta = Self :: periodic_map ( self . theta , P_THETA . 0 , P_THETA . 1 ) ;
72+ self . sigma = Self :: periodic_map ( self . sigma , P_SIGMA . 0 , P_SIGMA . 1 ) . abs ( ) ;
73+ self . rho = Self :: periodic_map ( self . rho , P_RHO . 0 , P_RHO . 1 ) ;
74+ self . v0 = Self :: periodic_map ( self . v0 , P_V0 . 0 , P_V0 . 1 ) ;
75+
4376 self . v0 = self . v0 . max ( 0.0 ) ;
4477 self . kappa = self . kappa . max ( KAPPA_MIN ) ;
4578 self . theta = self . theta . max ( THETA_MIN ) ;
4679 self . sigma = self . sigma . abs ( ) . max ( SIGMA_MIN ) ;
4780 self . rho = self . rho . max ( -RHO_BOUND ) . min ( RHO_BOUND ) ;
4881
49- // Feller condition: 2*kappa*theta ≥ sigma^2.
82+ // 3) Feller condition: 2*kappa*theta ≥ sigma^2.
5083 if 2.0 * self . kappa * self . theta < self . sigma * self . sigma {
5184 let sigma_star = ( 2.0 * self . kappa * self . theta ) . sqrt ( ) ;
52- if sigma_star >= SIGMA_MIN {
53- // Prefer reducing sigma to satisfy Feller to avoid blowing up theta .
54- self . sigma = sigma_star;
85+ if sigma_star >= P_SIGMA . 0 {
86+ // Prefer reducing sigma, but keep within the range lower bound as well .
87+ self . sigma = sigma_star. min ( P_SIGMA . 1 ) ;
5588 } else {
56- // As a fallback (when sigma would go below minimum), bump theta minimally.
57- self . theta = ( ( self . sigma * self . sigma ) / ( 2.0 * self . kappa ) ) . max ( THETA_MIN ) + EPS ;
89+ // As a fallback (when sigma would go below minimum), bump theta minimally, respecting the range upper bound.
90+ let theta_star = ( ( self . sigma * self . sigma ) / ( 2.0 * self . kappa ) ) . max ( THETA_MIN ) + EPS ;
91+ self . theta = theta_star. min ( P_THETA . 1 ) ;
5892 }
5993 }
6094 }
0 commit comments