FinStoch 0.0.0__tar.gz

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.
Files changed (41) hide show
  1. finstoch-0.0.0/.github/workflows/cd.yml +40 -0
  2. finstoch-0.0.0/.github/workflows/ci.yml +46 -0
  3. finstoch-0.0.0/.gitignore +35 -0
  4. finstoch-0.0.0/FinStoch/__init__.py +35 -0
  5. finstoch-0.0.0/FinStoch/processes/__init__.py +19 -0
  6. finstoch-0.0.0/FinStoch/processes/base.py +202 -0
  7. finstoch-0.0.0/FinStoch/processes/cev.py +90 -0
  8. finstoch-0.0.0/FinStoch/processes/cir.py +91 -0
  9. finstoch-0.0.0/FinStoch/processes/gbm.py +59 -0
  10. finstoch-0.0.0/FinStoch/processes/heston.py +169 -0
  11. finstoch-0.0.0/FinStoch/processes/merton.py +118 -0
  12. finstoch-0.0.0/FinStoch/processes/ou.py +88 -0
  13. finstoch-0.0.0/FinStoch/py.typed +0 -0
  14. finstoch-0.0.0/FinStoch/utils/__init__.py +0 -0
  15. finstoch-0.0.0/FinStoch/utils/plotting.py +60 -0
  16. finstoch-0.0.0/FinStoch/utils/random.py +44 -0
  17. finstoch-0.0.0/FinStoch/utils/timesteps.py +108 -0
  18. finstoch-0.0.0/FinStoch.egg-info/PKG-INFO +390 -0
  19. finstoch-0.0.0/FinStoch.egg-info/SOURCES.txt +39 -0
  20. finstoch-0.0.0/FinStoch.egg-info/dependency_links.txt +1 -0
  21. finstoch-0.0.0/FinStoch.egg-info/requires.txt +13 -0
  22. finstoch-0.0.0/FinStoch.egg-info/top_level.txt +1 -0
  23. finstoch-0.0.0/LICENSE +21 -0
  24. finstoch-0.0.0/PKG-INFO +390 -0
  25. finstoch-0.0.0/README.md +354 -0
  26. finstoch-0.0.0/image/cev.png +0 -0
  27. finstoch-0.0.0/image/cir.png +0 -0
  28. finstoch-0.0.0/image/gbm.png +0 -0
  29. finstoch-0.0.0/image/heston_val.png +0 -0
  30. finstoch-0.0.0/image/heston_vol.png +0 -0
  31. finstoch-0.0.0/image/merton.png +0 -0
  32. finstoch-0.0.0/image/ou.png +0 -0
  33. finstoch-0.0.0/pyproject.toml +61 -0
  34. finstoch-0.0.0/setup.cfg +4 -0
  35. finstoch-0.0.0/tests/business_days_test.py +169 -0
  36. finstoch-0.0.0/tests/cev_test.py +94 -0
  37. finstoch-0.0.0/tests/cir_test.py +94 -0
  38. finstoch-0.0.0/tests/gbm_test.py +86 -0
  39. finstoch-0.0.0/tests/heston_test.py +127 -0
  40. finstoch-0.0.0/tests/merton_test.py +92 -0
  41. finstoch-0.0.0/tests/ou_test.py +94 -0
@@ -0,0 +1,40 @@
1
+ name: CD
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ permissions:
9
+ id-token: write
10
+ contents: read
11
+
12
+ jobs:
13
+ build:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ with:
18
+ fetch-depth: 0 # Required for setuptools-scm to read git tags
19
+ - uses: actions/setup-python@v5
20
+ with:
21
+ python-version: "3.12"
22
+ - run: pip install build
23
+ - run: python -m build
24
+ - uses: actions/upload-artifact@v4
25
+ with:
26
+ name: dist
27
+ path: dist/
28
+
29
+ publish:
30
+ needs: build
31
+ runs-on: ubuntu-latest
32
+ environment: pypi
33
+ permissions:
34
+ id-token: write
35
+ steps:
36
+ - uses: actions/download-artifact@v4
37
+ with:
38
+ name: dist
39
+ path: dist/
40
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,46 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ lint:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ with:
15
+ fetch-depth: 0
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: "3.12"
19
+ - run: pip install -e ".[dev]"
20
+ - name: Check formatting
21
+ run: ruff format --check .
22
+ - name: Lint (critical errors)
23
+ run: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
24
+ - name: Lint (warnings)
25
+ run: flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
26
+ - name: Type check
27
+ run: mypy . --exclude venv --exclude build --exclude dist --ignore-missing-imports
28
+
29
+ test:
30
+ needs: lint
31
+ runs-on: ${{ matrix.os }}
32
+ strategy:
33
+ fail-fast: false
34
+ matrix:
35
+ os: [ubuntu-latest, windows-latest, macos-latest]
36
+ python-version: ["3.10", "3.11", "3.12"]
37
+ steps:
38
+ - uses: actions/checkout@v4
39
+ with:
40
+ fetch-depth: 0
41
+ - uses: actions/setup-python@v5
42
+ with:
43
+ python-version: ${{ matrix.python-version }}
44
+ - run: pip install -e ".[dev]"
45
+ - name: Run tests
46
+ run: python -m unittest discover -s tests -p "*_test.py" -v
@@ -0,0 +1,35 @@
1
+ # Python
2
+ __pycache__/
3
+ **/__pycache__/
4
+ *.pyc
5
+ *.pyo
6
+
7
+ # Build & distribution
8
+ dist/
9
+ build/
10
+ *.egg-info/
11
+
12
+ # Virtual environments
13
+ venv/
14
+ .venv/
15
+
16
+ # IDE
17
+ .vscode/
18
+
19
+ # Testing & linting caches
20
+ .pytest_cache/
21
+ .mypy_cache/
22
+ .ruff_cache/
23
+
24
+ # Generated version file
25
+ FinStoch/_version.py
26
+
27
+ # Claude Code (local only)
28
+ .claude/
29
+
30
+ # Project-specific
31
+ dependencies.txt
32
+ dependencies.json
33
+ *.ipynb
34
+ notes.txt
35
+ CLAUDE.md
@@ -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)