FinStoch 0.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
FinStoch/__init__.py ADDED
@@ -0,0 +1,35 @@
1
+ """FinStoch — A Python library for simulating stochastic processes in finance."""
2
+
3
+ try:
4
+ from importlib.metadata import version, PackageNotFoundError
5
+
6
+ __version__ = version("FinStoch")
7
+ except PackageNotFoundError:
8
+ try:
9
+ from FinStoch._version import version as __version__ # type: ignore[no-redef]
10
+ except ImportError:
11
+ __version__ = "0.0.0-unknown"
12
+
13
+ __author__ = "Yosri Ben Halima"
14
+ __email__ = "yosri.benhalima@ept.ucar.tn"
15
+ __license__ = "MIT"
16
+
17
+ from FinStoch.processes import (
18
+ StochasticProcess,
19
+ GeometricBrownianMotion,
20
+ MertonJumpDiffusion,
21
+ OrnsteinUhlenbeck,
22
+ CoxIngersollRoss,
23
+ HestonModel,
24
+ ConstantElasticityOfVariance,
25
+ )
26
+
27
+ __all__ = [
28
+ "StochasticProcess",
29
+ "GeometricBrownianMotion",
30
+ "MertonJumpDiffusion",
31
+ "OrnsteinUhlenbeck",
32
+ "CoxIngersollRoss",
33
+ "HestonModel",
34
+ "ConstantElasticityOfVariance",
35
+ ]
@@ -0,0 +1,19 @@
1
+ """Stochastic process simulators."""
2
+
3
+ from .base import StochasticProcess
4
+ from .gbm import GeometricBrownianMotion
5
+ from .merton import MertonJumpDiffusion
6
+ from .ou import OrnsteinUhlenbeck
7
+ from .cir import CoxIngersollRoss
8
+ from .heston import HestonModel
9
+ from .cev import ConstantElasticityOfVariance
10
+
11
+ __all__ = [
12
+ "StochasticProcess",
13
+ "GeometricBrownianMotion",
14
+ "MertonJumpDiffusion",
15
+ "OrnsteinUhlenbeck",
16
+ "CoxIngersollRoss",
17
+ "HestonModel",
18
+ "ConstantElasticityOfVariance",
19
+ ]
@@ -0,0 +1,202 @@
1
+ """Base class for all stochastic process simulators."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Union
5
+
6
+ import numpy as np
7
+ from pandas import DatetimeIndex
8
+
9
+ from FinStoch.utils.plotting import plot_simulated_paths
10
+ from FinStoch.utils.timesteps import (
11
+ generate_date_range_with_granularity,
12
+ date_range_duration,
13
+ )
14
+
15
+
16
+ class StochasticProcess(ABC):
17
+ """Abstract base class for stochastic process simulators.
18
+
19
+ Provides shared initialization, time grid management, and plotting
20
+ for all Euler-Maruyama discretized stochastic processes.
21
+
22
+ Parameters
23
+ ----------
24
+ S0 : float
25
+ The initial value of the process.
26
+ mu : float
27
+ The drift coefficient.
28
+ sigma : float
29
+ The volatility coefficient.
30
+ num_paths : int
31
+ The number of paths to simulate.
32
+ start_date : str
33
+ The start date for the simulation (e.g., '2023-09-01').
34
+ end_date : str
35
+ The end date for the simulation (e.g., '2023-12-31').
36
+ granularity : str
37
+ The time granularity for each step (e.g., 'D', 'H', '10T').
38
+ business_days : bool, optional
39
+ If True, use business days instead of calendar days when
40
+ granularity is 'D'. Default is False.
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ S0: float,
46
+ mu: float,
47
+ sigma: float,
48
+ num_paths: int,
49
+ start_date: str,
50
+ end_date: str,
51
+ granularity: str,
52
+ business_days: bool = False,
53
+ ) -> None:
54
+ self._S0 = S0
55
+ self._mu = mu
56
+ self._sigma = sigma
57
+ self._num_paths = num_paths
58
+ self._start_date = start_date
59
+ self._end_date = end_date
60
+ self._granularity = granularity
61
+ self._business_days = business_days
62
+ self._recalculate_time_grid()
63
+
64
+ def _recalculate_time_grid(self) -> None:
65
+ """Recompute time grid attributes from date range and granularity."""
66
+ self._t = generate_date_range_with_granularity(
67
+ self._start_date, self._end_date, self._granularity, self._business_days
68
+ )
69
+ self._T = date_range_duration(self._t)
70
+ self._num_steps = len(self._t)
71
+ self._dt = self._T / self._num_steps
72
+
73
+ @abstractmethod
74
+ def simulate(self) -> Union[np.ndarray, tuple[np.ndarray, np.ndarray]]:
75
+ """Simulate paths of the stochastic process.
76
+
77
+ Returns
78
+ -------
79
+ np.ndarray or tuple[np.ndarray, np.ndarray]
80
+ A 2D array of shape (num_paths, num_steps), or a tuple of two
81
+ such arrays for models with multiple outputs (e.g., Heston).
82
+ """
83
+ ...
84
+
85
+ def plot(
86
+ self,
87
+ paths: np.ndarray | None = None,
88
+ title: str = "Simulated Paths",
89
+ ylabel: str = "Value",
90
+ fig_size: tuple | None = None,
91
+ **kwargs: object,
92
+ ) -> None:
93
+ """Plot simulated paths.
94
+
95
+ Parameters
96
+ ----------
97
+ paths : np.ndarray, optional
98
+ Pre-computed paths to plot. If None, calls simulate().
99
+ title : str
100
+ Plot title.
101
+ ylabel : str
102
+ Y-axis label.
103
+ fig_size : tuple, optional
104
+ Figure size in inches.
105
+ **kwargs
106
+ Additional keyword arguments passed to plot_simulated_paths.
107
+ """
108
+ plot_simulated_paths(
109
+ self._t,
110
+ self.simulate,
111
+ paths,
112
+ title=title,
113
+ ylabel=ylabel,
114
+ fig_size=fig_size,
115
+ grid=kwargs.get("grid", True),
116
+ )
117
+
118
+ # --- Shared properties ---
119
+
120
+ @property
121
+ def S0(self) -> float:
122
+ return self._S0
123
+
124
+ @S0.setter
125
+ def S0(self, value: float) -> None:
126
+ self._S0 = value
127
+
128
+ @property
129
+ def mu(self) -> float:
130
+ return self._mu
131
+
132
+ @mu.setter
133
+ def mu(self, value: float) -> None:
134
+ self._mu = value
135
+
136
+ @property
137
+ def sigma(self) -> float:
138
+ return self._sigma
139
+
140
+ @sigma.setter
141
+ def sigma(self, value: float) -> None:
142
+ self._sigma = value
143
+
144
+ @property
145
+ def T(self) -> float:
146
+ return self._T
147
+
148
+ @property
149
+ def num_steps(self) -> int:
150
+ return self._num_steps
151
+
152
+ @property
153
+ def num_paths(self) -> int:
154
+ return self._num_paths
155
+
156
+ @num_paths.setter
157
+ def num_paths(self, value: int) -> None:
158
+ self._num_paths = value
159
+
160
+ @property
161
+ def dt(self) -> float:
162
+ return self._dt
163
+
164
+ @property
165
+ def t(self) -> DatetimeIndex:
166
+ return self._t
167
+
168
+ @property
169
+ def start_date(self) -> str:
170
+ return self._start_date
171
+
172
+ @start_date.setter
173
+ def start_date(self, value: str) -> None:
174
+ self._start_date = value
175
+ self._recalculate_time_grid()
176
+
177
+ @property
178
+ def end_date(self) -> str:
179
+ return self._end_date
180
+
181
+ @end_date.setter
182
+ def end_date(self, value: str) -> None:
183
+ self._end_date = value
184
+ self._recalculate_time_grid()
185
+
186
+ @property
187
+ def granularity(self) -> str:
188
+ return self._granularity
189
+
190
+ @granularity.setter
191
+ def granularity(self, value: str) -> None:
192
+ self._granularity = value
193
+ self._recalculate_time_grid()
194
+
195
+ @property
196
+ def business_days(self) -> bool:
197
+ return self._business_days
198
+
199
+ @business_days.setter
200
+ def business_days(self, value: bool) -> None:
201
+ self._business_days = value
202
+ self._recalculate_time_grid()
@@ -0,0 +1,90 @@
1
+ """Constant Elasticity of Variance process."""
2
+
3
+ import numpy as np
4
+
5
+ from FinStoch.processes.base import StochasticProcess
6
+ from FinStoch.utils.random import generate_random_numbers
7
+
8
+
9
+ class ConstantElasticityOfVariance(StochasticProcess):
10
+ """Constant Elasticity of Variance (CEV) process simulator.
11
+
12
+ Models an asset price following the SDE:
13
+ dS = mu * S * dt + sigma * S^gamma * dW
14
+
15
+ Parameters
16
+ ----------
17
+ S0 : float
18
+ The initial value of the asset.
19
+ mu : float
20
+ The annualized drift coefficient.
21
+ sigma : float
22
+ The annualized volatility coefficient.
23
+ gamma : float
24
+ The elasticity parameter.
25
+ num_paths : int
26
+ The number of paths to simulate.
27
+ start_date : str
28
+ The start date for the simulation.
29
+ end_date : str
30
+ The end date for the simulation.
31
+ granularity : str
32
+ The time granularity for each step.
33
+ business_days : bool, optional
34
+ If True, use business days instead of calendar days. Default is False.
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ S0: float,
40
+ mu: float,
41
+ sigma: float,
42
+ gamma: float,
43
+ num_paths: int,
44
+ start_date: str,
45
+ end_date: str,
46
+ granularity: str,
47
+ business_days: bool = False,
48
+ ) -> None:
49
+ self._gamma = gamma
50
+ super().__init__(S0, mu, sigma, num_paths, start_date, end_date, granularity, business_days)
51
+
52
+ def simulate(self) -> np.ndarray:
53
+ """Simulate paths of the CEV model.
54
+
55
+ Returns
56
+ -------
57
+ np.ndarray
58
+ A 2D array of shape (num_paths, num_steps).
59
+ """
60
+ S = np.zeros((self._num_paths, self._num_steps))
61
+ S[:, 0] = self._S0
62
+
63
+ for t in range(1, self._num_steps):
64
+ Z = generate_random_numbers("normal", self._num_paths, mean=0, stddev=1)
65
+ S[:, t] = (
66
+ S[:, t - 1]
67
+ + self._mu * S[:, t - 1] * self._dt
68
+ + self._sigma * (S[:, t - 1] ** self._gamma) * np.sqrt(self._dt) * Z
69
+ )
70
+
71
+ return S
72
+
73
+ def plot(
74
+ self,
75
+ paths: np.ndarray | None = None,
76
+ title: str = "Constant Elasticity of Variance",
77
+ ylabel: str = "Value",
78
+ fig_size: tuple | None = None,
79
+ **kwargs: object,
80
+ ) -> None:
81
+ """Plot simulated CEV paths."""
82
+ super().plot(paths, title=title, ylabel=ylabel, fig_size=fig_size, **kwargs)
83
+
84
+ @property
85
+ def gamma(self) -> float:
86
+ return self._gamma
87
+
88
+ @gamma.setter
89
+ def gamma(self, value: float) -> None:
90
+ self._gamma = value
@@ -0,0 +1,91 @@
1
+ """Cox-Ingersoll-Ross process."""
2
+
3
+ import numpy as np
4
+
5
+ from FinStoch.processes.base import StochasticProcess
6
+ from FinStoch.utils.random import generate_random_numbers
7
+
8
+
9
+ class CoxIngersollRoss(StochasticProcess):
10
+ """Cox-Ingersoll-Ross (CIR) mean-reverting process simulator.
11
+
12
+ Models a non-negative process following the SDE:
13
+ dS = theta * (mu - S) * dt + sigma * sqrt(S) * dW
14
+
15
+ Parameters
16
+ ----------
17
+ S0 : float
18
+ Initial value of the process.
19
+ mu : float
20
+ Long-term mean to which the process reverts.
21
+ sigma : float
22
+ Volatility parameter.
23
+ theta : float
24
+ Speed of reversion to the mean.
25
+ num_paths : int
26
+ Number of simulation paths to generate.
27
+ start_date : str
28
+ Starting date for the simulation.
29
+ end_date : str
30
+ Ending date for the simulation.
31
+ granularity : str
32
+ Granularity of time steps (e.g., '10T' for 10 minutes, 'H' for hours).
33
+ business_days : bool, optional
34
+ If True, use business days instead of calendar days. Default is False.
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ S0: float,
40
+ mu: float,
41
+ sigma: float,
42
+ theta: float,
43
+ num_paths: int,
44
+ start_date: str,
45
+ end_date: str,
46
+ granularity: str,
47
+ business_days: bool = False,
48
+ ) -> None:
49
+ self._theta = theta
50
+ super().__init__(S0, mu, sigma, num_paths, start_date, end_date, granularity, business_days)
51
+
52
+ def simulate(self) -> np.ndarray:
53
+ """Simulate paths of the CIR model.
54
+
55
+ Returns
56
+ -------
57
+ np.ndarray
58
+ A 2D array of shape (num_paths, num_steps).
59
+ """
60
+ S = np.zeros((self._num_paths, self._num_steps))
61
+ S[:, 0] = self._S0
62
+
63
+ for t in range(1, self._num_steps):
64
+ Z = generate_random_numbers("normal", self._num_paths, mean=0, stddev=1)
65
+ drift = self._theta * (self._mu - S[:, t - 1]) * self._dt
66
+ diffusion = self._sigma * np.sqrt(S[:, t - 1]) * np.sqrt(self._dt) * Z
67
+ S[:, t] = S[:, t - 1] + drift + diffusion
68
+
69
+ # Ensure non-negativity
70
+ S[:, t] = np.maximum(S[:, t], 0)
71
+
72
+ return S
73
+
74
+ def plot(
75
+ self,
76
+ paths: np.ndarray | None = None,
77
+ title: str = "Cox-Ingersoll-Ross",
78
+ ylabel: str = "Value",
79
+ fig_size: tuple | None = None,
80
+ **kwargs: object,
81
+ ) -> None:
82
+ """Plot simulated CIR paths."""
83
+ super().plot(paths, title=title, ylabel=ylabel, fig_size=fig_size, **kwargs)
84
+
85
+ @property
86
+ def theta(self) -> float:
87
+ return self._theta
88
+
89
+ @theta.setter
90
+ def theta(self, value: float) -> None:
91
+ self._theta = value
@@ -0,0 +1,59 @@
1
+ """Geometric Brownian Motion process."""
2
+
3
+ import numpy as np
4
+
5
+ from FinStoch.processes.base import StochasticProcess
6
+ from FinStoch.utils.random import generate_random_numbers
7
+
8
+
9
+ class GeometricBrownianMotion(StochasticProcess):
10
+ """Geometric Brownian Motion (GBM) process simulator.
11
+
12
+ Models an asset price following the SDE:
13
+ dS = mu * S * dt + sigma * S * dW
14
+
15
+ Parameters
16
+ ----------
17
+ S0 : float
18
+ The initial value of the asset.
19
+ mu : float
20
+ The annualized drift coefficient.
21
+ sigma : float
22
+ The annualized volatility coefficient.
23
+ num_paths : int
24
+ The number of paths to simulate.
25
+ start_date : str
26
+ The start date for the simulation (e.g., '2023-09-01').
27
+ end_date : str
28
+ The end date for the simulation (e.g., '2023-12-31').
29
+ granularity : str
30
+ The time granularity for each step (e.g., '10T' for 10 minutes, 'H' for hours).
31
+ """
32
+
33
+ def simulate(self) -> np.ndarray:
34
+ """Simulate paths of the GBM model.
35
+
36
+ Returns
37
+ -------
38
+ np.ndarray
39
+ A 2D array of shape (num_paths, num_steps).
40
+ """
41
+ S = np.zeros((self._num_paths, self._num_steps))
42
+ S[:, 0] = self._S0
43
+
44
+ for t in range(1, self._num_steps):
45
+ Z = generate_random_numbers("normal", self._num_paths, mean=0, stddev=1)
46
+ S[:, t] = S[:, t - 1] * np.exp((self._mu - 0.5 * self._sigma**2) * self._dt + self._sigma * np.sqrt(self._dt) * Z)
47
+
48
+ return S
49
+
50
+ def plot(
51
+ self,
52
+ paths: np.ndarray | None = None,
53
+ title: str = "Geometric Brownian Motion",
54
+ ylabel: str = "Value",
55
+ fig_size: tuple | None = None,
56
+ **kwargs: object,
57
+ ) -> None:
58
+ """Plot simulated GBM paths."""
59
+ super().plot(paths, title=title, ylabel=ylabel, fig_size=fig_size, **kwargs)
@@ -0,0 +1,169 @@
1
+ """Heston stochastic volatility model."""
2
+
3
+ import numpy as np
4
+
5
+ from FinStoch.processes.base import StochasticProcess
6
+ from FinStoch.utils.plotting import plot_simulated_paths
7
+ from FinStoch.utils.random import generate_random_numbers
8
+
9
+
10
+ class HestonModel(StochasticProcess):
11
+ """Heston stochastic volatility model simulator.
12
+
13
+ Models an asset price with stochastic variance:
14
+ dS = mu * S * dt + sqrt(v) * S * dW_s
15
+ dv = kappa * (theta - v) * dt + sigma * sqrt(v) * dW_v
16
+ corr(dW_s, dW_v) = rho
17
+
18
+ Parameters
19
+ ----------
20
+ S0 : float
21
+ The initial value of the asset.
22
+ v0 : float
23
+ The initial variance.
24
+ mu : float
25
+ The drift rate of the asset.
26
+ sigma : float
27
+ The volatility of the variance (vol of vol).
28
+ theta : float
29
+ The long-term mean of the variance process.
30
+ kappa : float
31
+ The rate of mean reversion for the variance.
32
+ rho : float
33
+ The correlation between asset and variance Brownian motions.
34
+ num_paths : int
35
+ The number of paths to simulate.
36
+ start_date : str
37
+ The start date for the simulation.
38
+ end_date : str
39
+ The end date for the simulation.
40
+ granularity : str
41
+ The time granularity for each step.
42
+ business_days : bool, optional
43
+ If True, use business days instead of calendar days. Default is False.
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ S0: float,
49
+ v0: float,
50
+ mu: float,
51
+ sigma: float,
52
+ theta: float,
53
+ kappa: float,
54
+ rho: float,
55
+ num_paths: int,
56
+ start_date: str,
57
+ end_date: str,
58
+ granularity: str,
59
+ business_days: bool = False,
60
+ ) -> None:
61
+ self._v0 = v0
62
+ self._theta = theta
63
+ self._kappa = kappa
64
+ self._rho = rho
65
+ super().__init__(S0, mu, sigma, num_paths, start_date, end_date, granularity, business_days)
66
+
67
+ def simulate(self) -> tuple[np.ndarray, np.ndarray]:
68
+ """Simulate paths of the Heston model.
69
+
70
+ Returns
71
+ -------
72
+ tuple[np.ndarray, np.ndarray]
73
+ A tuple (S, v) of 2D arrays of shape (num_paths, num_steps)
74
+ for asset prices and variance paths respectively.
75
+ """
76
+ S = np.zeros((self._num_paths, self._num_steps))
77
+ S[:, 0] = self._S0
78
+
79
+ v = np.zeros((self._num_paths, self._num_steps))
80
+ v[:, 0] = self._v0
81
+
82
+ for t in range(1, self._num_steps):
83
+ Xs, Xv = (
84
+ generate_random_numbers("normal", self._num_paths, mean=0, stddev=1),
85
+ generate_random_numbers("normal", self._num_paths, mean=0, stddev=1),
86
+ )
87
+ L = np.array([[1, 0], [self._rho, np.sqrt(1 - self._rho**2)]])
88
+
89
+ X = np.dot(L, np.array([Xs, Xv]))
90
+ Ws = X[0]
91
+ Wv = X[1]
92
+
93
+ v[:, t] = np.maximum(
94
+ v[:, t - 1]
95
+ + self._kappa * (self._theta - v[:, t - 1]) * self._dt
96
+ + self._sigma * np.sqrt(v[:, t - 1]) * np.sqrt(self._dt) * Wv,
97
+ 0,
98
+ )
99
+ S[:, t] = S[:, t - 1] * np.exp(
100
+ (self._mu - 0.5 * v[:, t - 1]) * self._dt + np.sqrt(v[:, t - 1]) * np.sqrt(self._dt) * Ws
101
+ )
102
+
103
+ return S, v
104
+
105
+ def plot(
106
+ self,
107
+ paths: np.ndarray | None = None,
108
+ title: str = "Heston Model",
109
+ ylabel: str = "Value",
110
+ fig_size: tuple | None = None,
111
+ **kwargs: object,
112
+ ) -> None:
113
+ """Plot simulated Heston paths.
114
+
115
+ Parameters
116
+ ----------
117
+ paths : np.ndarray, optional
118
+ Pre-computed paths to plot.
119
+ title : str
120
+ Plot title.
121
+ ylabel : str
122
+ Y-axis label.
123
+ fig_size : tuple, optional
124
+ Figure size in inches.
125
+ **kwargs
126
+ Additional keyword arguments. Pass ``variance=True`` to plot
127
+ variance paths instead of asset prices.
128
+ """
129
+ plot_simulated_paths(
130
+ self._t,
131
+ self.simulate,
132
+ paths,
133
+ title=title,
134
+ ylabel=ylabel,
135
+ fig_size=fig_size,
136
+ **kwargs,
137
+ )
138
+
139
+ @property
140
+ def v0(self) -> float:
141
+ return self._v0
142
+
143
+ @v0.setter
144
+ def v0(self, value: float) -> None:
145
+ self._v0 = value
146
+
147
+ @property
148
+ def theta(self) -> float:
149
+ return self._theta
150
+
151
+ @theta.setter
152
+ def theta(self, value: float) -> None:
153
+ self._theta = value
154
+
155
+ @property
156
+ def kappa(self) -> float:
157
+ return self._kappa
158
+
159
+ @kappa.setter
160
+ def kappa(self, value: float) -> None:
161
+ self._kappa = value
162
+
163
+ @property
164
+ def rho(self) -> float:
165
+ return self._rho
166
+
167
+ @rho.setter
168
+ def rho(self, value: float) -> None:
169
+ self._rho = value