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.
- kestrel/__init__.py +32 -0
- kestrel/base.py +77 -0
- kestrel/diffusion/__init__.py +12 -0
- kestrel/diffusion/brownian.py +404 -0
- kestrel/diffusion/cir.py +332 -0
- kestrel/diffusion/ou.py +362 -0
- kestrel/jump_diffusion/__init__.py +5 -0
- kestrel/jump_diffusion/merton.py +375 -0
- kestrel/utils/__init__.py +5 -0
- kestrel/utils/kestrel_result.py +67 -0
- stokestrel-0.1.0.dist-info/METADATA +234 -0
- stokestrel-0.1.0.dist-info/RECORD +15 -0
- stokestrel-0.1.0.dist-info/WHEEL +5 -0
- stokestrel-0.1.0.dist-info/licenses/LICENSE +21 -0
- stokestrel-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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,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
|
+
[](https://pypi.org/project/stokestrel/)
|
|
48
|
+
[](https://www.python.org/downloads/)
|
|
49
|
+
[](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
|
+
```
|