@@ -499,37 +499,99 @@ def cond_states_step(self, dt, vol_0, nz_theta=True):
499499 return vol_t , avgvar , avgvol
500500
501501
502- class OusvMcChoi2023KL (OusvMcABC ):
502+ class OusvMcChoi2025KL (OusvMcABC ):
503+ """
504+ Exact Monte Carlo simulation for the OUSV model using Karhunen–Loève (KL) expansions.
505+
506+ The stochastic volatility σ_t follows an Ornstein–Uhlenbeck (OU) process:
507+
508+ dS_t / S_t = r dt + σ_t (ρ dZ_t + √(1-ρ²) dW_t)
509+ dσ_t = κ(θ - σ_t) dt + ξ dZ_t
510+
511+ where κ (`mr`) is the mean-reversion speed, θ (`theta`) the long-term equilibrium
512+ volatility, ξ (`vov`) the volatility of volatility, and ρ (`rho`) the
513+ price–volatility correlation.
514+
515+ The volatility path is expressed as an infinite sine series via the KL expansion
516+ of the OU bridge process (Eq. (11) of the reference):
517+
518+ σ_t = θ + σ̄₀ e^{-κt} + σ̂_T sinh(κt)/sinh(κT)
519+ + ξ√T Σ_{n=1}^∞ a_n sin(nπt/T) Z_n,
520+
521+ where a_n = √(2 / ((κT)² + (nπ)²)) and Z_n are i.i.d. standard normals.
522+
523+ This representation allows the time integrals of volatility (U_{0,T}) and
524+ variance (V_{0,T}), which are the sufficient statistics for pricing via
525+ conditional simulation, to be computed analytically as finite sums of
526+ independent normal random variables. The first L = `n_sin` sine terms are
527+ sampled explicitly; the truncated tail is approximated by four normal
528+ random variables (G_L, P_L, Q_L, R_L) whose joint covariance is given
529+ analytically (Eqs. (13)–(17) of the reference).
530+
531+ The method is several hundred times faster than the numerical Fourier-inversion
532+ approach of Li and Wu (2019). Variance is further reduced by conditional
533+ Monte Carlo simulation and a martingale-preserving control variate.
534+
535+ Examples:
536+ >>> import numpy as np
537+ >>> import pyfeng as pf
538+ >>> m = pf.OusvMcChoi2025KL(sigma=0.2, vov=0.1, mr=4, rho=-0.7, theta=0.2)
539+ >>> m.set_num_params(n_path=100000, dt=None, rn_seed=42, n_sin=4)
540+ >>> m.price(np.arange(80, 121, 10), spot=100, texp=1)
541+
542+ References:
543+ - Choi J (2025) Exact simulation scheme for the Ornstein–Uhlenbeck driven
544+ stochastic volatility model with the Karhunen–Loève expansions.
545+ Operations Research Letters 60:107280.
546+ https://doi.org/10.1016/j.orl.2025.107280
547+ """
503548
504549 n_sin = 2
505550
506551 def set_num_params (self , n_path = 10000 , dt = None , rn_seed = None , antithetic = True , n_sin = 2 ):
507552 """
508- Set MC parameters
553+ Set Monte Carlo parameters.
509554
510555 Args:
511- n_path: number of paths
512- dt: time step for Euler/Milstein steps
513- rn_seed: random number seed
514- antithetic: antithetic
515- """
516- assert n_sin % 2 == 0
556+ n_path: number of simulation paths
557+ dt: time step size. If None (default), the entire period [0, T] is
558+ simulated in a single exact step (no discretization error).
559+ rn_seed: random number seed for reproducibility
560+ antithetic: if True (default), use antithetic variates for variance reduction
561+ n_sin: number of sine terms L in the KL expansion (must be a positive
562+ even integer). Higher values reduce the truncation error at the
563+ cost of slightly more computation. Default is 2.
564+ """
565+ if n_sin % 2 != 0 :
566+ raise ValueError (f"n_sin must be an even integer, got { n_sin } ." )
517567 self .n_sin = n_sin
518568
519569 super ().set_num_params (n_path , dt , rn_seed , antithetic )
520570
521571 @classmethod
522572 def _a2sum (cls , mr_t , ns = 0 , odd = None ):
523573 """
524- sum_{n=ns+1}^\infty a_n^2 where a_n = sqrt(2 / (mr_t^2 + (n*pi)^2))
574+ Tail sum Σ_{n=ns+1}^∞ a_n² where a_n = √(2 / ((κT)² + (nπ)²)).
575+
576+ Used when pre-specified normal variates `zn` are passed to `cond_states_step`.
577+ In that case the tail quadratic contribution Σ_{n>L} a_n² (Z_n² - 1) is
578+ replaced by its correction term −Σ_{n>L} a_n² = −`_a2sum(mr_t, ns=L)`,
579+ so that the combined result Σ_{n=1}^L a_n² Z_n² − Σ_{n=1}^∞ a_n²
580+ is computed correctly (see in-code comment "`-r_m` is the correction to
581+ include only `an2 @ z_sin**2`").
582+
583+ The closed-form for the full sum (ns=0) is:
584+ Σ a_n² = (κT / tanh(κT) - 1) / (κT)²
525585
526586 Args:
527- mr_t: mean reversion * time step
528- ns: number of truncated terms. Must be an even number
529- odd: sum all terms if None (default), odd terms only if odd=1, or even terms only if odd=2.
587+ mr_t: κT = mean-reversion speed × time step
588+ ns: number of leading terms already summed explicitly (must be even).
589+ Returns the tail sum from n=ns+1 onward.
590+ odd: None sums all terms; odd=1 sums odd-indexed terms only;
591+ odd=2 sums even-indexed terms only.
530592
531593 Returns:
532- sum
594+ tail sum value (scalar)
533595 """
534596 if odd == 2 : # even
535597 rv = cls ._a2sum (mr_t / 2 ) / 2 ** 2
@@ -555,15 +617,24 @@ def _a2sum(cls, mr_t, ns=0, odd=None):
555617 @classmethod
556618 def _a2overn2sum (cls , mr_t , ns = 0 , odd = None ):
557619 """
558- sum_{n=ns+1}^\infty a_n^2 / (n pi)^2 where a_n = sqrt(2 / (mr_t^2 + (n*pi)^2))
620+ Tail sum Σ_{n=ns+1}^∞ a_n² / (nπ)² where a_n = √(2 / ((κT)² + (nπ)²)).
621+
622+ This is f_L in Eq. (13) of the reference. It gives the variance of G_L,
623+ the truncated-tail contribution to Ũ_{0,T} from odd-indexed sine terms:
624+ Var(G_L) = f̃_L (odd-index tail of this sum).
625+
626+ The closed-form for the full sum (ns=0) is:
627+ Σ a_n²/(nπ)² = (1/3 - (κT/tanh(κT) - 1)/(κT)²) / (κT)²
559628
560629 Args:
561- mr_t: mean reversion * time step
562- ns: number of truncated terms. Must be an even number
563- odd: sum all terms if None (default), odd terms only if odd=1, or even terms only if odd=2.
630+ mr_t: κT = mean-reversion speed × time step
631+ ns: number of leading terms already summed explicitly (must be even).
632+ Returns the tail sum from n=ns+1 onward.
633+ odd: None sums all terms; odd=1 sums odd-indexed terms only (f̃_L);
634+ odd=2 sums even-indexed terms only.
564635
565636 Returns:
566- sum
637+ tail sum value (scalar)
567638 """
568639
569640 if odd == 2 : # even
@@ -590,15 +661,25 @@ def _a2overn2sum(cls, mr_t, ns=0, odd=None):
590661 @classmethod
591662 def _a4sum (cls , mr_t , ns = 0 , odd = None ):
592663 """
593- sum_{n=ns+1}^\infty a_n^4 where a_n = sqrt(2 / (mr_t^2 + (n*pi)^2))
664+ Tail sum Σ_{n=ns+1}^∞ a_n⁴ where a_n = √(2 / ((κT)² + (nπ)²)).
665+
666+ This is c_L in Eq. (13) of the reference. It determines:
667+ - Var(R_L) = 2 c_L (variance of the quadratic correction term)
668+ - Cov(G_L, P_L) = c̃_L (cross-covariance between tail G and P, odd terms)
669+ used in the sampling scheme of Eq. (16).
670+
671+ The closed-form for the full sum (ns=0) is:
672+ Σ a_n⁴ = (κT/tanh(κT) + (κT)²/sinh²(κT) - 2) / (κT)⁴
594673
595674 Args:
596- mr_t: mean reversion * time step
597- ns: number of truncated terms. Must be an even number
598- odd: sum all terms if None (default), odd terms only if odd=1, or even terms only if odd=2.
675+ mr_t: κT = mean-reversion speed × time step
676+ ns: number of leading terms already summed explicitly (must be even).
677+ Returns the tail sum from n=ns+1 onward.
678+ odd: None sums all terms; odd=1 sums odd-indexed terms only (c̃_L);
679+ odd=2 sums even-indexed terms only.
599680
600681 Returns:
601- sum
682+ tail sum value (scalar)
602683 """
603684
604685 if odd == 2 : # even
@@ -625,15 +706,24 @@ def _a4sum(cls, mr_t, ns=0, odd=None):
625706 @classmethod
626707 def _a6sum (cls , mr_t , ns = 0 , odd = None ):
627708 """
628- sum_{n=ns+1}^\infty a_n^6 where a_n = sqrt(2 / (mr_t^2 + (n*pi)^2))
709+ Tail sum Σ_{n=ns+1}^∞ a_n⁶ where a_n = √(2 / ((κT)² + (nπ)²)).
710+
711+ Auxiliary sum used internally to compute `_a6n2sum` via the identity
712+ Σ (nπ)² a_n⁶ = 2 Σ a_n⁴ - (κT)² Σ a_n⁶.
713+
714+ The closed-form for the full sum (ns=0) is:
715+ Σ a_n⁶ = (3κT/tanh(κT) + (3 + 2κT/tanh(κT))(κT)²/sinh²(κT) - 8)
716+ / (2(κT)⁶)
629717
630718 Args:
631- mr_t: mean reversion * time step
632- ns: number of truncated terms. Must be an even number
633- odd: sum all terms if None (default), odd terms only if odd=1, or even terms only if odd=2.
719+ mr_t: κT = mean-reversion speed × time step
720+ ns: number of leading terms already summed explicitly (must be even).
721+ Returns the tail sum from n=ns+1 onward.
722+ odd: None sums all terms; odd=1 sums odd-indexed terms only;
723+ odd=2 sums even-indexed terms only.
634724
635725 Returns:
636- sum
726+ tail sum value (scalar)
637727 """
638728
639729 if odd == 2 : # even
@@ -661,15 +751,24 @@ def _a6sum(cls, mr_t, ns=0, odd=None):
661751 @classmethod
662752 def _a6n2sum (cls , mr_t , ns = 0 , odd = None ):
663753 """
664- sum_{n=ns+1}^\infty (n pi)^2 a_n^6 where a_n = sqrt(2 / (mr_t^2 + (n*pi)^2))
754+ Tail sum Σ_{n=ns+1}^∞ (nπ)² a_n⁶ where a_n = √(2 / ((κT)² + (nπ)²)).
755+
756+ This is g_L in Eq. (13) of the reference. It determines the variances of
757+ the truncated-tail contributions P_L (odd) and Q_L (even) to Ṽ_{0,T}:
758+ Var(P_L) = g̃_L (odd-index tail), Var(Q_L) = ğ_L (even-index tail).
759+
760+ Computed via the identity (see Appendix B of the reference):
761+ Σ (nπ)² a_n⁶ = 2 Σ a_n⁴ - (κT)² Σ a_n⁶.
665762
666763 Args:
667- mr_t: mean reversion * time step
668- ns: number of truncated terms. Must be an even number
669- odd: sum all terms if None (default), odd terms only if odd=1, or even terms only if odd=2.
764+ mr_t: κT = mean-reversion speed × time step
765+ ns: number of leading terms already summed explicitly (must be even).
766+ Returns the tail sum from n=ns+1 onward.
767+ odd: None sums all terms; odd=1 sums odd-indexed terms only (g̃_L);
768+ odd=2 sums even-indexed terms only (ğ_L).
670769
671770 Returns:
672- sum
771+ tail sum value (scalar)
673772 """
674773
675774 if odd == 2 : # even
@@ -695,16 +794,40 @@ def _a6n2sum(cls, mr_t, ns=0, odd=None):
695794
696795 def cond_states_step (self , dt , vol_0 , nz_theta = True , zn = None ):
697796 """
698- Final volatility (sigma), average variance and volatilityu over dt given vol_0
797+ Exact simulation of (σ_T, V_{0,T}, U_{0,T}) over one time step dt.
798+
799+ Samples the triplet of sufficient statistics for the OUSV model using the
800+ KL expansion with L = `n_sin` explicit sine terms. The truncated tail
801+ (n > L) is approximated by four normal random variables G_L, P_L, Q_L, R_L
802+ whose joint distribution is given analytically in Eqs. (15)–(17) of the
803+ reference:
804+
805+ - G_L ~ N(0, f̃_L) (odd-term tail of Ũ_{0,T}, correlated with P_L)
806+ - P_L ~ N(0, g̃_L) (odd-term tail of Ṽ_{0,T})
807+ - Q_L ~ N(0, ğ_L) (even-term tail of Ṽ_{0,T}, independent of G_L, P_L)
808+ - R_L ≈ √(c_L) (W₄² - 1) (quadratic correction, Eq. (17))
809+
810+ Here f̃_L, g̃_L, ğ_L, and c_L are the tail sums computed by `_a2overn2sum`,
811+ `_a6n2sum` (odd/even), and `_a4sum`, respectively (see Eq. (13)).
699812
700813 Args:
701- dt: time-to-expiry
702- vol_0: initial volatility
703- zn: normal RVs to specify in the (1+n_sin, n_path) format. None by default.
704- nz_theta: non-zero theta. True by default. If False, assume theta=0 making computation simpler.
814+ dt: time step size T
815+ vol_0: initial volatility σ_0 (scalar or array of shape (n_path,))
816+ nz_theta: if True (default), include the long-term mean θ (non-zero theta).
817+ If False, assumes θ = 0 for a simpler computation.
818+ zn: pre-specified normal random variables of shape (1 + n_sin, n_path).
819+ Row 0 is used for σ_T; rows 1: are the Z_n sine coefficients.
820+ If None (default), random variates are drawn internally.
705821
706822 Returns:
707- (final vol, average var, average vol)
823+ tuple (vol_t, vv_t, uu_t):
824+ - vol_t: terminal volatility σ_T, shape (n_path,)
825+ - vv_t: average variance V_{0,T} = (1/T) ∫₀ᵀ σ_t² dt, shape (n_path,)
826+ - uu_t: average volatility U_{0,T} = (1/T) ∫₀ᵀ σ_t dt, shape (n_path,)
827+
828+ References:
829+ - Choi J (2025) Eqs. (12), (15)–(18). Operations Research Letters 60:107280.
830+ https://doi.org/10.1016/j.orl.2025.107280
708831 """
709832
710833 mr_t = self .mr * dt
@@ -781,6 +904,21 @@ def cond_states_step(self, dt, vol_0, nz_theta=True, zn=None):
781904 return vol_t , vv_t , uu_t
782905
783906 def unexplained_var_ratio (self , mr_t , ns = None ):
907+ """
908+ Fraction of variance in Ṽ_{0,T} not explained by the first `ns` sine terms.
909+
910+ Computed as c̃_L / c_0 = Σ_{n=ns+1}^∞ a_n⁴ / Σ_{n=1}^∞ a_n⁴, where
911+ c_L = `_a4sum(mr_t, ns=ns)` is the tail of the a_n⁴ series (Eq. (13)).
912+ A value close to 0 indicates that `ns` sine terms capture most of the
913+ variance, so the truncation error is small.
914+
915+ Args:
916+ mr_t: κT = mean-reversion speed × time step
917+ ns: number of explicit sine terms L. If None, uses `self.n_sin`.
918+
919+ Returns:
920+ unexplained variance ratio in [0, 1]
921+ """
784922
785923 if ns is None :
786924 ns = self .n_sin
@@ -795,13 +933,31 @@ def strike_var_swap_analytic(self, texp, dt=None):
795933
796934 def vol_path_sin (self , tobs , zn = None ):
797935 """
798- vol path composed of sin terms
936+ Simulate the full volatility path σ_t using the KL sine-series expansion.
937+
938+ Constructs the path according to Eq. (11) of the reference:
939+
940+ σ_t = θ + σ̄₀ e^{-κt} + σ̂_T sinh(κt)/sinh(κT)
941+ + ξ√T Σ_{n=1}^L a_n sin(nπt/T) Z_n
942+
943+ where σ̄₀ = σ_0 - θ, σ̂_T is the centered terminal value, and
944+ a_n = √(2/((κT)² + (nπ)²)). Only the L = `n_sin` explicit sine terms
945+ are included (no tail correction).
946+
799947 Args:
800- tobs: observation time (n_time, )
801- zn: specified normal rvs to use (n_sin + 1, n_path). None by default
948+ tobs: observation times in [0, T], shape (n_time,). The last element
949+ is taken as T.
950+ zn: pre-specified normal random variables of shape (n_sin + 1, n_path).
951+ Row 0 is used for the terminal value σ_T (via `vol_step`);
952+ rows 1: are the Z_n sine coefficients. If None (default),
953+ variates are drawn internally using `rng_spawn[2]`.
802954
803955 Returns:
804- vol path (n_time, n_path)
956+ volatility path of shape (n_time, n_path)
957+
958+ References:
959+ - Choi J (2025) Eq. (11). Operations Research Letters 60:107280.
960+ https://doi.org/10.1016/j.orl.2025.107280
805961 """
806962 dt = tobs [- 1 ]
807963 mr_t = self .mr * dt
0 commit comments