55
66import numpy as np
77from pydantic import Field
8- from scipy .optimize import Bounds
98from scipy .stats import gamma , norm
109
1110from ..ta .paths import Paths
1211from ..utils .distributions import Exponential
1312from ..utils .types import Float , FloatArrayLike , Vector
14- from .base import Im , IntensityProcess
13+ from .base import IntensityProcess , StochasticProcess1D
1514from .poisson import CompoundPoissonProcess , D
1615from .weiner import WeinerProcess
1716
1817
19- class Vasicek (IntensityProcess ):
20- r"""Gaussian OU process, also know as the
21- [Vasiceck model](https://en.wikipedia.org/wiki/Vasicek_model)
18+ class Vasicek (StochasticProcess1D ):
19+ r"""Gaussian OU process, also known as the
20+ [Vasicek model](https://en.wikipedia.org/wiki/Vasicek_model).
2221
2322 Historically, the Vasicek model was used to model the short rate, but it can be
2423 used to model any process that reverts to a mean level at a rate proportional to
25- the difference between the current level and the mean level.
24+ the difference between the current level and the mean level. Unlike intensity
25+ processes, the Vasicek process can take negative values, so it is not constrained
26+ to a positive domain.
2627
2728 \begin{equation}
2829 dx_t = \kappa (\theta - x_t) dt + \sigma dw_t
@@ -37,11 +38,15 @@ class Vasicek(IntensityProcess):
3738 \end{equation}
3839 """
3940
41+ rate : float = Field (default = 1.0 , description = r"Initial value $x_0$" )
42+ kappa : float = Field (
43+ default = 1.0 , gt = 0 , description = r"Mean reversion speed $\kappa$"
44+ )
45+ theta : float = Field (default = 1.0 , description = r"Mean level $\theta$" )
4046 bdlp : WeinerProcess = Field (
4147 default_factory = WeinerProcess ,
42- description = "Background driving Weiner process" ,
48+ description = "Background driving Wiener process" ,
4349 )
44- theta : float = Field (default = 1.0 , gt = 0 , description = r"Mean rate $\theta$" )
4550
4651 def characteristic_exponent (self , t : FloatArrayLike , u : Vector ) -> Vector :
4752 r"""The characteristic exponent of the Vasicek process is given by
@@ -54,9 +59,6 @@ def characteristic_exponent(self, t: FloatArrayLike, u: Vector) -> Vector:
5459 var = self .analytical_variance (t )
5560 return u * (- 1j * mu + 0.5 * var * u )
5661
57- def integrated_log_laplace (self , t : FloatArrayLike , u : Vector ) -> Vector :
58- raise NotImplementedError
59-
6062 def sample (self , n : int , time_horizon : float = 1 , time_steps : int = 100 ) -> Paths :
6163 paths = Paths .normal_draws (n , time_horizon , time_steps )
6264 return self .sample_from_draws (paths )
@@ -74,8 +76,8 @@ def sample_from_draws(self, draws: Paths, *args: Paths) -> Paths:
7476 paths [t + 1 , :] = x + dx
7577 return Paths (t = draws .t , data = paths )
7678
77- def domain_range (self ) -> Bounds :
78- return Bounds ( - np . inf , np . inf )
79+ def ekt (self , t : FloatArrayLike ) -> FloatArrayLike :
80+ return np . exp ( - self . kappa * t )
7981
8082 def analytical_mean (self , t : FloatArrayLike ) -> FloatArrayLike :
8183 r"""The analytical mean of the Vasicek process is given by
@@ -149,10 +151,12 @@ def intensity(self) -> float:
149151
150152 @abstractmethod
151153 def characteristic_exponent (self , t : FloatArrayLike , u : Vector ) -> Vector :
152- r"""
154+ r"""Characteristic exponent of a non-Gaussian OU process driven by a
155+ Lévy process $Z$ with characteristic exponent $\phi_Z$:
156+
153157 \begin{equation}
154- \phi_{x_t,u} = iu x_0 e^{-\kappa t} +
155- \int_{ue ^{-\kappa t}}^{u} \frac{\psi_Z (v)}{v} , dv
158+ \phi_{x_t, u} = - iu\, x_0\, e^{-\kappa t}
159+ + \int_{u e ^{-\kappa t}}^{u} \frac{\phi_Z (v)}{v}\, dv
156160 \end{equation}
157161 """
158162
@@ -180,30 +184,82 @@ def create(cls, rate: float = 1, decay: float = 1, kappa: float = 1) -> Self:
180184 )
181185
182186 def characteristic_exponent (self , t : FloatArrayLike , u : Vector ) -> Vector :
183- r"""
187+ r"""Closed form of the [NGOU][quantflow.sp.ou.NGOU] characteristic
188+ exponent when the BDLP is a compound Poisson process with intensity
189+ $\lambda$ and exponential jumps of decay $\beta$:
190+
191+ \begin{equation}
192+ \phi_{x_t, u} =
193+ - iu\, x_0\, e^{-\kappa t}
194+ + \lambda \log\!\left(
195+ \frac{\beta - iu}{\beta - iu\, e^{-\kappa t}}
196+ \right)
197+ \end{equation}
198+
199+ Derivation. The characteristic function of the
200+ [Exponential][quantflow.utils.distributions.Exponential.characteristic]
201+ jumps is
202+
184203 \begin{equation}
185- \phi_{x_t,u} =
186- - iu x_0 e^{-\kappa t}
187- + \lambda \log\!\left(\frac{\beta - iu}{\beta - iu e^{-\kappa t}}\right)
204+ \Phi_J(v) = \frac{\beta}{\beta - iv},
188205 \end{equation}
206+
207+ so the BDLP unit-time characteristic exponent
208+ ([CompoundPoissonProcess][quantflow.sp.poisson.CompoundPoissonProcess]
209+ at $t = 1$) is
210+
211+ \begin{equation}
212+ \phi_Z(v) = \lambda\bigl(1 - \Phi_J(v)\bigr)
213+ = -\frac{i v\, \lambda}{\beta - iv},
214+ \end{equation}
215+
216+ and $\phi_Z(v)/v = -i\lambda / (\beta - iv)$. Substituting
217+ $w = \beta - iv$ (so $dv = i\, dw$) in the NGOU integral gives
218+
219+ \begin{equation}
220+ \int_{u e^{-\kappa t}}^{u} \frac{\phi_Z(v)}{v}\, dv
221+ = \lambda \int_{\beta - iu\, e^{-\kappa t}}^{\beta - iu}
222+ \frac{dw}{w}
223+ = \lambda \log\!\left(
224+ \frac{\beta - iu}{\beta - iu\, e^{-\kappa t}}
225+ \right),
226+ \end{equation}
227+
228+ which combined with the drift term $-iu\, x_0\, e^{-\kappa t}$ yields
229+ the closed form above.
189230 """
190231 b = self .beta
191- iu = Im * u
232+ iu = 1j * u
192233 c1 = iu * np .exp (- self .kappa * t )
193234 c0 = self .intensity * np .log ((b - c1 ) / (b - iu ))
194235 return - c0 - c1 * self .rate
195236
196237 def integrated_log_laplace (self , t : FloatArrayLike , u : Vector ) -> Vector :
238+ r"""Log-Laplace transform of the time-integrated Gamma-OU process.
239+
240+ \begin{equation}
241+ \begin{aligned}
242+ \phi(t, u) &= \log E\!\left[e^{-u \int_0^t x_s\, ds}\right] \\
243+ &= -u\, x_0\, \frac{1 - e^{-\kappa t}}{\kappa}
244+ + \frac{\lambda}{u + \beta\kappa}
245+ \!\left[\beta\kappa\,
246+ \log\!\frac{\beta\kappa + u(1 - e^{-\kappa t})}{\beta\kappa}
247+ - u\kappa t\right]
248+ \end{aligned}
249+ \end{equation}
250+
251+ where $x_0$ is the initial rate, $\lambda$ is the BDLP intensity and
252+ $\beta$ the exponential jump decay.
253+ """
197254 kappa = self .kappa
198255 b = self .beta
199- iu = Im * u
200- iuk = iu / kappa
256+ muk = - u / kappa
201257 ekt = np .exp (- kappa * t )
202- c1 = iuk * (1 - ekt )
258+ c1 = muk * (1 - ekt )
203259 c0 = self .intensity * (
204- b * np .log (b / (iuk + (b - iuk ) / ekt )) / (iuk - b ) - kappa * t
260+ b * np .log (b / (muk + (b - muk ) / ekt )) / (muk - b ) - kappa * t
205261 )
206- return np . exp ( c0 + c1 * self .rate )
262+ return c0 + c1 * self .rate
207263
208264 def sample (self , n : int , time_horizon : float = 1 , time_steps : int = 100 ) -> Paths :
209265 dt = time_horizon / time_steps
@@ -235,28 +291,43 @@ def _advance(
235291 ) -> int :
236292 x = pp [i - 1 ]
237293 kappa = self .kappa
238- t0 = i * dt
239- t1 = t0 + dt
294+ # step i fills pp[i] from pp[i-1] over the interval [(i-1) dt, i dt]
295+ t0 = (i - 1 ) * dt
296+ t1 = i * dt
240297 a = arrival or t1
241298 pp [i ] = x - kappa * x * (a - t0 ) - kappa * (x + jump ) * (t1 - a ) + jump
242299 return i + 1
243300
244- def cumulative_characteristic2 (self , t : FloatArrayLike , u : Vector ) -> Vector :
245- """Formula from a paper"""
246- kappa = self .kappa
247- b = self .beta
248- iu = Im * u
249- iuk = iu / kappa
250- ekt = np .exp (- kappa * t )
251- c1 = iuk * (1 - ekt )
252- c0 = self .intensity * (b * np .log (b / (b - c1 )) - iu * t ) / (iuk - b )
253- return np .exp (c0 + c1 * self .rate )
254-
255301 def analytical_mean (self , t : FloatArrayLike ) -> FloatArrayLike :
256- return self .intensity / self .beta
302+ r"""Analytical mean of the Gamma-OU process at time $t$.
303+
304+ \begin{equation}
305+ E[x_t] = x_0\, e^{-\kappa t}
306+ + \frac{\lambda}{\beta}\bigl(1 - e^{-\kappa t}\bigr)
307+ \end{equation}
308+
309+ which converges to the stationary mean $\lambda / \beta$ as
310+ $t \to \infty$.
311+ """
312+ ekt = self .ekt (t )
313+ return self .rate * ekt + (self .intensity / self .beta ) * (1 - ekt )
257314
258315 def analytical_variance (self , t : FloatArrayLike ) -> FloatArrayLike :
259- return self .intensity / self .beta / self .beta
316+ r"""Analytical variance of the Gamma-OU process at time $t$.
317+
318+ \begin{equation}
319+ \mathrm{Var}(x_t) = \frac{\lambda}{\beta^2}
320+ \bigl(1 - e^{-2\kappa t}\bigr)
321+ \end{equation}
322+
323+ which converges to the stationary variance $\lambda / \beta^2$ as
324+ $t \to \infty$.
325+ """
326+ return self .intensity * (1 - self .ekt (2 * t )) / (self .beta * self .beta )
260327
261328 def analytical_pdf (self , t : FloatArrayLike , x : FloatArrayLike ) -> FloatArrayLike :
329+ """Stationary marginal density of the Gamma-OU process. The transient
330+ density is not available in closed form; use `pdf_from_characteristic`
331+ for finite $t$.
332+ """
262333 return gamma .pdf (x , self .intensity , scale = 1 / self .beta )
0 commit comments