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 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