jump-diffusion-estimation 0.2.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.
- jump_diffusion/__init__.py +40 -0
- jump_diffusion/distributions/__init__.py +22 -0
- jump_diffusion/distributions/base.py +152 -0
- jump_diffusion/distributions/kou.py +104 -0
- jump_diffusion/distributions/normal.py +73 -0
- jump_diffusion/distributions/sged.py +113 -0
- jump_diffusion/distributions/skew_normal.py +97 -0
- jump_diffusion/distributions/student_t.py +86 -0
- jump_diffusion/estimation/__init__.py +19 -0
- jump_diffusion/estimation/base_estimator.py +62 -0
- jump_diffusion/estimation/maximum_likelihood.py +1239 -0
- jump_diffusion/models/__init__.py +14 -0
- jump_diffusion/models/base_model.py +93 -0
- jump_diffusion/models/jump_diffusion.py +168 -0
- jump_diffusion/scripts/__init__.py +3 -0
- jump_diffusion/scripts/benchmark.py +10 -0
- jump_diffusion/scripts/validate.py +74 -0
- jump_diffusion/simulation/__init__.py +14 -0
- jump_diffusion/simulation/base_simulator.py +51 -0
- jump_diffusion/simulation/jump_diffusion_simulator.py +264 -0
- jump_diffusion/validation/__init__.py +14 -0
- jump_diffusion/validation/distribution_comparison.py +270 -0
- jump_diffusion/validation/monte_carlo.py +349 -0
- jump_diffusion_estimation-0.2.0.dist-info/METADATA +241 -0
- jump_diffusion_estimation-0.2.0.dist-info/RECORD +29 -0
- jump_diffusion_estimation-0.2.0.dist-info/WHEEL +5 -0
- jump_diffusion_estimation-0.2.0.dist-info/entry_points.txt +3 -0
- jump_diffusion_estimation-0.2.0.dist-info/licenses/LICENSE +25 -0
- jump_diffusion_estimation-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Jump-Diffusion Model Definitions
|
|
3
|
+
|
|
4
|
+
This module contains the mathematical models for various types of
|
|
5
|
+
jump-diffusion processes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .jump_diffusion import JumpDiffusionModel
|
|
9
|
+
from .base_model import BaseStochasticModel
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"JumpDiffusionModel",
|
|
13
|
+
"BaseStochasticModel",
|
|
14
|
+
]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base classes for stochastic process models.
|
|
3
|
+
|
|
4
|
+
This module provides abstract base classes that define the interface
|
|
5
|
+
for all stochastic process models in the library.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from typing import Dict, Any, Optional
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BaseStochasticModel(ABC):
|
|
14
|
+
"""
|
|
15
|
+
Abstract base class for all stochastic process models.
|
|
16
|
+
|
|
17
|
+
This class defines the common interface that all models must implement,
|
|
18
|
+
ensuring consistency and extensibility across different model types.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, **kwargs):
|
|
22
|
+
"""Initialize the model with parameters."""
|
|
23
|
+
self.parameters = kwargs
|
|
24
|
+
self.fitted = False
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def simulate(
|
|
28
|
+
self,
|
|
29
|
+
T: float,
|
|
30
|
+
n_steps: int,
|
|
31
|
+
x0: float = 1.0,
|
|
32
|
+
seed: Optional[int] = None,
|
|
33
|
+
) -> tuple:
|
|
34
|
+
"""
|
|
35
|
+
Simulate a path from the stochastic process.
|
|
36
|
+
|
|
37
|
+
Parameters:
|
|
38
|
+
-----------
|
|
39
|
+
T : float
|
|
40
|
+
Total time horizon
|
|
41
|
+
n_steps : int
|
|
42
|
+
Number of time steps
|
|
43
|
+
x0 : float
|
|
44
|
+
Initial value
|
|
45
|
+
seed : int, optional
|
|
46
|
+
Random seed for reproducibility
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
--------
|
|
50
|
+
tuple
|
|
51
|
+
(times, path) or (times, path, additional_info)
|
|
52
|
+
"""
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
@abstractmethod
|
|
56
|
+
def log_likelihood(self, data: np.ndarray, dt: float) -> float:
|
|
57
|
+
"""
|
|
58
|
+
Calculate the log-likelihood of observed data.
|
|
59
|
+
|
|
60
|
+
Parameters:
|
|
61
|
+
-----------
|
|
62
|
+
data : np.ndarray
|
|
63
|
+
Observed increments
|
|
64
|
+
dt : float
|
|
65
|
+
Time step size
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
--------
|
|
69
|
+
float
|
|
70
|
+
Log-likelihood value
|
|
71
|
+
"""
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
@abstractmethod
|
|
75
|
+
def get_parameter_bounds(self) -> list:
|
|
76
|
+
"""
|
|
77
|
+
Get parameter bounds for optimization.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
--------
|
|
81
|
+
list
|
|
82
|
+
List of (lower, upper) tuples for each parameter
|
|
83
|
+
"""
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
def update_parameters(self, **kwargs):
|
|
87
|
+
"""Update model parameters."""
|
|
88
|
+
self.parameters.update(kwargs)
|
|
89
|
+
self.fitted = False
|
|
90
|
+
|
|
91
|
+
def get_parameters(self) -> Dict[str, Any]:
|
|
92
|
+
"""Get current model parameters."""
|
|
93
|
+
return self.parameters.copy()
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Jump-Diffusion Model Implementation
|
|
3
|
+
|
|
4
|
+
This module implements the jump-diffusion model with a pluggable jump-size
|
|
5
|
+
distribution.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from scipy.stats import norm
|
|
10
|
+
from typing import Dict, Optional, Tuple
|
|
11
|
+
from .base_model import BaseStochasticModel
|
|
12
|
+
from ..distributions import JumpDistribution, SkewNormalJump
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class JumpDiffusionModel(BaseStochasticModel):
|
|
16
|
+
"""
|
|
17
|
+
Jump-diffusion model with a pluggable jump-size distribution.
|
|
18
|
+
|
|
19
|
+
The model follows the SDE:
|
|
20
|
+
dX_t = μ dt + σ dW_t + J_t dN_t
|
|
21
|
+
|
|
22
|
+
Where J_t follows the distribution given by ``jump_distribution``
|
|
23
|
+
(skew-normal by default) and N_t is a Poisson process approximated by
|
|
24
|
+
Bernoulli trials.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
mu: float = 0.05,
|
|
30
|
+
sigma: float = 0.2,
|
|
31
|
+
jump_prob: float = 0.1,
|
|
32
|
+
jump_distribution: Optional[JumpDistribution] = None,
|
|
33
|
+
**jump_params: float,
|
|
34
|
+
):
|
|
35
|
+
r"""
|
|
36
|
+
Initialize the jump-diffusion model.
|
|
37
|
+
|
|
38
|
+
Parameters:
|
|
39
|
+
-----------
|
|
40
|
+
mu : float
|
|
41
|
+
Drift parameter
|
|
42
|
+
sigma : float
|
|
43
|
+
Diffusion volatility
|
|
44
|
+
jump_prob : float
|
|
45
|
+
Probability of jump per time step
|
|
46
|
+
jump_distribution : JumpDistribution, optional
|
|
47
|
+
Distribution used for the jump sizes. Defaults to
|
|
48
|
+
:class:`SkewNormalJump`.
|
|
49
|
+
\*\*jump_params : float
|
|
50
|
+
Values for ``jump_distribution.param_names``. Any name not
|
|
51
|
+
supplied falls back to ``jump_distribution.default_params()``.
|
|
52
|
+
"""
|
|
53
|
+
self.jump_distribution = jump_distribution or SkewNormalJump()
|
|
54
|
+
|
|
55
|
+
resolved_jump_params = self.jump_distribution.default_params()
|
|
56
|
+
resolved_jump_params.update(jump_params)
|
|
57
|
+
|
|
58
|
+
super().__init__(
|
|
59
|
+
mu=mu,
|
|
60
|
+
sigma=sigma,
|
|
61
|
+
jump_prob=jump_prob,
|
|
62
|
+
**resolved_jump_params,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def _jump_params(self) -> Dict[str, float]:
|
|
66
|
+
"""Extract this model's jump-distribution parameter values."""
|
|
67
|
+
return {
|
|
68
|
+
name: self.parameters[name] for name in self.jump_distribution.param_names
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
def simulate(
|
|
72
|
+
self,
|
|
73
|
+
T: float,
|
|
74
|
+
n_steps: int,
|
|
75
|
+
x0: float = 1.0,
|
|
76
|
+
seed: Optional[int] = None,
|
|
77
|
+
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
78
|
+
"""
|
|
79
|
+
Simulate a jump-diffusion path.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
--------
|
|
83
|
+
tuple
|
|
84
|
+
(times, path, jumps) where jumps contains the jump component
|
|
85
|
+
"""
|
|
86
|
+
if seed is not None:
|
|
87
|
+
np.random.seed(seed)
|
|
88
|
+
|
|
89
|
+
dt = T / n_steps
|
|
90
|
+
times = np.linspace(0, T, n_steps + 1)
|
|
91
|
+
path = np.zeros(n_steps + 1)
|
|
92
|
+
path[0] = x0
|
|
93
|
+
|
|
94
|
+
# Generate innovations
|
|
95
|
+
brownian_innovations = np.random.normal(0, 1, n_steps)
|
|
96
|
+
jump_indicators = np.random.binomial(
|
|
97
|
+
1,
|
|
98
|
+
self.parameters["jump_prob"],
|
|
99
|
+
n_steps,
|
|
100
|
+
)
|
|
101
|
+
jump_sizes = self.jump_distribution.rvs(self._jump_params(), size=n_steps)
|
|
102
|
+
|
|
103
|
+
# Construct path
|
|
104
|
+
for i in range(n_steps):
|
|
105
|
+
drift = self.parameters["mu"] * dt
|
|
106
|
+
sigma_term = self.parameters["sigma"] * np.sqrt(dt)
|
|
107
|
+
diffusion = sigma_term * brownian_innovations[i]
|
|
108
|
+
jump = jump_indicators[i] * jump_sizes[i]
|
|
109
|
+
|
|
110
|
+
path[i + 1] = path[i] + drift + diffusion + jump
|
|
111
|
+
|
|
112
|
+
jump_components = jump_indicators * jump_sizes
|
|
113
|
+
|
|
114
|
+
return times, path, jump_components
|
|
115
|
+
|
|
116
|
+
def log_likelihood(self, data: np.ndarray, dt: float) -> float:
|
|
117
|
+
"""
|
|
118
|
+
Calculate log-likelihood of observed increments.
|
|
119
|
+
"""
|
|
120
|
+
increments = data if data.ndim == 1 else np.diff(data)
|
|
121
|
+
|
|
122
|
+
# Calculate mixture densities
|
|
123
|
+
densities = self._mixture_density(increments, dt)
|
|
124
|
+
densities = np.maximum(densities, 1e-300) # Numerical stability
|
|
125
|
+
|
|
126
|
+
return np.sum(np.log(densities))
|
|
127
|
+
|
|
128
|
+
def _mixture_density(self, x: np.ndarray, dt: float) -> np.ndarray:
|
|
129
|
+
"""
|
|
130
|
+
Calculate mixture density for each increment.
|
|
131
|
+
"""
|
|
132
|
+
mu = self.parameters["mu"]
|
|
133
|
+
sigma = self.parameters["sigma"]
|
|
134
|
+
p = self.parameters["jump_prob"]
|
|
135
|
+
|
|
136
|
+
# Pure diffusion component
|
|
137
|
+
diffusion_mean = mu * dt
|
|
138
|
+
diffusion_std = sigma * np.sqrt(dt)
|
|
139
|
+
diffusion_density = norm.pdf(
|
|
140
|
+
x,
|
|
141
|
+
loc=diffusion_mean,
|
|
142
|
+
scale=diffusion_std,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Jump + diffusion component: use a closed form if the jump
|
|
146
|
+
# distribution has one, otherwise fall back to FFT convolution.
|
|
147
|
+
jump_params = self._jump_params()
|
|
148
|
+
jump_diffusion_density = self.jump_distribution.diffusion_convolved_pdf(
|
|
149
|
+
x, jump_params, diffusion_mean, diffusion_std
|
|
150
|
+
)
|
|
151
|
+
if jump_diffusion_density is None:
|
|
152
|
+
jump_diffusion_density = self.jump_distribution.fft_convolved_pdf(
|
|
153
|
+
x, jump_params, diffusion_mean, diffusion_std
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Mixture
|
|
157
|
+
return (1 - p) * diffusion_density + p * jump_diffusion_density
|
|
158
|
+
|
|
159
|
+
def get_parameter_bounds(self) -> list:
|
|
160
|
+
"""Get parameter bounds for optimization."""
|
|
161
|
+
bounds = [
|
|
162
|
+
(None, None), # mu
|
|
163
|
+
(1e-6, None), # sigma > 0
|
|
164
|
+
(1e-6, 1 - 1e-6), # 0 < jump_prob < 1
|
|
165
|
+
]
|
|
166
|
+
jump_bounds = self.jump_distribution.param_bounds()
|
|
167
|
+
bounds += [jump_bounds[name] for name in self.jump_distribution.param_names]
|
|
168
|
+
return bounds
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Run validation experiments from the command line."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
from ..validation import ValidationExperiment
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main() -> None:
|
|
10
|
+
parser = argparse.ArgumentParser(
|
|
11
|
+
description="Run jump-diffusion validation experiment"
|
|
12
|
+
)
|
|
13
|
+
parser.add_argument(
|
|
14
|
+
"--n_sims",
|
|
15
|
+
type=int,
|
|
16
|
+
default=10,
|
|
17
|
+
help="Number of simulations",
|
|
18
|
+
)
|
|
19
|
+
parser.add_argument("--T", type=float, default=1.0, help="Time horizon")
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"--n_steps",
|
|
22
|
+
type=int,
|
|
23
|
+
default=252,
|
|
24
|
+
help="Number of time steps",
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument("--output", type=str, help="Output file for results")
|
|
27
|
+
parser.add_argument("--seed", type=int, default=42, help="Random seed")
|
|
28
|
+
|
|
29
|
+
args = parser.parse_args()
|
|
30
|
+
|
|
31
|
+
true_params = {
|
|
32
|
+
"mu": 0.05,
|
|
33
|
+
"sigma": 0.2,
|
|
34
|
+
"jump_prob": 0.1,
|
|
35
|
+
"jump_scale": 0.15,
|
|
36
|
+
"jump_skew": 2.0,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
print(f"Running validation with {args.n_sims} simulations...")
|
|
40
|
+
experiment = ValidationExperiment(true_params)
|
|
41
|
+
results_df = experiment.run_experiment(
|
|
42
|
+
n_simulations=args.n_sims,
|
|
43
|
+
T=args.T,
|
|
44
|
+
n_steps=args.n_steps,
|
|
45
|
+
seed_base=args.seed,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if len(results_df) == 0:
|
|
49
|
+
print("Validation experiment failed.")
|
|
50
|
+
raise SystemExit(1)
|
|
51
|
+
|
|
52
|
+
analysis = experiment.analyze_results()
|
|
53
|
+
|
|
54
|
+
if args.output:
|
|
55
|
+
output_data = {
|
|
56
|
+
"true_parameters": true_params,
|
|
57
|
+
"experiment_settings": {
|
|
58
|
+
"n_simulations": args.n_sims,
|
|
59
|
+
"T": args.T,
|
|
60
|
+
"n_steps": args.n_steps,
|
|
61
|
+
"seed": args.seed,
|
|
62
|
+
},
|
|
63
|
+
"results": results_df.to_dict("records"),
|
|
64
|
+
"analysis": analysis,
|
|
65
|
+
}
|
|
66
|
+
with open(args.output, "w") as f:
|
|
67
|
+
json.dump(output_data, f, indent=2, default=str)
|
|
68
|
+
print(f"Results saved to {args.output}")
|
|
69
|
+
|
|
70
|
+
experiment.plot_results()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__": # pragma: no cover
|
|
74
|
+
main()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Simulation Tools for Jump-Diffusion Processes
|
|
3
|
+
|
|
4
|
+
This module provides tools for simulating various jump-diffusion processes
|
|
5
|
+
with different jump distributions and arrival mechanisms.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .jump_diffusion_simulator import JumpDiffusionSimulator
|
|
9
|
+
from .base_simulator import BaseSimulator
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"JumpDiffusionSimulator",
|
|
13
|
+
"BaseSimulator",
|
|
14
|
+
]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base simulator class for stochastic processes.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Optional, Tuple
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseSimulator(ABC):
|
|
11
|
+
"""
|
|
12
|
+
Abstract base class for all simulators.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, **kwargs):
|
|
16
|
+
"""Initialize simulator with model parameters."""
|
|
17
|
+
self.parameters = kwargs
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def simulate_path(
|
|
21
|
+
self,
|
|
22
|
+
T: float,
|
|
23
|
+
n_steps: int,
|
|
24
|
+
x0: float = 1.0,
|
|
25
|
+
seed: Optional[int] = None,
|
|
26
|
+
) -> Tuple[np.ndarray, ...]:
|
|
27
|
+
"""
|
|
28
|
+
Simulate a single path.
|
|
29
|
+
|
|
30
|
+
Parameters:
|
|
31
|
+
-----------
|
|
32
|
+
T : float
|
|
33
|
+
Time horizon
|
|
34
|
+
n_steps : int
|
|
35
|
+
Number of time steps
|
|
36
|
+
x0 : float
|
|
37
|
+
Initial value
|
|
38
|
+
seed : int, optional
|
|
39
|
+
Random seed
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
--------
|
|
43
|
+
tuple
|
|
44
|
+
Simulation results
|
|
45
|
+
"""
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def plot_simulation(self, *args, **kwargs):
|
|
50
|
+
"""Plot simulation results."""
|
|
51
|
+
pass
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Jump-Diffusion Simulator Implementation
|
|
3
|
+
|
|
4
|
+
This module contains JumpDiffusionSimulator, which builds on the path
|
|
5
|
+
and likelihood math implemented in :class:`JumpDiffusionModel` and adds
|
|
6
|
+
state-tracking and visualisation on top of it.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import matplotlib.pyplot as plt
|
|
11
|
+
from typing import Optional, Tuple
|
|
12
|
+
from .base_simulator import BaseSimulator
|
|
13
|
+
from ..models.jump_diffusion import JumpDiffusionModel
|
|
14
|
+
from ..distributions import JumpDistribution
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class JumpDiffusionSimulator(BaseSimulator, JumpDiffusionModel):
|
|
18
|
+
"""
|
|
19
|
+
Simulator for jump-diffusion processes with asymmetric jumps.
|
|
20
|
+
|
|
21
|
+
Inherits path generation from :class:`JumpDiffusionModel` and adds
|
|
22
|
+
state-tracking (for repeated plotting) and visualisation on top.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
mu: float = 0.05,
|
|
28
|
+
sigma: float = 0.2,
|
|
29
|
+
jump_prob: float = 0.1,
|
|
30
|
+
jump_distribution: Optional[JumpDistribution] = None,
|
|
31
|
+
**jump_params: float,
|
|
32
|
+
):
|
|
33
|
+
r"""
|
|
34
|
+
Initialize the jump-diffusion simulator.
|
|
35
|
+
|
|
36
|
+
Parameters:
|
|
37
|
+
-----------
|
|
38
|
+
mu : float
|
|
39
|
+
Drift parameter
|
|
40
|
+
sigma : float
|
|
41
|
+
Diffusion volatility
|
|
42
|
+
jump_prob : float
|
|
43
|
+
Probability of jump per period
|
|
44
|
+
jump_distribution : JumpDistribution, optional
|
|
45
|
+
Distribution used for the jump sizes. Defaults to
|
|
46
|
+
:class:`SkewNormalJump`.
|
|
47
|
+
\*\*jump_params : float
|
|
48
|
+
Values for ``jump_distribution.param_names``, e.g.
|
|
49
|
+
``jump_scale``/``jump_skew`` for the default skew-normal
|
|
50
|
+
distribution. Any name not supplied falls back to
|
|
51
|
+
``jump_distribution.default_params()``.
|
|
52
|
+
"""
|
|
53
|
+
JumpDiffusionModel.__init__(
|
|
54
|
+
self,
|
|
55
|
+
mu=mu,
|
|
56
|
+
sigma=sigma,
|
|
57
|
+
jump_prob=jump_prob,
|
|
58
|
+
jump_distribution=jump_distribution,
|
|
59
|
+
**jump_params,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Store last simulation results
|
|
63
|
+
self.last_path: Optional[np.ndarray] = None
|
|
64
|
+
self.last_jumps: Optional[np.ndarray] = None
|
|
65
|
+
self.last_jump_times: Optional[np.ndarray] = None
|
|
66
|
+
|
|
67
|
+
def simulate_path(
|
|
68
|
+
self,
|
|
69
|
+
T: float = 1.0,
|
|
70
|
+
n_steps: int = 252,
|
|
71
|
+
x0: float = 1.0,
|
|
72
|
+
seed: Optional[int] = None,
|
|
73
|
+
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
74
|
+
"""
|
|
75
|
+
Simulate a complete jump-diffusion path.
|
|
76
|
+
|
|
77
|
+
Parameters:
|
|
78
|
+
-----------
|
|
79
|
+
T : float
|
|
80
|
+
Total simulation time
|
|
81
|
+
n_steps : int
|
|
82
|
+
Number of time steps
|
|
83
|
+
x0 : float
|
|
84
|
+
Initial value
|
|
85
|
+
seed : int, optional
|
|
86
|
+
Random seed for reproducibility
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
--------
|
|
90
|
+
tuple
|
|
91
|
+
(times, path, jumps)
|
|
92
|
+
"""
|
|
93
|
+
times, path, jump_components = self.simulate(
|
|
94
|
+
T=T, n_steps=n_steps, x0=x0, seed=seed
|
|
95
|
+
)
|
|
96
|
+
jump_times = np.where(jump_components != 0)[0]
|
|
97
|
+
|
|
98
|
+
# Store for plotting
|
|
99
|
+
self.last_path = path
|
|
100
|
+
self.last_jumps = jump_components
|
|
101
|
+
self.last_jump_times = jump_times
|
|
102
|
+
|
|
103
|
+
return times, path, jump_components
|
|
104
|
+
|
|
105
|
+
def plot_simulation(
|
|
106
|
+
self,
|
|
107
|
+
times=None,
|
|
108
|
+
path=None,
|
|
109
|
+
jumps=None,
|
|
110
|
+
figsize=(12, 8),
|
|
111
|
+
show_theoretical=True,
|
|
112
|
+
):
|
|
113
|
+
"""
|
|
114
|
+
Plot simulation results with comprehensive diagnostics.
|
|
115
|
+
|
|
116
|
+
Parameters
|
|
117
|
+
----------
|
|
118
|
+
times : array-like, optional
|
|
119
|
+
Time grid for the plotted path. When ``None``, the time points from
|
|
120
|
+
the most recent call to :meth:`simulate_path` (uniformly spaced
|
|
121
|
+
between 0 and 1) are used.
|
|
122
|
+
path : array-like, optional
|
|
123
|
+
Simulated trajectory to visualise. Defaults to the last simulated
|
|
124
|
+
path stored by the simulator.
|
|
125
|
+
jumps : array-like, optional
|
|
126
|
+
Jump magnitudes for each step. Required to display jump-related
|
|
127
|
+
diagnostics. If ``None``, the jump component from the last
|
|
128
|
+
simulation is used.
|
|
129
|
+
figsize : tuple, optional
|
|
130
|
+
Figure size passed to :func:`matplotlib.pyplot.subplots`.
|
|
131
|
+
show_theoretical : bool, optional
|
|
132
|
+
When ``True`` (default) and at least one jump was realized,
|
|
133
|
+
overlay the jump distribution's theoretical ``pdf`` (evaluated at
|
|
134
|
+
``self.jump_distribution`` / ``self._jump_params()``) on top of
|
|
135
|
+
the empirical jump-size histogram, so the plot doubles as a
|
|
136
|
+
visual check that simulated jumps match the selected
|
|
137
|
+
distribution's shape.
|
|
138
|
+
|
|
139
|
+
Returns
|
|
140
|
+
-------
|
|
141
|
+
matplotlib.figure.Figure
|
|
142
|
+
The created figure, e.g. for further customization or testing.
|
|
143
|
+
|
|
144
|
+
Notes
|
|
145
|
+
-----
|
|
146
|
+
The method creates a 2×2 grid of plots showing the trajectory with jump
|
|
147
|
+
markers, a stem plot of jump magnitudes, the distribution of increments,
|
|
148
|
+
and the distribution of jump sizes. If no arrays are provided and a
|
|
149
|
+
simulation has been run previously, these plots display the stored
|
|
150
|
+
results. Supplying ``times``, ``path`` and ``jumps`` overrides the stored
|
|
151
|
+
data and visualises the provided arrays instead.
|
|
152
|
+
"""
|
|
153
|
+
if times is None or path is None:
|
|
154
|
+
if self.last_path is None:
|
|
155
|
+
raise ValueError(
|
|
156
|
+
"No simulation data available. Run simulate_path() first."
|
|
157
|
+
)
|
|
158
|
+
times = np.linspace(0, 1, len(self.last_path))
|
|
159
|
+
path = self.last_path
|
|
160
|
+
jumps = self.last_jumps
|
|
161
|
+
jump_times = self.last_jump_times
|
|
162
|
+
else:
|
|
163
|
+
jump_times = (
|
|
164
|
+
np.where(jumps != 0)[0]
|
|
165
|
+
if jumps is not None
|
|
166
|
+
else np.array([], dtype=int)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
fig, axes = plt.subplots(2, 2, figsize=figsize)
|
|
170
|
+
|
|
171
|
+
# Main trajectory plot
|
|
172
|
+
axes[0, 0].plot(times, path, "b-", linewidth=1.5, alpha=0.8)
|
|
173
|
+
if len(jump_times) > 0:
|
|
174
|
+
axes[0, 0].scatter(
|
|
175
|
+
times[1:][jump_times],
|
|
176
|
+
path[1:][jump_times],
|
|
177
|
+
color="red",
|
|
178
|
+
s=30,
|
|
179
|
+
alpha=0.7,
|
|
180
|
+
zorder=5,
|
|
181
|
+
)
|
|
182
|
+
axes[0, 0].set_title("Jump-Diffusion Path")
|
|
183
|
+
axes[0, 0].set_xlabel("Time")
|
|
184
|
+
axes[0, 0].set_ylabel("X(t)")
|
|
185
|
+
axes[0, 0].grid(True, alpha=0.3)
|
|
186
|
+
|
|
187
|
+
# Jump magnitudes
|
|
188
|
+
if len(jump_times) > 0 and jumps is not None:
|
|
189
|
+
jump_magnitudes = jumps[jump_times]
|
|
190
|
+
axes[0, 1].stem(jump_times, jump_magnitudes, basefmt=" ")
|
|
191
|
+
axes[0, 1].set_title(
|
|
192
|
+
f"Detected Jumps (Total: {len(jump_times)})",
|
|
193
|
+
)
|
|
194
|
+
else:
|
|
195
|
+
axes[0, 1].text(
|
|
196
|
+
0.5,
|
|
197
|
+
0.5,
|
|
198
|
+
"No jumps detected",
|
|
199
|
+
ha="center",
|
|
200
|
+
va="center",
|
|
201
|
+
transform=axes[0, 1].transAxes,
|
|
202
|
+
)
|
|
203
|
+
axes[0, 1].set_title("Detected Jumps (Total: 0)")
|
|
204
|
+
axes[0, 1].set_xlabel("Time Index")
|
|
205
|
+
axes[0, 1].set_ylabel("Jump Magnitude")
|
|
206
|
+
axes[0, 1].grid(True, alpha=0.3)
|
|
207
|
+
|
|
208
|
+
# Increment distribution
|
|
209
|
+
increments = np.diff(path)
|
|
210
|
+
axes[1, 0].hist(
|
|
211
|
+
increments,
|
|
212
|
+
bins=50,
|
|
213
|
+
density=True,
|
|
214
|
+
alpha=0.7,
|
|
215
|
+
color="skyblue",
|
|
216
|
+
edgecolor="black",
|
|
217
|
+
)
|
|
218
|
+
axes[1, 0].set_title("Distribution of Increments")
|
|
219
|
+
axes[1, 0].set_xlabel("ΔX")
|
|
220
|
+
axes[1, 0].set_ylabel("Density")
|
|
221
|
+
axes[1, 0].grid(True, alpha=0.3)
|
|
222
|
+
|
|
223
|
+
# Jump size distribution
|
|
224
|
+
if len(jump_times) > 0 and jumps is not None:
|
|
225
|
+
actual_jumps = jumps[jumps != 0]
|
|
226
|
+
axes[1, 1].hist(
|
|
227
|
+
actual_jumps,
|
|
228
|
+
bins=20,
|
|
229
|
+
density=True,
|
|
230
|
+
alpha=0.7,
|
|
231
|
+
color="orange",
|
|
232
|
+
edgecolor="black",
|
|
233
|
+
label="Simulated",
|
|
234
|
+
)
|
|
235
|
+
if show_theoretical:
|
|
236
|
+
x_grid = np.linspace(actual_jumps.min(), actual_jumps.max(), 200)
|
|
237
|
+
theoretical_density = self.jump_distribution.pdf(
|
|
238
|
+
x_grid, self._jump_params()
|
|
239
|
+
)
|
|
240
|
+
axes[1, 1].plot(
|
|
241
|
+
x_grid,
|
|
242
|
+
theoretical_density,
|
|
243
|
+
color="darkred",
|
|
244
|
+
linewidth=2,
|
|
245
|
+
label="Theoretical pdf",
|
|
246
|
+
)
|
|
247
|
+
axes[1, 1].legend()
|
|
248
|
+
axes[1, 1].set_title("Jump Size Distribution")
|
|
249
|
+
else:
|
|
250
|
+
axes[1, 1].text(
|
|
251
|
+
0.5,
|
|
252
|
+
0.5,
|
|
253
|
+
"No jumps to analyze",
|
|
254
|
+
ha="center",
|
|
255
|
+
va="center",
|
|
256
|
+
transform=axes[1, 1].transAxes,
|
|
257
|
+
)
|
|
258
|
+
axes[1, 1].set_xlabel("Jump Size")
|
|
259
|
+
axes[1, 1].set_ylabel("Density")
|
|
260
|
+
axes[1, 1].grid(True, alpha=0.3)
|
|
261
|
+
|
|
262
|
+
plt.tight_layout()
|
|
263
|
+
|
|
264
|
+
return fig
|