stokestrel 0.1.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.
@@ -0,0 +1,5 @@
1
+ """Jump diffusion processes."""
2
+
3
+ from kestrel.jump_diffusion.merton import MertonProcess
4
+
5
+ __all__ = ["MertonProcess"]
@@ -0,0 +1,375 @@
1
+ # kestrel/jump_diffusion/merton.py
2
+ """Merton Jump Diffusion process implementation."""
3
+
4
+ import math
5
+ import numpy as np
6
+ import pandas as pd
7
+ from scipy.optimize import minimize
8
+ from scipy.stats import norm
9
+ from kestrel.base import StochasticProcess
10
+ from kestrel.utils.kestrel_result import KestrelResult
11
+
12
+
13
+ class MertonProcess(StochasticProcess):
14
+ """
15
+ Merton Jump Diffusion Process.
16
+
17
+ Combines Brownian motion with Poisson-distributed jumps.
18
+ For log-returns: r_t = (mu - 0.5*sigma^2 - lambda*k) dt + sigma dW_t + J_t dN_t
19
+
20
+ Where J_t ~ N(jump_mu, jump_sigma^2) and N_t is Poisson(lambda).
21
+
22
+ Parameters
23
+ ----------
24
+ mu : float, optional
25
+ Drift of continuous component.
26
+ sigma : float, optional
27
+ Volatility of continuous component.
28
+ lambda_ : float, optional
29
+ Jump intensity (expected jumps per unit time).
30
+ jump_mu : float, optional
31
+ Mean of jump size distribution.
32
+ jump_sigma : float, optional
33
+ Standard deviation of jump size distribution.
34
+ """
35
+
36
+ def __init__(self, mu: float = None, sigma: float = None,
37
+ lambda_: float = None, jump_mu: float = None, jump_sigma: float = None):
38
+ super().__init__()
39
+ self.mu = mu
40
+ self.sigma = sigma
41
+ self.lambda_ = lambda_
42
+ self.jump_mu = jump_mu
43
+ self.jump_sigma = jump_sigma
44
+
45
+ def fit(self, data: pd.Series, dt: float = None, method: str = 'mle'):
46
+ """
47
+ Estimates parameters from log-return time-series data.
48
+
49
+ Parameters
50
+ ----------
51
+ data : pd.Series
52
+ Log-returns (or price levels, which are converted to log-returns).
53
+ dt : float, optional
54
+ Time step between observations.
55
+ method : str
56
+ Estimation method: 'mle' only currently supported.
57
+
58
+ Raises
59
+ ------
60
+ ValueError
61
+ If data not pandas Series or unknown method.
62
+ """
63
+ if not isinstance(data, pd.Series):
64
+ raise ValueError("Input data must be a pandas Series.")
65
+
66
+ if dt is None:
67
+ dt = self._infer_dt(data)
68
+
69
+ self.dt_ = dt
70
+
71
+ if method == 'mle':
72
+ param_ses = self._fit_mle(data, dt)
73
+ else:
74
+ raise ValueError(f"Unknown estimation method: {method}. Choose 'mle'.")
75
+
76
+ self._set_params(
77
+ last_data_point=data.iloc[-1],
78
+ mu=self.mu,
79
+ sigma=self.sigma,
80
+ lambda_=self.lambda_,
81
+ jump_mu=self.jump_mu,
82
+ jump_sigma=self.jump_sigma,
83
+ dt=self.dt_,
84
+ param_ses=param_ses
85
+ )
86
+
87
+ def _infer_dt(self, data: pd.Series) -> float:
88
+ """Infers dt from DatetimeIndex or defaults to 1.0."""
89
+ if isinstance(data.index, pd.DatetimeIndex):
90
+ if len(data.index) < 2:
91
+ return 1.0
92
+
93
+ inferred_timedelta = data.index[1] - data.index[0]
94
+ current_freq = pd.infer_freq(data.index)
95
+ if current_freq is None:
96
+ current_freq = 'B'
97
+
98
+ if current_freq in ['B', 'C', 'D']:
99
+ dt = inferred_timedelta / pd.Timedelta(days=252.0)
100
+ elif current_freq.startswith('W'):
101
+ dt = inferred_timedelta / pd.Timedelta(weeks=52)
102
+ elif current_freq in ['M', 'MS', 'BM', 'BMS']:
103
+ dt = inferred_timedelta / pd.Timedelta(days=365 / 12)
104
+ elif current_freq in ['Q', 'QS', 'BQ', 'BQS']:
105
+ dt = inferred_timedelta / pd.Timedelta(days=365 / 4)
106
+ elif current_freq in ['A', 'AS', 'BA', 'BAS', 'Y', 'YS', 'BY', 'BYS']:
107
+ dt = inferred_timedelta / pd.Timedelta(days=365)
108
+ else:
109
+ dt = inferred_timedelta.total_seconds() / (365 * 24 * 3600)
110
+
111
+ return max(dt, 1e-10)
112
+ return 1.0
113
+
114
+ def _fit_mle(self, data: pd.Series, dt: float) -> dict:
115
+ """
116
+ Estimates Merton parameters using Maximum Likelihood.
117
+
118
+ Uses mixture density approach where observation density is weighted
119
+ sum over possible jump counts.
120
+ """
121
+ if len(data) < 10:
122
+ raise ValueError("MLE estimation requires at least 10 data points.")
123
+
124
+ returns = data.values
125
+ n = len(returns)
126
+
127
+ # Initial parameter estimates from moments
128
+ mean_r = np.mean(returns)
129
+ var_r = np.var(returns)
130
+ skew_r = self._skewness(returns)
131
+ kurt_r = self._kurtosis(returns)
132
+
133
+ # Method of moments initial guess
134
+ # Excess kurtosis suggests jumps; higher kurtosis = more/larger jumps
135
+ excess_kurt = max(0, kurt_r - 3)
136
+
137
+ if excess_kurt > 0.5:
138
+ # Evidence of jumps
139
+ lambda_0 = min(2.0, max(0.1, excess_kurt / 2))
140
+ jump_sigma_0 = np.sqrt(var_r * 0.3)
141
+ sigma_0 = np.sqrt(max(0.01, var_r * 0.7 / dt))
142
+ else:
143
+ # Weak evidence of jumps
144
+ lambda_0 = 0.1
145
+ jump_sigma_0 = np.sqrt(var_r * 0.1)
146
+ sigma_0 = np.sqrt(max(0.01, var_r * 0.9 / dt))
147
+
148
+ mu_0 = mean_r / dt
149
+ jump_mu_0 = 0.0 # Symmetric jumps initially
150
+
151
+ initial_params = [mu_0, sigma_0, lambda_0, jump_mu_0, jump_sigma_0]
152
+ bounds = [
153
+ (None, None), # mu
154
+ (1e-6, None), # sigma
155
+ (1e-6, 10.0), # lambda_
156
+ (None, None), # jump_mu
157
+ (1e-6, None) # jump_sigma
158
+ ]
159
+
160
+ result = minimize(
161
+ self._neg_log_likelihood,
162
+ initial_params,
163
+ args=(returns, dt),
164
+ bounds=bounds,
165
+ method='L-BFGS-B',
166
+ options={'maxiter': 500}
167
+ )
168
+
169
+ if result.success:
170
+ self.mu, self.sigma, self.lambda_, self.jump_mu, self.jump_sigma = result.x
171
+ else:
172
+ # Fallback to moment estimates
173
+ self.mu = mu_0
174
+ self.sigma = sigma_0
175
+ self.lambda_ = lambda_0
176
+ self.jump_mu = jump_mu_0
177
+ self.jump_sigma = jump_sigma_0
178
+
179
+ # Ensure sigma and jump_sigma are positive
180
+ self.sigma = max(1e-6, self.sigma)
181
+ self.lambda_ = max(1e-6, self.lambda_)
182
+ self.jump_sigma = max(1e-6, self.jump_sigma)
183
+
184
+ param_ses = self._compute_standard_errors(
185
+ result,
186
+ ['mu', 'sigma', 'lambda_', 'jump_mu', 'jump_sigma']
187
+ )
188
+ return param_ses
189
+
190
+ def _neg_log_likelihood(self, params, returns, dt):
191
+ """
192
+ Negative log-likelihood for Merton model.
193
+
194
+ Density is mixture over Poisson jump counts:
195
+ f(r) = sum_{k=0}^{K} P(N=k) * phi(r; mu_k, sigma_k^2)
196
+
197
+ where mu_k = (mu - 0.5*sigma^2)*dt + k*jump_mu
198
+ sigma_k^2 = sigma^2*dt + k*jump_sigma^2
199
+ """
200
+ mu, sigma, lambda_, jump_mu, jump_sigma = params
201
+
202
+ if sigma <= 0 or lambda_ < 0 or jump_sigma <= 0:
203
+ return np.inf
204
+
205
+ n = len(returns)
206
+ ll = 0.0
207
+
208
+ # Truncate Poisson sum at reasonable number of jumps
209
+ max_jumps = max(10, int(3 * lambda_ * dt + 5))
210
+
211
+ for r in returns:
212
+ density = 0.0
213
+
214
+ for k in range(max_jumps + 1):
215
+ # Poisson probability of k jumps
216
+ poisson_prob = self._poisson_pmf(k, lambda_ * dt)
217
+
218
+ # Conditional mean and variance given k jumps
219
+ mean_k = (mu - 0.5 * sigma ** 2) * dt + k * jump_mu
220
+ var_k = sigma ** 2 * dt + k * jump_sigma ** 2
221
+
222
+ if var_k <= 0:
223
+ continue
224
+
225
+ # Gaussian density contribution
226
+ density += poisson_prob * norm.pdf(r, loc=mean_k, scale=np.sqrt(var_k))
227
+
228
+ if density <= 1e-300:
229
+ ll += -700 # Log of very small number
230
+ else:
231
+ ll += np.log(density)
232
+
233
+ return -ll
234
+
235
+ def _poisson_pmf(self, k: int, lam: float) -> float:
236
+ """Poisson probability mass function."""
237
+ if lam <= 0:
238
+ return 1.0 if k == 0 else 0.0
239
+ return np.exp(-lam) * (lam ** k) / math.factorial(k)
240
+
241
+ def _skewness(self, x: np.ndarray) -> float:
242
+ """Computes sample skewness."""
243
+ n = len(x)
244
+ mean = np.mean(x)
245
+ std = np.std(x, ddof=0)
246
+ if std == 0:
247
+ return 0.0
248
+ return np.sum((x - mean) ** 3) / (n * std ** 3)
249
+
250
+ def _kurtosis(self, x: np.ndarray) -> float:
251
+ """Computes sample kurtosis (not excess)."""
252
+ n = len(x)
253
+ mean = np.mean(x)
254
+ std = np.std(x, ddof=0)
255
+ if std == 0:
256
+ return 3.0
257
+ return np.sum((x - mean) ** 4) / (n * std ** 4)
258
+
259
+ def _compute_standard_errors(self, result, param_names: list) -> dict:
260
+ """Computes standard errors from optimisation result."""
261
+ param_ses = {}
262
+ if hasattr(result, 'hess_inv') and result.hess_inv is not None:
263
+ if callable(getattr(result.hess_inv, 'todense', None)):
264
+ cov_matrix = result.hess_inv.todense()
265
+ else:
266
+ cov_matrix = result.hess_inv
267
+
268
+ if cov_matrix.shape == (len(param_names), len(param_names)):
269
+ for i, name in enumerate(param_names):
270
+ param_ses[name] = np.sqrt(max(0, cov_matrix[i, i]))
271
+ else:
272
+ for name in param_names:
273
+ param_ses[name] = np.nan
274
+ else:
275
+ for name in param_names:
276
+ param_ses[name] = np.nan
277
+
278
+ return param_ses
279
+
280
+ def sample(self, n_paths: int = 1, horizon: int = 1, dt: float = None) -> KestrelResult:
281
+ """
282
+ Simulates future paths of Merton Jump Diffusion.
283
+
284
+ Parameters
285
+ ----------
286
+ n_paths : int
287
+ Number of simulation paths.
288
+ horizon : int
289
+ Number of time steps to simulate.
290
+ dt : float, optional
291
+ Simulation time step. Uses fitted dt if None.
292
+
293
+ Returns
294
+ -------
295
+ KestrelResult
296
+ Simulation results containing log-return paths.
297
+ """
298
+ if not self.is_fitted and any(p is None for p in
299
+ [self.mu, self.sigma, self.lambda_, self.jump_mu, self.jump_sigma]):
300
+ raise RuntimeError("Model must be fitted or initialised with parameters before sampling.")
301
+
302
+ if dt is None:
303
+ dt = self._dt_ if self.is_fitted and hasattr(self, '_dt_') else 1.0
304
+
305
+ mu = self.mu_ if self.is_fitted else self.mu
306
+ sigma = self.sigma_ if self.is_fitted else self.sigma
307
+ lambda_ = self.lambda__ if self.is_fitted else self.lambda_
308
+ jump_mu = self.jump_mu_ if self.is_fitted else self.jump_mu
309
+ jump_sigma = self.jump_sigma_ if self.is_fitted else self.jump_sigma
310
+
311
+ if any(p is None for p in [mu, sigma, lambda_, jump_mu, jump_sigma]):
312
+ raise RuntimeError("Merton parameters must be set or estimated to sample.")
313
+
314
+ paths = np.zeros((horizon + 1, n_paths))
315
+ if self.is_fitted and hasattr(self, '_last_data_point'):
316
+ initial_val = self._last_data_point
317
+ else:
318
+ initial_val = 0.0
319
+
320
+ paths[0, :] = initial_val
321
+
322
+ for t in range(horizon):
323
+ # Continuous component (Brownian motion)
324
+ dW = np.random.normal(loc=0.0, scale=np.sqrt(dt), size=n_paths)
325
+ continuous_step = (mu - 0.5 * sigma ** 2) * dt + sigma * dW
326
+
327
+ # Jump component (Poisson process with normal jump sizes)
328
+ num_jumps = np.random.poisson(lambda_ * dt, size=n_paths)
329
+ jump_sizes = np.zeros(n_paths)
330
+
331
+ for i in range(n_paths):
332
+ if num_jumps[i] > 0:
333
+ # Sum of normal jumps
334
+ individual_jumps = np.random.normal(
335
+ loc=jump_mu,
336
+ scale=jump_sigma,
337
+ size=num_jumps[i]
338
+ )
339
+ jump_sizes[i] = np.sum(individual_jumps)
340
+
341
+ paths[t + 1, :] = paths[t, :] + continuous_step + jump_sizes
342
+
343
+ return KestrelResult(pd.DataFrame(paths), initial_value=initial_val)
344
+
345
+ def expected_return(self) -> float:
346
+ """
347
+ Computes expected return per unit time.
348
+
349
+ E[r] = mu - 0.5*sigma^2 + lambda*jump_mu
350
+ """
351
+ mu = self.mu_ if self.is_fitted else self.mu
352
+ sigma = self.sigma_ if self.is_fitted else self.sigma
353
+ lambda_ = self.lambda__ if self.is_fitted else self.lambda_
354
+ jump_mu = self.jump_mu_ if self.is_fitted else self.jump_mu
355
+
356
+ if any(p is None for p in [mu, sigma, lambda_, jump_mu]):
357
+ raise RuntimeError("Parameters must be set or estimated first.")
358
+
359
+ return mu - 0.5 * sigma ** 2 + lambda_ * jump_mu
360
+
361
+ def total_variance(self) -> float:
362
+ """
363
+ Computes total variance per unit time.
364
+
365
+ Var[r] = sigma^2 + lambda*(jump_mu^2 + jump_sigma^2)
366
+ """
367
+ sigma = self.sigma_ if self.is_fitted else self.sigma
368
+ lambda_ = self.lambda__ if self.is_fitted else self.lambda_
369
+ jump_mu = self.jump_mu_ if self.is_fitted else self.jump_mu
370
+ jump_sigma = self.jump_sigma_ if self.is_fitted else self.jump_sigma
371
+
372
+ if any(p is None for p in [sigma, lambda_, jump_mu, jump_sigma]):
373
+ raise RuntimeError("Parameters must be set or estimated first.")
374
+
375
+ return sigma ** 2 + lambda_ * (jump_mu ** 2 + jump_sigma ** 2)
@@ -0,0 +1,5 @@
1
+ """Utility classes and functions."""
2
+
3
+ from kestrel.utils.kestrel_result import KestrelResult
4
+
5
+ __all__ = ["KestrelResult"]
@@ -0,0 +1,67 @@
1
+ # kestrel/utils/kestrel_result.py
2
+ import numpy as np
3
+ import pandas as pd
4
+ import matplotlib.pyplot as plt
5
+ from typing import Optional
6
+
7
+ class KestrelResult:
8
+ """
9
+ A wrapper for simulation results, providing enhanced functionality.
10
+
11
+ This class holds simulation paths, typically a pandas DataFrame,
12
+ and offers convenience methods for plotting and analysis.
13
+ """
14
+ def __init__(self, paths: pd.DataFrame, initial_value: Optional[float] = None):
15
+ if not isinstance(paths, pd.DataFrame):
16
+ raise TypeError("`paths` must be a pandas DataFrame.")
17
+ self._paths = paths
18
+ self._initial_value = initial_value
19
+
20
+ @property
21
+ def paths(self) -> pd.DataFrame:
22
+ """
23
+ Returns the underlying DataFrame of simulation paths.
24
+ """
25
+ return self._paths
26
+
27
+ def plot(self, title: str = "Simulation Paths", xlabel: str = "Time Step",
28
+ ylabel: str = "Value", figsize: tuple = (10, 6), **kwargs):
29
+ """
30
+ Plots the simulation paths.
31
+
32
+ Args:
33
+ title (str): Plot title.
34
+ xlabel (str): Label for the x-axis.
35
+ ylabel (str): Label for the y-axis.
36
+ figsize (tuple): Figure size (width, height).
37
+ **kwargs: Additional keyword arguments passed to `DataFrame.plot()`.
38
+ """
39
+ ax = self._paths.plot(figsize=figsize, title=title, legend=False, **kwargs)
40
+ ax.set_xlabel(xlabel)
41
+ ax.set_ylabel(ylabel)
42
+ plt.grid(True, linestyle=':', alpha=0.7)
43
+ plt.show()
44
+
45
+ def mean_path(self) -> pd.Series:
46
+ """
47
+ Calculates the mean path across all simulations.
48
+ """
49
+ return self._paths.mean(axis=1)
50
+
51
+ def percentile_paths(self, percentiles: list = [25, 50, 75]) -> pd.DataFrame:
52
+ """
53
+ Calculates percentile paths for the simulations.
54
+
55
+ Args:
56
+ percentiles (list): List of percentiles to calculate (e.g., [25, 50, 75]).
57
+
58
+ Returns:
59
+ pd.DataFrame: DataFrame with percentile paths.
60
+ """
61
+ return self._paths.quantile(np.array(percentiles) / 100, axis=1).T
62
+
63
+ def __repr__(self) -> str:
64
+ return f"KestrelResult(paths_shape={self._paths.shape}, initial_value={self._initial_value})"
65
+
66
+ def __str__(self) -> str:
67
+ return self.__repr__()
@@ -0,0 +1,234 @@
1
+ Metadata-Version: 2.4
2
+ Name: stokestrel
3
+ Version: 0.1.0
4
+ Summary: A modern stochastic modelling library for parameter estimation and Monte Carlo simulation.
5
+ Author-email: April Kidd <bk0851@outlook.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/april-webm/kestrel
8
+ Project-URL: Documentation, https://github.com/april-webm/kestrel#readme
9
+ Project-URL: Repository, https://github.com/april-webm/kestrel
10
+ Project-URL: Issues, https://github.com/april-webm/kestrel/issues
11
+ Project-URL: Changelog, https://github.com/april-webm/kestrel/releases
12
+ Keywords: stochastic,sde,monte-carlo,simulation,ornstein-uhlenbeck,brownian-motion,quantitative-finance,time-series
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Financial and Insurance Industry
16
+ Classifier: Intended Audience :: Science/Research
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.9
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Topic :: Scientific/Engineering
25
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
26
+ Classifier: Topic :: Office/Business :: Financial
27
+ Classifier: Typing :: Typed
28
+ Requires-Python: >=3.9
29
+ Description-Content-Type: text/markdown
30
+ License-File: LICENSE
31
+ Requires-Dist: numpy>=1.20
32
+ Requires-Dist: scipy>=1.7
33
+ Requires-Dist: pandas>=1.3
34
+ Requires-Dist: matplotlib>=3.4
35
+ Provides-Extra: dev
36
+ Requires-Dist: pytest>=7.0; extra == "dev"
37
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
38
+ Dynamic: license-file
39
+
40
+ # Kestrel
41
+
42
+ <!-- TODO: Add kestrel image -->
43
+ <p align="center">
44
+ <img src="assets/kestrel.png" alt="Kestrel" width="200">
45
+ </p>
46
+
47
+ [![PyPI version](https://img.shields.io/pypi/v/stokestrel.svg)](https://pypi.org/project/stokestrel/)
48
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
49
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
50
+
51
+ A modern Python library for stochastic process modelling, parameter estimation, and Monte Carlo simulation.
52
+
53
+ ## Overview
54
+
55
+ Kestrel provides a unified, scikit-learn-style interface for working with stochastic differential equations (SDEs). The library supports parameter estimation from time-series data and path simulation for a variety of continuous and jump-diffusion processes.
56
+
57
+ ### Supported Processes
58
+
59
+ | Process | Module | Description |
60
+ |---------|--------|-------------|
61
+ | Brownian Motion | `kestrel.diffusion` | Standard Wiener process with drift |
62
+ | Geometric Brownian Motion | `kestrel.diffusion` | Log-normal price dynamics (Black-Scholes) |
63
+ | Ornstein-Uhlenbeck | `kestrel.diffusion` | Mean-reverting Gaussian process |
64
+ | Cox-Ingersoll-Ross | `kestrel.diffusion` | Mean-reverting process with state-dependent volatility |
65
+ | Merton Jump Diffusion | `kestrel.jump_diffusion` | GBM with Poisson-distributed jumps |
66
+
67
+ ### Key Features
68
+
69
+ - **Consistent API**: All processes follow `fit()` / `sample()` pattern
70
+ - **Multiple Estimation Methods**: MLE, AR(1) regression, least squares
71
+ - **Standard Error Reporting**: Parameter uncertainty quantification
72
+ - **Flexible Time Handling**: Automatic dt inference from DatetimeIndex
73
+ - **Simulation Engine**: Euler-Maruyama discretisation with exact solutions where available
74
+
75
+ ## Installation
76
+
77
+ **Requirements**: Python 3.9+
78
+
79
+ ```bash
80
+ pip install stokestrel
81
+ ```
82
+
83
+ For development installation:
84
+
85
+ ```bash
86
+ git clone https://github.com/april-webm/kestrel.git
87
+ cd kestrel
88
+ pip install -e ".[dev]"
89
+ ```
90
+
91
+ ## Quick Start
92
+
93
+ ```python
94
+ from kestrel import OUProcess
95
+ import pandas as pd
96
+
97
+ # Load or generate time-series data
98
+ data = pd.Series([...]) # Your observed data
99
+
100
+ # Fit model parameters
101
+ model = OUProcess()
102
+ model.fit(data, dt=1/252, method='mle')
103
+
104
+ # View estimated parameters and standard errors
105
+ print(f"Mean reversion speed: {model.theta_:.4f} ± {model.theta_se_:.4f}")
106
+ print(f"Long-run mean: {model.mu_:.4f} ± {model.mu_se_:.4f}")
107
+ print(f"Volatility: {model.sigma_:.4f} ± {model.sigma_se_:.4f}")
108
+
109
+ # Simulate future paths
110
+ paths = model.sample(n_paths=1000, horizon=252)
111
+ paths.plot(title="OU Process Simulation")
112
+ ```
113
+
114
+ ## Usage Examples
115
+
116
+ ### Ornstein-Uhlenbeck Process
117
+
118
+ Mean-reverting process commonly used for interest rates and volatility modelling.
119
+
120
+ ```python
121
+ from kestrel import OUProcess
122
+
123
+ model = OUProcess()
124
+ model.fit(data, dt=1/252, method='mle') # or method='ar1'
125
+
126
+ # Simulate from fitted model
127
+ simulation = model.sample(n_paths=100, horizon=50)
128
+ ```
129
+
130
+ ### Geometric Brownian Motion
131
+
132
+ Standard model for equity prices.
133
+
134
+ ```python
135
+ from kestrel import GeometricBrownianMotion
136
+
137
+ model = GeometricBrownianMotion()
138
+ model.fit(price_data, dt=1/252)
139
+
140
+ # Expected price and variance at horizon
141
+ expected = model.expected_price(t=1.0, s0=100)
142
+ variance = model.variance_price(t=1.0, s0=100)
143
+ ```
144
+
145
+ ### Cox-Ingersoll-Ross Process
146
+
147
+ Ensures non-negativity; suitable for interest rate modelling.
148
+
149
+ ```python
150
+ from kestrel import CIRProcess
151
+
152
+ model = CIRProcess()
153
+ model.fit(rate_data, dt=1/252, method='mle')
154
+
155
+ # Check Feller condition for strict positivity
156
+ if model.feller_condition_satisfied():
157
+ print("Process guaranteed to remain positive")
158
+ ```
159
+
160
+ ### Merton Jump Diffusion
161
+
162
+ Captures sudden market movements via Poisson jumps.
163
+
164
+ ```python
165
+ from kestrel import MertonProcess
166
+
167
+ model = MertonProcess()
168
+ model.fit(log_returns, dt=1/252)
169
+
170
+ # Jump-adjusted expected return
171
+ total_drift = model.expected_return()
172
+ total_var = model.total_variance()
173
+ ```
174
+
175
+ ## API Reference
176
+
177
+ ### Base Interface
178
+
179
+ All stochastic processes inherit from `StochasticProcess` and implement:
180
+
181
+ | Method | Description |
182
+ |--------|-------------|
183
+ | `fit(data, dt, method)` | Estimate parameters from time-series |
184
+ | `sample(n_paths, horizon, dt)` | Generate Monte Carlo paths |
185
+ | `is_fitted` | Property indicating fit status |
186
+ | `params` | Dictionary of estimated parameters |
187
+
188
+ ### Fitted Attributes
189
+
190
+ After calling `fit()`, estimated parameters are available as attributes with trailing underscore:
191
+
192
+ ```python
193
+ model.theta_ # Estimated parameter value
194
+ model.theta_se_ # Standard error of estimate
195
+ ```
196
+
197
+ ## Dependencies
198
+
199
+ - numpy
200
+ - scipy
201
+ - pandas
202
+ - matplotlib
203
+
204
+ ## Testing
205
+
206
+ ```bash
207
+ pytest tests/ -v
208
+ ```
209
+
210
+ ## Contributing
211
+
212
+ Contributions are welcome. Please ensure:
213
+
214
+ 1. Code follows existing style conventions
215
+ 2. New features include appropriate tests
216
+ 3. Documentation is updated accordingly
217
+
218
+ ## License
219
+
220
+ Released under the MIT License. See [LICENSE](LICENSE) for details.
221
+
222
+ ## Citation
223
+
224
+ If Kestrel is used in academic research, citation is appreciated:
225
+
226
+ ```bibtex
227
+ @software{stokestrel,
228
+ title = {Stokestrel: A Modern Stochastic Modelling Library},
229
+ author = {Kidd, April},
230
+ url = {https://github.com/april-webm/kestrel},
231
+ version = {0.1.0},
232
+ year = {2024}
233
+ }
234
+ ```