volengine 0.9.0__py3-none-win_amd64.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.
- volengine/__init__.py +37 -0
- volengine/_dcc.py +264 -0
- volengine/_diagnostics.py +51 -0
- volengine/_fit.py +209 -0
- volengine/_forecast.py +65 -0
- volengine/_lib/libvolengine.dll +0 -0
- volengine/_native.py +329 -0
- volengine/_simulation.py +70 -0
- volengine/_spec.py +240 -0
- volengine/api.py +417 -0
- volengine/py.typed +0 -0
- volengine-0.9.0.dist-info/METADATA +6 -0
- volengine-0.9.0.dist-info/RECORD +14 -0
- volengine-0.9.0.dist-info/WHEEL +4 -0
volengine/__init__.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""VolEngine: GARCH estimation over the VolEngine C API."""
|
|
2
|
+
|
|
3
|
+
from .api import (
|
|
4
|
+
CoefTable,
|
|
5
|
+
Context,
|
|
6
|
+
DccFit,
|
|
7
|
+
DccForecast,
|
|
8
|
+
DccSimulation,
|
|
9
|
+
DccSpec,
|
|
10
|
+
DiagnosticResult,
|
|
11
|
+
Fit,
|
|
12
|
+
FitOptions,
|
|
13
|
+
Forecast,
|
|
14
|
+
Nyblom,
|
|
15
|
+
SignBias,
|
|
16
|
+
Simulation,
|
|
17
|
+
Spec,
|
|
18
|
+
VolEngineError,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"CoefTable",
|
|
23
|
+
"Context",
|
|
24
|
+
"DccFit",
|
|
25
|
+
"DccForecast",
|
|
26
|
+
"DccSimulation",
|
|
27
|
+
"DccSpec",
|
|
28
|
+
"DiagnosticResult",
|
|
29
|
+
"Fit",
|
|
30
|
+
"FitOptions",
|
|
31
|
+
"Forecast",
|
|
32
|
+
"Nyblom",
|
|
33
|
+
"SignBias",
|
|
34
|
+
"Simulation",
|
|
35
|
+
"Spec",
|
|
36
|
+
"VolEngineError",
|
|
37
|
+
]
|
volengine/_dcc.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""DCC specifications, batch fitting and immutable native results."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ctypes
|
|
6
|
+
import operator
|
|
7
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
8
|
+
from contextlib import ExitStack
|
|
9
|
+
from dataclasses import KW_ONLY, dataclass, field
|
|
10
|
+
from typing import Any, cast
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
from numpy.typing import ArrayLike, NDArray
|
|
14
|
+
|
|
15
|
+
from ._fit import IC_PER_OBS, IC_TOTAL, SE_KINDS, CoefTable, Fit, status_name
|
|
16
|
+
from ._forecast import DccForecast
|
|
17
|
+
from ._native import ELEMENT_BYTES, Handle, check, create_handle, int32, lib, native_view, open_panel, read_scalar
|
|
18
|
+
from ._spec import code_of, encoded_name, frozen_values
|
|
19
|
+
|
|
20
|
+
MODELS = {"dcc": 0, "adcc": 1, "fdcc": 2, "static": 3}
|
|
21
|
+
COPULAS = {"parametric": 1, "empirical": 2}
|
|
22
|
+
STATIC_METHODS = {"kendall": 1, "ml": 2}
|
|
23
|
+
DISTRIBUTIONS = {"mvnorm": 0, "mvt": 1, "mvlaplace": 2}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class DccSpec:
|
|
28
|
+
"""Second-stage DCC model, using the paper convention.
|
|
29
|
+
|
|
30
|
+
FDCC needs one group label per series, consecutive from 1, and order (1, 1).
|
|
31
|
+
Fixed parameters are excluded from coefficient tables and information criteria.
|
|
32
|
+
|
|
33
|
+
copula="parametric" (each fit's cdf) or "empirical" (ranks) makes it a Copula-DCC on
|
|
34
|
+
the normal or Student scores of the margins; model="static" is a constant-correlation
|
|
35
|
+
copula, with static_method "kendall" (the default) or "ml", whose C1.. are the free
|
|
36
|
+
entries of A in R = cov2cor(A A'), as in rmgarch.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
model: str = "dcc"
|
|
40
|
+
_: KW_ONLY
|
|
41
|
+
order: tuple[int, int] = (1, 1)
|
|
42
|
+
dist: str = "mvnorm"
|
|
43
|
+
groups: Sequence[int] | None = None
|
|
44
|
+
fixed: Mapping[str, float] = field(default_factory=dict)
|
|
45
|
+
start: Mapping[str, float] = field(default_factory=dict)
|
|
46
|
+
copula: str | None = None
|
|
47
|
+
static_method: str | None = None
|
|
48
|
+
|
|
49
|
+
def __post_init__(self) -> None:
|
|
50
|
+
if len(self.order) != 2 or any(type(value) is not int for value in self.order):
|
|
51
|
+
raise ValueError("order must be a pair of integers (p, q)")
|
|
52
|
+
object.__setattr__(self, "order", tuple(self.order))
|
|
53
|
+
if self.groups is not None:
|
|
54
|
+
object.__setattr__(self, "groups", tuple(self.groups))
|
|
55
|
+
object.__setattr__(self, "fixed", frozen_values(self.fixed))
|
|
56
|
+
object.__setattr__(self, "start", frozen_values(self.start))
|
|
57
|
+
self.native_handle().close()
|
|
58
|
+
|
|
59
|
+
def __hash__(self) -> int:
|
|
60
|
+
groups = None
|
|
61
|
+
if self.groups is not None:
|
|
62
|
+
groups = tuple(self.groups)
|
|
63
|
+
return hash((self.model, self.order, self.dist, groups, frozenset(self.fixed.items()), frozenset(self.start.items()),
|
|
64
|
+
self.copula, self.static_method))
|
|
65
|
+
|
|
66
|
+
def native_handle(self) -> Handle:
|
|
67
|
+
model = code_of(MODELS, "model", self.model)
|
|
68
|
+
dist = code_of(DISTRIBUTIONS, "dist", self.dist)
|
|
69
|
+
handle = create_handle(lib.ve_dcc_spec_create, lib.ve_dcc_spec_free, model)
|
|
70
|
+
check(lib.ve_dcc_spec_set_order(handle.pointer, int32(self.order[0], "p"), int32(self.order[1], "q")))
|
|
71
|
+
check(lib.ve_dcc_spec_set_dist(handle.pointer, dist))
|
|
72
|
+
if self.groups is not None:
|
|
73
|
+
groups = (ctypes.c_int32 * len(self.groups))(*(int32(value, "group") for value in self.groups))
|
|
74
|
+
check(lib.ve_dcc_spec_set_groups(handle.pointer, groups, len(groups)))
|
|
75
|
+
for name, value in self.fixed.items():
|
|
76
|
+
check(lib.ve_dcc_spec_set_fixed(handle.pointer, encoded_name(name), value))
|
|
77
|
+
for name, value in self.start.items():
|
|
78
|
+
check(lib.ve_dcc_spec_set_start(handle.pointer, encoded_name(name), value))
|
|
79
|
+
if self.copula is not None:
|
|
80
|
+
check(lib.ve_dcc_spec_set_copula(handle.pointer, code_of(COPULAS, "copula", self.copula)))
|
|
81
|
+
if self.static_method is not None:
|
|
82
|
+
method = code_of(STATIC_METHODS, "static_method", self.static_method)
|
|
83
|
+
check(lib.ve_dcc_spec_set_static_method(handle.pointer, method))
|
|
84
|
+
return handle
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def fit_pointers(fits: Sequence[Fit]) -> Any:
|
|
88
|
+
if any(not isinstance(fit, Fit) for fit in fits):
|
|
89
|
+
raise TypeError("univariate must contain Fit objects")
|
|
90
|
+
return (ctypes.c_void_p * len(fits))(*(fit._handle.pointer for fit in fits))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def fit_dcc(context: Handle, spec: DccSpec | Sequence[DccSpec], univariate: Sequence[Fit], standard_errors: bool) -> list[DccFit]:
|
|
94
|
+
specs: list[DccSpec] = []
|
|
95
|
+
if isinstance(spec, DccSpec):
|
|
96
|
+
specs.append(spec)
|
|
97
|
+
if not isinstance(spec, DccSpec):
|
|
98
|
+
specs = list(spec)
|
|
99
|
+
if not specs:
|
|
100
|
+
raise ValueError("fit_dcc needs at least one spec")
|
|
101
|
+
if any(not isinstance(item, DccSpec) for item in specs):
|
|
102
|
+
raise TypeError("spec must contain DccSpec objects")
|
|
103
|
+
fits = fit_pointers(univariate)
|
|
104
|
+
with ExitStack() as stack:
|
|
105
|
+
handles = [stack.enter_context(item.native_handle()) for item in specs]
|
|
106
|
+
pointers = (ctypes.c_void_p * len(handles))(*(handle.pointer for handle in handles))
|
|
107
|
+
options = stack.enter_context(create_handle(lib.ve_dcc_options_create, lib.ve_dcc_options_free))
|
|
108
|
+
check(lib.ve_dcc_options_set_standard_errors(options.pointer, int(standard_errors)))
|
|
109
|
+
batch = stack.enter_context(create_handle(lib.ve_dcc_batch_create, lib.ve_dcc_batch_free))
|
|
110
|
+
check(lib.ve_dcc_batch_set_specs(batch.pointer, pointers, len(handles)))
|
|
111
|
+
check(lib.ve_dcc_batch_set_fits(batch.pointer, fits, len(univariate)))
|
|
112
|
+
check(lib.ve_dcc_batch_set_options(batch.pointer, options.pointer))
|
|
113
|
+
out = (ctypes.c_void_p * len(specs))()
|
|
114
|
+
check(lib.ve_dcc_fit_batch(context.pointer, batch.pointer, out))
|
|
115
|
+
owned = [Handle(pointer, lib.ve_dcc_fit_free) for pointer in out]
|
|
116
|
+
return [DccFit(handle) for handle in owned]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def future_panel_of(stack: ExitStack, values: ArrayLike | None) -> int | None:
|
|
120
|
+
"""The native panel of one series' regressors of the horizon (T x k, or (T,) for one), None for none."""
|
|
121
|
+
if values is None:
|
|
122
|
+
return None
|
|
123
|
+
array = np.asarray(values, dtype=np.float64)
|
|
124
|
+
if array.ndim == 1:
|
|
125
|
+
array = array[:, None]
|
|
126
|
+
if array.ndim != 2:
|
|
127
|
+
raise ValueError(f"the regressors of a series must be 1D or 2D, got {array.ndim} dimensions")
|
|
128
|
+
if any(stride % ELEMENT_BYTES for stride in array.strides):
|
|
129
|
+
array = np.ascontiguousarray(array)
|
|
130
|
+
return open_panel(stack, array).pointer
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def listed(entries: Sequence[ArrayLike | None] | None) -> list[ArrayLike | None] | None:
|
|
134
|
+
if entries is None:
|
|
135
|
+
return None
|
|
136
|
+
return list(entries)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def entry_of(entries: list[ArrayLike | None] | None, series: int) -> ArrayLike | None:
|
|
140
|
+
if entries is None:
|
|
141
|
+
return None
|
|
142
|
+
return entries[series]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def open_future(
|
|
146
|
+
stack: ExitStack,
|
|
147
|
+
horizon: int,
|
|
148
|
+
mxreg: Sequence[ArrayLike | None] | None,
|
|
149
|
+
vxreg: Sequence[ArrayLike | None] | None,
|
|
150
|
+
) -> int | None:
|
|
151
|
+
"""The native regressors of the horizon, one entry per series, or None when no series has any."""
|
|
152
|
+
if mxreg is None and vxreg is None:
|
|
153
|
+
return None
|
|
154
|
+
mean = listed(mxreg)
|
|
155
|
+
variance = listed(vxreg)
|
|
156
|
+
if mean is not None and variance is not None and len(mean) != len(variance):
|
|
157
|
+
raise ValueError(f"mxreg and vxreg must have one entry per series, got {len(mean)} and {len(variance)}")
|
|
158
|
+
entries = mean if mean is not None else variance
|
|
159
|
+
assert entries is not None
|
|
160
|
+
future = stack.enter_context(create_handle(lib.ve_dcc_future_create, lib.ve_dcc_future_free, len(entries), horizon))
|
|
161
|
+
for series in range(len(entries)):
|
|
162
|
+
mean_panel = future_panel_of(stack, entry_of(mean, series))
|
|
163
|
+
variance_panel = future_panel_of(stack, entry_of(variance, series))
|
|
164
|
+
check(lib.ve_dcc_future_set(future.pointer, series, mean_panel, variance_panel))
|
|
165
|
+
return future.pointer
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class DccFit:
|
|
169
|
+
"""One DCC result. R and H are read-only views that keep the result alive.
|
|
170
|
+
|
|
171
|
+
H is computed once on first access. Standard errors can be added to a new
|
|
172
|
+
result using the original ordered univariate fits; regressor fits are unsupported.
|
|
173
|
+
|
|
174
|
+
A fit made by `Context.fit_dcc` keeps the final state of each series and of its correlation recursion, so
|
|
175
|
+
`forecast` and `Context.simulate` do not need the univariate `Fit` objects, which `standard_errors` does need
|
|
176
|
+
because it refilters the whole sample.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
def __init__(self, handle: Handle) -> None:
|
|
180
|
+
self._handle = handle
|
|
181
|
+
self.status = status_name(read_scalar(ctypes.c_int32, lib.ve_dcc_fit_status, handle.pointer))
|
|
182
|
+
self.nobs = read_scalar(ctypes.c_size_t, lib.ve_dcc_fit_nobs, handle.pointer)
|
|
183
|
+
self.series = read_scalar(ctypes.c_size_t, lib.ve_dcc_fit_series, handle.pointer)
|
|
184
|
+
count = read_scalar(ctypes.c_size_t, lib.ve_dcc_fit_param_count, handle.pointer)
|
|
185
|
+
self.names = tuple(self._param_name(index) for index in range(count))
|
|
186
|
+
|
|
187
|
+
def _param_name(self, index: int) -> str:
|
|
188
|
+
name = read_scalar(ctypes.c_char_p, lib.ve_dcc_fit_param_name, self._handle.pointer, index)
|
|
189
|
+
return cast(bytes, name).decode("utf-8")
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def R(self) -> NDArray[np.float64]:
|
|
193
|
+
return native_view(self, lib.ve_dcc_fit_correlation, self._handle.pointer).reshape(-1, self.series, self.series)
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def H(self) -> NDArray[np.float64]:
|
|
197
|
+
return native_view(self, lib.ve_dcc_fit_covariance, self._handle.pointer).reshape(-1, self.series, self.series)
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def loglik(self) -> float:
|
|
201
|
+
return cast(float, read_scalar(ctypes.c_double, lib.ve_dcc_fit_loglik, self._handle.pointer))
|
|
202
|
+
|
|
203
|
+
def aic(self, *, per_obs: bool = True) -> float:
|
|
204
|
+
return self._criterion(0, per_obs)
|
|
205
|
+
|
|
206
|
+
def bic(self, *, per_obs: bool = True) -> float:
|
|
207
|
+
return self._criterion(1, per_obs)
|
|
208
|
+
|
|
209
|
+
def hqic(self, *, per_obs: bool = True) -> float:
|
|
210
|
+
return self._criterion(2, per_obs)
|
|
211
|
+
|
|
212
|
+
def sic(self, *, per_obs: bool = True) -> float:
|
|
213
|
+
return self._criterion(3, per_obs)
|
|
214
|
+
|
|
215
|
+
def _criterion(self, kind: int, per_obs: bool) -> float:
|
|
216
|
+
scale = IC_PER_OBS if per_obs else IC_TOTAL
|
|
217
|
+
return cast(float, read_scalar(ctypes.c_double, lib.ve_dcc_fit_criterion, self._handle.pointer, kind, scale))
|
|
218
|
+
|
|
219
|
+
def coef(self, se: str = "hessian") -> CoefTable:
|
|
220
|
+
kind = code_of(SE_KINDS, "se", se)
|
|
221
|
+
return CoefTable(
|
|
222
|
+
self.names,
|
|
223
|
+
self._values(lib.ve_dcc_fit_estimates),
|
|
224
|
+
self._values(lib.ve_dcc_fit_std_errors, kind),
|
|
225
|
+
self._values(lib.ve_dcc_fit_t_values, kind),
|
|
226
|
+
self._values(lib.ve_dcc_fit_p_values, kind),
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
def _values(self, function: Callable[..., int], *args: Any) -> NDArray[np.float64]:
|
|
230
|
+
out = np.empty(len(self.names), dtype=np.float64)
|
|
231
|
+
check(function(self._handle.pointer, *args, out.ctypes.data))
|
|
232
|
+
return out
|
|
233
|
+
|
|
234
|
+
def forecast(
|
|
235
|
+
self,
|
|
236
|
+
horizon: int,
|
|
237
|
+
*,
|
|
238
|
+
mxreg: Sequence[ArrayLike | None] | None = None,
|
|
239
|
+
vxreg: Sequence[ArrayLike | None] | None = None,
|
|
240
|
+
) -> DccForecast:
|
|
241
|
+
"""R, H, sigma and mean for h = 1..horizon after the last observation, as rmgarch's dccforecast (n.roll = 0).
|
|
242
|
+
|
|
243
|
+
`mxreg` and `vxreg` hold, for each series, the regressors of the horizon ((horizon, k), or (horizon,) for
|
|
244
|
+
one) or None for a series without; a series' regressors must match its spec (VolEngineError naming the
|
|
245
|
+
series, from 1, otherwise). A fit with no finite point raises VolEngineError. The fit keeps what it needs:
|
|
246
|
+
the univariate `Fit` objects can be freed. See `DccForecast` for the rules of R.
|
|
247
|
+
"""
|
|
248
|
+
horizon = operator.index(horizon)
|
|
249
|
+
if horizon < 1:
|
|
250
|
+
raise ValueError(f"horizon must be at least 1, got {horizon}")
|
|
251
|
+
with ExitStack() as stack:
|
|
252
|
+
future = open_future(stack, horizon, mxreg, vxreg)
|
|
253
|
+
handle = create_handle(
|
|
254
|
+
lib.ve_dcc_forecast_create, lib.ve_dcc_forecast_free, self._handle.pointer, horizon, future
|
|
255
|
+
)
|
|
256
|
+
return DccForecast(handle)
|
|
257
|
+
|
|
258
|
+
def standard_errors(self, univariate: Sequence[Fit]) -> DccFit:
|
|
259
|
+
fits = fit_pointers(univariate)
|
|
260
|
+
with create_handle(lib.ve_context_create, lib.ve_context_free) as context:
|
|
261
|
+
return DccFit(create_handle(
|
|
262
|
+
lib.ve_dcc_fit_standard_errors, lib.ve_dcc_fit_free,
|
|
263
|
+
context.pointer, self._handle.pointer, fits, len(univariate),
|
|
264
|
+
))
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Diagnostic result types and native array readers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import operator
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import Any, NamedTuple
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from ._native import check, lib
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DiagnosticResult(NamedTuple):
|
|
15
|
+
statistic: float
|
|
16
|
+
p_value: float
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SignBias(NamedTuple):
|
|
20
|
+
sign: DiagnosticResult
|
|
21
|
+
negative: DiagnosticResult
|
|
22
|
+
positive: DiagnosticResult
|
|
23
|
+
joint: DiagnosticResult
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Nyblom(NamedTuple):
|
|
27
|
+
individual: dict[str, float]
|
|
28
|
+
joint: float
|
|
29
|
+
individual_critical: tuple[float, float, float]
|
|
30
|
+
joint_critical: tuple[float, float, float] | None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def size_argument(value: int, name: str) -> int:
|
|
34
|
+
value = operator.index(value)
|
|
35
|
+
if not 0 <= value < 2**64:
|
|
36
|
+
raise ValueError(f"{name} must be an integer in [0, 2**64), got {value}")
|
|
37
|
+
return value
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def read_test(function: Callable[..., int], *args: Any) -> DiagnosticResult:
|
|
41
|
+
out = np.empty(2, dtype=np.float64)
|
|
42
|
+
check(function(*args, out.ctypes.data))
|
|
43
|
+
return DiagnosticResult(float(out[0]), float(out[1]))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def critical_values(parameters: int) -> tuple[float, float, float] | None:
|
|
47
|
+
if not 1 <= parameters <= 20:
|
|
48
|
+
return None
|
|
49
|
+
out = np.empty(3, dtype=np.float64)
|
|
50
|
+
check(lib.ve_nyblom_critical(parameters, out.ctypes.data))
|
|
51
|
+
return float(out[0]), float(out[1]), float(out[2])
|
volengine/_fit.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Fit results over ve_fit handles: coefficient tables, read-only views and information criteria."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ctypes
|
|
6
|
+
import operator
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from contextlib import ExitStack
|
|
9
|
+
from typing import Any, NamedTuple, cast
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
from numpy.typing import ArrayLike, NDArray
|
|
13
|
+
|
|
14
|
+
from ._diagnostics import DiagnosticResult, Nyblom, SignBias, critical_values, read_test, size_argument
|
|
15
|
+
from ._forecast import Forecast
|
|
16
|
+
from ._native import (
|
|
17
|
+
ELEMENT_BYTES,
|
|
18
|
+
Handle,
|
|
19
|
+
check,
|
|
20
|
+
create_handle,
|
|
21
|
+
lib,
|
|
22
|
+
native_view,
|
|
23
|
+
open_panel,
|
|
24
|
+
read_scalar,
|
|
25
|
+
)
|
|
26
|
+
from ._spec import code_of
|
|
27
|
+
|
|
28
|
+
STATUS_NAMES = {0: "converged", 1: "max_iter", 2: "failed", 3: "insufficient_data"}
|
|
29
|
+
SE_KINDS = {"hessian": 0, "robust": 1}
|
|
30
|
+
IC_PER_OBS = 0
|
|
31
|
+
IC_TOTAL = 1
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def status_name(code: int) -> str:
|
|
35
|
+
"""The name of a VE_FIT_* code; a code newer than this package is "unknown(<code>)"."""
|
|
36
|
+
return STATUS_NAMES.get(code, f"unknown({code})")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class CoefTable(NamedTuple):
|
|
40
|
+
names: tuple[str, ...]
|
|
41
|
+
estimate: NDArray[np.float64]
|
|
42
|
+
std_error: NDArray[np.float64]
|
|
43
|
+
t_value: NDArray[np.float64]
|
|
44
|
+
p_value: NDArray[np.float64]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Fit:
|
|
48
|
+
"""One fitted model. Owns its native handle; the arrays it returns keep it alive.
|
|
49
|
+
|
|
50
|
+
`status` is "converged", "max_iter", "failed" or "insufficient_data" ("unknown(<code>)" for a status
|
|
51
|
+
added by a newer native library). A "failed" fit may carry the values of the best finite point the
|
|
52
|
+
optimizer reached, or NaN (with empty series) when it reached none. An "insufficient_data" fit is
|
|
53
|
+
always NaN with empty series. Check `status` before using the values.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
status: str
|
|
57
|
+
nobs: int
|
|
58
|
+
names: tuple[str, ...]
|
|
59
|
+
|
|
60
|
+
def __init__(self, handle: Handle) -> None:
|
|
61
|
+
self._handle = handle
|
|
62
|
+
pointer = handle.pointer
|
|
63
|
+
self.status = status_name(read_scalar(ctypes.c_int32, lib.ve_fit_status, pointer))
|
|
64
|
+
self.nobs = read_scalar(ctypes.c_size_t, lib.ve_fit_nobs, pointer)
|
|
65
|
+
count = read_scalar(ctypes.c_size_t, lib.ve_fit_param_count, pointer)
|
|
66
|
+
self.names = tuple(self._param_name(index) for index in range(count))
|
|
67
|
+
|
|
68
|
+
def _param_name(self, index: int) -> str:
|
|
69
|
+
text = read_scalar(ctypes.c_char_p, lib.ve_fit_param_name, self._handle.pointer, index)
|
|
70
|
+
return cast(bytes, text).decode("utf-8")
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def sigma(self) -> NDArray[np.float64]:
|
|
74
|
+
return self._view(lib.ve_fit_sigma)
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def residuals(self) -> NDArray[np.float64]:
|
|
78
|
+
return self._view(lib.ve_fit_residuals)
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def std_residuals(self) -> NDArray[np.float64]:
|
|
82
|
+
return self._view(lib.ve_fit_std_residuals)
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def fitted(self) -> NDArray[np.float64]:
|
|
86
|
+
return self._view(lib.ve_fit_fitted)
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def loglik(self) -> float:
|
|
90
|
+
return cast(float, read_scalar(ctypes.c_double, lib.ve_loglik, self._handle.pointer))
|
|
91
|
+
|
|
92
|
+
def aic(self, *, per_obs: bool = True) -> float:
|
|
93
|
+
return self._criterion(lib.ve_aic, per_obs)
|
|
94
|
+
|
|
95
|
+
def bic(self, *, per_obs: bool = True) -> float:
|
|
96
|
+
return self._criterion(lib.ve_bic, per_obs)
|
|
97
|
+
|
|
98
|
+
def hqic(self, *, per_obs: bool = True) -> float:
|
|
99
|
+
return self._criterion(lib.ve_hqic, per_obs)
|
|
100
|
+
|
|
101
|
+
def sic(self, *, per_obs: bool = True) -> float:
|
|
102
|
+
return self._criterion(lib.ve_sic, per_obs)
|
|
103
|
+
|
|
104
|
+
def _criterion(self, function: Callable[..., int], per_obs: bool) -> float:
|
|
105
|
+
scale = IC_PER_OBS if per_obs else IC_TOTAL
|
|
106
|
+
return cast(float, read_scalar(ctypes.c_double, function, self._handle.pointer, scale))
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def estimate(self) -> NDArray[np.float64]:
|
|
110
|
+
"""The estimates, in `names` order; also on a fit made with FitOptions(standard_errors=False)."""
|
|
111
|
+
return self._values(lib.ve_fit_estimates)
|
|
112
|
+
|
|
113
|
+
def coef(self, se: str = "hessian") -> CoefTable:
|
|
114
|
+
kind = code_of(SE_KINDS, "se", se)
|
|
115
|
+
return CoefTable(
|
|
116
|
+
self.names,
|
|
117
|
+
self._values(lib.ve_fit_estimates),
|
|
118
|
+
self._values(lib.ve_fit_std_errors, kind),
|
|
119
|
+
self._values(lib.ve_fit_t_values, kind),
|
|
120
|
+
self._values(lib.ve_fit_p_values, kind),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def _values(self, function: Callable[..., int], *args: Any) -> NDArray[np.float64]:
|
|
124
|
+
out = np.empty(len(self.names), dtype=np.float64)
|
|
125
|
+
check(function(self._handle.pointer, *args, out.ctypes.data))
|
|
126
|
+
return out
|
|
127
|
+
|
|
128
|
+
def _view(self, function: Callable[..., int]) -> NDArray[np.float64]:
|
|
129
|
+
return native_view(self, function, self._handle.pointer)
|
|
130
|
+
|
|
131
|
+
def ljung_box(self, lag: int, *, squared: bool = False, fitdf: int = 0) -> DiagnosticResult:
|
|
132
|
+
"""Weighted Ljung-Box. show() uses lags 1, max(2, 3*d-1), max(5, 5*d-1).
|
|
133
|
+
|
|
134
|
+
d is the ARMA order sum (residuals) or p+q (squares). fitdf is forced to zero for squares.
|
|
135
|
+
Requires 1 <= lag < nobs and lag >= 3*fitdf-1. Constant autocorrelations return NaN.
|
|
136
|
+
"""
|
|
137
|
+
lag = size_argument(lag, "lag")
|
|
138
|
+
fitdf = size_argument(fitdf, "fitdf")
|
|
139
|
+
return read_test(lib.ve_fit_ljung_box, self._handle.pointer, int(squared), lag, fitdf)
|
|
140
|
+
|
|
141
|
+
def arch_lm(self, lag: int, *, fitdf: int) -> DiagnosticResult:
|
|
142
|
+
"""Weighted ARCH-LM. show() uses fitdf=p+q and lags fitdf+1, fitdf+3, fitdf+5."""
|
|
143
|
+
lag = size_argument(lag, "lag")
|
|
144
|
+
fitdf = size_argument(fitdf, "fitdf")
|
|
145
|
+
return read_test(lib.ve_fit_arch_lm, self._handle.pointer, lag, fitdf)
|
|
146
|
+
|
|
147
|
+
def nyblom(self) -> Nyblom:
|
|
148
|
+
"""Parameter stability, with Hansen critical values at 10%, 5%, 1%."""
|
|
149
|
+
values = np.empty(len(self.names), dtype=np.float64)
|
|
150
|
+
estimated = np.empty(len(self.names), dtype=np.int32)
|
|
151
|
+
joint = ctypes.c_double()
|
|
152
|
+
check(lib.ve_fit_nyblom(self._handle.pointer, values.ctypes.data, ctypes.byref(joint), estimated.ctypes.data))
|
|
153
|
+
roles = zip(self.names, values, estimated, strict=True)
|
|
154
|
+
individual = {name: float(value) for name, value, flag in roles if flag}
|
|
155
|
+
individual_critical = critical_values(1)
|
|
156
|
+
assert individual_critical is not None
|
|
157
|
+
return Nyblom(individual, joint.value, individual_critical, critical_values(len(individual)))
|
|
158
|
+
|
|
159
|
+
def sign_bias(self) -> SignBias:
|
|
160
|
+
"""Engle-Ng tests with lagged raw residuals, as in rugarch."""
|
|
161
|
+
out = np.empty(8, dtype=np.float64)
|
|
162
|
+
check(lib.ve_fit_sign_bias(self._handle.pointer, out.ctypes.data))
|
|
163
|
+
tests = [DiagnosticResult(float(out[i]), float(out[i + 1])) for i in range(0, 8, 2)]
|
|
164
|
+
return SignBias(*tests)
|
|
165
|
+
|
|
166
|
+
def gof(self, groups: int = 20) -> DiagnosticResult:
|
|
167
|
+
"""Adjusted Pearson goodness of fit, 2 <= groups <= nobs. show() uses 20, 30, 40 and 50 groups."""
|
|
168
|
+
return read_test(lib.ve_fit_gof, self._handle.pointer, size_argument(groups, "groups"))
|
|
169
|
+
|
|
170
|
+
def forecast(self, horizon: int, *, mxreg: ArrayLike | None = None, vxreg: ArrayLike | None = None) -> Forecast:
|
|
171
|
+
"""sigma and mean for h = 1..horizon after the last observation, as rugarch's ugarchforecast (n.roll = 0).
|
|
172
|
+
|
|
173
|
+
`mxreg` and `vxreg` are the regressors of the horizon, (horizon, k) for a spec with k of them ((horizon,)
|
|
174
|
+
when k = 1), and must be absent when the spec has none. A fit with no finite point raises VolEngineError.
|
|
175
|
+
See `Forecast` for what `sigma` means in eGARCH, apARCH and fGARCH.
|
|
176
|
+
"""
|
|
177
|
+
horizon = operator.index(horizon)
|
|
178
|
+
if horizon < 1:
|
|
179
|
+
raise ValueError(f"horizon must be at least 1, got {horizon}")
|
|
180
|
+
with ExitStack() as stack:
|
|
181
|
+
mean = self._future(stack, mxreg, "mxreg", horizon)
|
|
182
|
+
variance = self._future(stack, vxreg, "vxreg", horizon)
|
|
183
|
+
handle = create_handle(
|
|
184
|
+
lib.ve_forecast_create, lib.ve_forecast_free, self._handle.pointer, horizon, mean, variance
|
|
185
|
+
)
|
|
186
|
+
return Forecast(handle)
|
|
187
|
+
|
|
188
|
+
def _future(self, stack: ExitStack, values: ArrayLike | None, name: str, horizon: int) -> int | None:
|
|
189
|
+
"""The native panel of the regressors of the horizon, or None for a spec without them."""
|
|
190
|
+
count = sum(1 for parameter in self.names if parameter.startswith(name))
|
|
191
|
+
return future_panel(stack, values, name, count, horizon)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def future_panel(stack: ExitStack, values: ArrayLike | None, name: str, count: int, horizon: int) -> int | None:
|
|
195
|
+
"""The native panel of `count` regressors over the horizon, or None when the spec has none."""
|
|
196
|
+
if count == 0:
|
|
197
|
+
if values is not None:
|
|
198
|
+
raise ValueError(f"{name} was given but the spec has no {name}")
|
|
199
|
+
return None
|
|
200
|
+
if values is None:
|
|
201
|
+
raise ValueError(f"the spec has {name} = {count}, so {name} is required")
|
|
202
|
+
array = np.asarray(values, dtype=np.float64)
|
|
203
|
+
if array.ndim == 1 and count == 1:
|
|
204
|
+
array = array[:, None]
|
|
205
|
+
if array.shape != (horizon, count):
|
|
206
|
+
raise ValueError(f"{name} must have shape {(horizon, count)}, got {array.shape}")
|
|
207
|
+
if any(stride % ELEMENT_BYTES for stride in array.strides):
|
|
208
|
+
array = np.ascontiguousarray(array)
|
|
209
|
+
return open_panel(stack, array).pointer
|
volengine/_forecast.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Forecasts over ve_forecast handles."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ctypes
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from numpy.typing import NDArray
|
|
9
|
+
|
|
10
|
+
from ._native import Handle, lib, native_view, read_scalar
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DccForecast:
|
|
14
|
+
"""The forecast of a DCC fit for h = 1..horizon after its last observation, as rmgarch's dccforecast.
|
|
15
|
+
|
|
16
|
+
`R` and `H` are read-only arrays of shape (horizon, N, N), `sigma` and `mean` of shape (horizon, N). `sigma`
|
|
17
|
+
and `mean` are those of each series' own `Fit.forecast`. `R[0]` is the exact recursion one step after the last
|
|
18
|
+
observation and, for h >= 2, R[h - 1] = (1 - k^(h-1)) Rbar + k^(h-1) R[0] with k the total persistence sum(a) +
|
|
19
|
+
sum(b) (the aDCC g does not enter; the FDCC takes k per element; a static copula keeps its R) and Rbar the
|
|
20
|
+
correlation of Qbar. `H = D R D` with D = diag(sigma). The forecast does not depend on its fit.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, handle: Handle) -> None:
|
|
24
|
+
self._handle = handle
|
|
25
|
+
self._series = read_scalar(ctypes.c_size_t, lib.ve_dcc_forecast_series, handle.pointer)
|
|
26
|
+
self._horizon = read_scalar(ctypes.c_size_t, lib.ve_dcc_forecast_horizon, handle.pointer)
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def R(self) -> NDArray[np.float64]:
|
|
30
|
+
view = native_view(self, lib.ve_dcc_forecast_correlation, self._handle.pointer)
|
|
31
|
+
return view.reshape(self._horizon, self._series, self._series)
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def H(self) -> NDArray[np.float64]:
|
|
35
|
+
view = native_view(self, lib.ve_dcc_forecast_covariance, self._handle.pointer)
|
|
36
|
+
return view.reshape(self._horizon, self._series, self._series)
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def sigma(self) -> NDArray[np.float64]:
|
|
40
|
+
return native_view(self, lib.ve_dcc_forecast_sigma, self._handle.pointer).reshape(self._horizon, self._series)
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def mean(self) -> NDArray[np.float64]:
|
|
44
|
+
return native_view(self, lib.ve_dcc_forecast_mean, self._handle.pointer).reshape(self._horizon, self._series)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Forecast:
|
|
48
|
+
"""The forecast of a fit for h = 1..horizon after its last observation, as rugarch's ugarchforecast.
|
|
49
|
+
|
|
50
|
+
`sigma` and `mean` are read-only arrays of shape (horizon,). `sigma` is rugarch's sigmaFor: the square root of
|
|
51
|
+
the expected variance in sGARCH, iGARCH, gjrGARCH and csGARCH, but exp(E[log sigma2] / 2) in eGARCH and
|
|
52
|
+
E[sigma^delta]^(1/delta) in apARCH and fGARCH, which for h > 1 is not sqrt(E[sigma2]). `mean` is the
|
|
53
|
+
conditional mean (rugarch's seriesFor). The forecast does not depend on its fit.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, handle: Handle) -> None:
|
|
57
|
+
self._handle = handle
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def sigma(self) -> NDArray[np.float64]:
|
|
61
|
+
return native_view(self, lib.ve_forecast_sigma, self._handle.pointer)
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def mean(self) -> NDArray[np.float64]:
|
|
65
|
+
return native_view(self, lib.ve_forecast_mean, self._handle.pointer)
|
|
Binary file
|