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/_native.py ADDED
@@ -0,0 +1,329 @@
1
+ """ctypes bindings that mirror include/volengine/volengine.h."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ctypes
6
+ import os
7
+ from collections.abc import Callable, Mapping
8
+ from contextlib import ExitStack
9
+ from pathlib import Path
10
+ from types import TracebackType
11
+ from typing import Any, cast
12
+
13
+ import numpy as np
14
+ from numpy.typing import NDArray
15
+
16
+ VE_ABI_VERSION = 0
17
+ VE_INTERNAL_ERROR = 5
18
+ INT32_MIN = -(2**31)
19
+ INT32_MAX = 2**31 - 1
20
+ LIBRARY_ENV = "VOLENGINE_LIB"
21
+ LIBRARY_DIRECTORY = Path(__file__).resolve().parent / "_lib"
22
+ LIBRARY_NAMES = ("libvolengine.dll", "volengine.dll", "libvolengine.so", "libvolengine.dylib")
23
+
24
+ _HANDLE = ctypes.c_void_p
25
+ _OUT_HANDLE = ctypes.POINTER(ctypes.c_void_p)
26
+ _STATUS = ctypes.c_int32
27
+ _SIZE = ctypes.c_size_t
28
+ _I32 = ctypes.c_int32
29
+ _DOUBLE = ctypes.c_double
30
+ _OUT_DOUBLE = ctypes.POINTER(ctypes.c_double)
31
+ _OUT_SIZE = ctypes.POINTER(ctypes.c_size_t)
32
+ _OUT_I32 = ctypes.POINTER(ctypes.c_int32)
33
+ _OUT_VIEW = ctypes.POINTER(ctypes.POINTER(ctypes.c_double))
34
+ _OUT_TEXT = ctypes.POINTER(ctypes.c_char_p)
35
+
36
+ _SCALAR_CRITERION = [_HANDLE, _I32, _OUT_DOUBLE]
37
+ _VIEW = [_HANDLE, _OUT_VIEW, _OUT_SIZE]
38
+ _SE_VALUES = [_HANDLE, _I32, _HANDLE]
39
+
40
+ PROTOTYPES: dict[str, tuple[Any, list[Any]]] = {
41
+ "ve_dcc_spec_create": (_STATUS, [_I32, _OUT_HANDLE]),
42
+ "ve_dcc_spec_set_order": (_STATUS, [_HANDLE, _I32, _I32]),
43
+ "ve_dcc_spec_set_dist": (_STATUS, [_HANDLE, _I32]),
44
+ "ve_dcc_spec_set_groups": (_STATUS, [_HANDLE, ctypes.POINTER(_I32), _SIZE]),
45
+ "ve_dcc_spec_set_fixed": (_STATUS, [_HANDLE, ctypes.c_char_p, _DOUBLE]),
46
+ "ve_dcc_spec_set_start": (_STATUS, [_HANDLE, ctypes.c_char_p, _DOUBLE]),
47
+ "ve_dcc_spec_set_copula": (_STATUS, [_HANDLE, _I32]),
48
+ "ve_dcc_spec_set_static_method": (_STATUS, [_HANDLE, _I32]),
49
+ "ve_dcc_options_create": (_STATUS, [_OUT_HANDLE]),
50
+ "ve_dcc_options_set_standard_errors": (_STATUS, [_HANDLE, _I32]),
51
+ "ve_dcc_batch_create": (_STATUS, [_OUT_HANDLE]),
52
+ "ve_dcc_batch_set_specs": (_STATUS, [_HANDLE, ctypes.POINTER(_HANDLE), _SIZE]),
53
+ "ve_dcc_batch_set_fits": (_STATUS, [_HANDLE, ctypes.POINTER(_HANDLE), _SIZE]),
54
+ "ve_dcc_batch_set_options": (_STATUS, [_HANDLE, _HANDLE]),
55
+ "ve_dcc_fit_batch": (_STATUS, [_HANDLE, _HANDLE, ctypes.POINTER(_HANDLE)]),
56
+ "ve_dcc_fit_standard_errors": (_STATUS, [_HANDLE, _HANDLE, ctypes.POINTER(_HANDLE), _SIZE, _OUT_HANDLE]),
57
+ "ve_dcc_fit_status": (_STATUS, [_HANDLE, _OUT_I32]),
58
+ "ve_dcc_fit_nobs": (_STATUS, [_HANDLE, _OUT_SIZE]),
59
+ "ve_dcc_fit_series": (_STATUS, [_HANDLE, _OUT_SIZE]),
60
+ "ve_dcc_fit_param_count": (_STATUS, [_HANDLE, _OUT_SIZE]),
61
+ "ve_dcc_fit_param_name": (_STATUS, [_HANDLE, _SIZE, _OUT_TEXT]),
62
+ "ve_dcc_fit_estimates": (_STATUS, [_HANDLE, _HANDLE]),
63
+ "ve_dcc_fit_std_errors": (_STATUS, _SE_VALUES),
64
+ "ve_dcc_fit_t_values": (_STATUS, _SE_VALUES),
65
+ "ve_dcc_fit_p_values": (_STATUS, _SE_VALUES),
66
+ "ve_dcc_fit_loglik": (_STATUS, [_HANDLE, _OUT_DOUBLE]),
67
+ "ve_dcc_fit_criterion": (_STATUS, [_HANDLE, _I32, _I32, _OUT_DOUBLE]),
68
+ "ve_dcc_fit_correlation": (_STATUS, _VIEW),
69
+ "ve_dcc_fit_covariance": (_STATUS, _VIEW),
70
+ "ve_dcc_spec_free": (None, [_HANDLE]),
71
+ "ve_dcc_options_free": (None, [_HANDLE]),
72
+ "ve_dcc_batch_free": (None, [_HANDLE]),
73
+ "ve_dcc_fit_free": (None, [_HANDLE]),
74
+ "ve_fit_ljung_box": (_STATUS, [_HANDLE, _I32, _SIZE, _SIZE, _HANDLE]),
75
+ "ve_fit_arch_lm": (_STATUS, [_HANDLE, _SIZE, _SIZE, _HANDLE]),
76
+ "ve_fit_nyblom": (_STATUS, [_HANDLE, _HANDLE, _OUT_DOUBLE, _HANDLE]),
77
+ "ve_nyblom_critical": (_STATUS, [_SIZE, _HANDLE]),
78
+ "ve_fit_sign_bias": (_STATUS, [_HANDLE, _HANDLE]),
79
+ "ve_fit_gof": (_STATUS, [_HANDLE, _SIZE, _HANDLE]),
80
+ "ve_abi_version": (_I32, []),
81
+ "ve_last_error": (ctypes.c_char_p, []),
82
+ "ve_context_create": (_STATUS, [_OUT_HANDLE]),
83
+ "ve_context_set_threads": (_STATUS, [_HANDLE, _I32]),
84
+ "ve_context_threads": (_STATUS, [_HANDLE, _OUT_I32]),
85
+ "ve_context_free": (None, [_HANDLE]),
86
+ "ve_spec_create": (_STATUS, [_I32, _OUT_HANDLE]),
87
+ "ve_spec_set_order": (_STATUS, [_HANDLE, _I32, _I32]),
88
+ "ve_spec_set_arma": (_STATUS, [_HANDLE, _I32, _I32]),
89
+ "ve_spec_set_dist": (_STATUS, [_HANDLE, _I32]),
90
+ "ve_spec_set_include_mean": (_STATUS, [_HANDLE, _I32]),
91
+ "ve_spec_set_mxreg": (_STATUS, [_HANDLE, _I32]),
92
+ "ve_spec_set_vxreg": (_STATUS, [_HANDLE, _I32]),
93
+ "ve_spec_set_archm": (_STATUS, [_HANDLE, _I32]),
94
+ "ve_spec_set_arfima": (_STATUS, [_HANDLE, _I32]),
95
+ "ve_spec_set_fixed": (_STATUS, [_HANDLE, ctypes.c_char_p, _DOUBLE]),
96
+ "ve_spec_set_start": (_STATUS, [_HANDLE, ctypes.c_char_p, _DOUBLE]),
97
+ "ve_spec_set_variance_targeting": (_STATUS, [_HANDLE, _I32]),
98
+ "ve_spec_validate": (_STATUS, [_HANDLE]),
99
+ "ve_spec_free": (None, [_HANDLE]),
100
+ "ve_fit_options_create": (_STATUS, [_OUT_HANDLE]),
101
+ "ve_fit_options_set_max_iter": (_STATUS, [_HANDLE, _I32]),
102
+ "ve_fit_options_set_tolerance": (_STATUS, [_HANDLE, _DOUBLE]),
103
+ "ve_fit_options_set_var_init": (_STATUS, [_HANDLE, _I32]),
104
+ "ve_fit_options_set_standard_errors": (_STATUS, [_HANDLE, _I32]),
105
+ "ve_fit_options_free": (None, [_HANDLE]),
106
+ "ve_panel_create": (_STATUS, [_HANDLE, _SIZE, _SIZE, _OUT_HANDLE]),
107
+ "ve_panel_set_strides": (_STATUS, [_HANDLE, ctypes.c_ssize_t, ctypes.c_ssize_t]),
108
+ "ve_panel_set_ranges": (_STATUS, [_HANDLE, _HANDLE, _HANDLE]),
109
+ "ve_panel_free": (None, [_HANDLE]),
110
+ "ve_batch_create": (_STATUS, [_OUT_HANDLE]),
111
+ "ve_batch_set_specs": (_STATUS, [_HANDLE, ctypes.POINTER(ctypes.c_void_p), _SIZE]),
112
+ "ve_batch_set_panel": (_STATUS, [_HANDLE, _HANDLE]),
113
+ "ve_batch_set_jobs": (_STATUS, [_HANDLE, _HANDLE, _HANDLE, _SIZE]),
114
+ "ve_batch_set_options": (_STATUS, [_HANDLE, _HANDLE]),
115
+ "ve_batch_set_regressors": (_STATUS, [_HANDLE, _HANDLE, _HANDLE, _SIZE]),
116
+ "ve_batch_job_count": (_STATUS, [_HANDLE, _OUT_SIZE]),
117
+ "ve_batch_free": (None, [_HANDLE]),
118
+ "ve_fit_batch": (_STATUS, [_HANDLE, _HANDLE, ctypes.POINTER(ctypes.c_void_p)]),
119
+ "ve_fit_status": (_STATUS, [_HANDLE, _OUT_I32]),
120
+ "ve_fit_nobs": (_STATUS, [_HANDLE, _OUT_SIZE]),
121
+ "ve_fit_param_count": (_STATUS, [_HANDLE, _OUT_SIZE]),
122
+ "ve_fit_param_name": (_STATUS, [_HANDLE, _SIZE, _OUT_TEXT]),
123
+ "ve_fit_estimates": (_STATUS, [_HANDLE, _HANDLE]),
124
+ "ve_fit_std_errors": (_STATUS, _SE_VALUES),
125
+ "ve_fit_t_values": (_STATUS, _SE_VALUES),
126
+ "ve_fit_p_values": (_STATUS, _SE_VALUES),
127
+ "ve_loglik": (_STATUS, [_HANDLE, _OUT_DOUBLE]),
128
+ "ve_aic": (_STATUS, _SCALAR_CRITERION),
129
+ "ve_bic": (_STATUS, _SCALAR_CRITERION),
130
+ "ve_hqic": (_STATUS, _SCALAR_CRITERION),
131
+ "ve_sic": (_STATUS, _SCALAR_CRITERION),
132
+ "ve_fit_sigma": (_STATUS, _VIEW),
133
+ "ve_fit_residuals": (_STATUS, _VIEW),
134
+ "ve_fit_std_residuals": (_STATUS, _VIEW),
135
+ "ve_fit_fitted": (_STATUS, _VIEW),
136
+ "ve_fit_free": (None, [_HANDLE]),
137
+ "ve_forecast_create": (_STATUS, [_HANDLE, _SIZE, _HANDLE, _HANDLE, _OUT_HANDLE]),
138
+ "ve_forecast_horizon": (_STATUS, [_HANDLE, _OUT_SIZE]),
139
+ "ve_forecast_sigma": (_STATUS, _VIEW),
140
+ "ve_forecast_mean": (_STATUS, _VIEW),
141
+ "ve_forecast_free": (None, [_HANDLE]),
142
+ "ve_sim_options_create": (_STATUS, [_SIZE, _SIZE, ctypes.c_uint64, _OUT_HANDLE]),
143
+ "ve_sim_options_set_regressors": (_STATUS, [_HANDLE, _HANDLE, _HANDLE]),
144
+ "ve_sim_options_free": (None, [_HANDLE]),
145
+ "ve_simulate_fit": (_STATUS, [_HANDLE, _HANDLE, _HANDLE, _OUT_HANDLE]),
146
+ "ve_simulate_spec": (_STATUS, [_HANDLE, _HANDLE, _HANDLE, _OUT_HANDLE]),
147
+ "ve_sim_paths": (_STATUS, [_HANDLE, _OUT_SIZE]),
148
+ "ve_sim_horizon": (_STATUS, [_HANDLE, _OUT_SIZE]),
149
+ "ve_sim_returns": (_STATUS, _VIEW),
150
+ "ve_sim_sigma": (_STATUS, _VIEW),
151
+ "ve_sim_free": (None, [_HANDLE]),
152
+ "ve_dcc_future_create": (_STATUS, [_SIZE, _SIZE, _OUT_HANDLE]),
153
+ "ve_dcc_future_set": (_STATUS, [_HANDLE, _SIZE, _HANDLE, _HANDLE]),
154
+ "ve_dcc_future_free": (None, [_HANDLE]),
155
+ "ve_dcc_forecast_create": (_STATUS, [_HANDLE, _SIZE, _HANDLE, _OUT_HANDLE]),
156
+ "ve_dcc_forecast_horizon": (_STATUS, [_HANDLE, _OUT_SIZE]),
157
+ "ve_dcc_forecast_series": (_STATUS, [_HANDLE, _OUT_SIZE]),
158
+ "ve_dcc_forecast_correlation": (_STATUS, _VIEW),
159
+ "ve_dcc_forecast_covariance": (_STATUS, _VIEW),
160
+ "ve_dcc_forecast_sigma": (_STATUS, _VIEW),
161
+ "ve_dcc_forecast_mean": (_STATUS, _VIEW),
162
+ "ve_dcc_forecast_free": (None, [_HANDLE]),
163
+ "ve_sim_options_set_correlations": (_STATUS, [_HANDLE, _I32]),
164
+ "ve_simulate_dcc": (_STATUS, [_HANDLE, _HANDLE, _HANDLE, _HANDLE, _OUT_HANDLE]),
165
+ "ve_dcc_sim_paths": (_STATUS, [_HANDLE, _OUT_SIZE]),
166
+ "ve_dcc_sim_horizon": (_STATUS, [_HANDLE, _OUT_SIZE]),
167
+ "ve_dcc_sim_series": (_STATUS, [_HANDLE, _OUT_SIZE]),
168
+ "ve_dcc_sim_returns": (_STATUS, _VIEW),
169
+ "ve_dcc_sim_sigma": (_STATUS, _VIEW),
170
+ "ve_dcc_sim_correlation": (_STATUS, _VIEW),
171
+ "ve_dcc_sim_covariance": (_STATUS, _VIEW),
172
+ "ve_dcc_sim_free": (None, [_HANDLE]),
173
+ }
174
+
175
+
176
+ class VolEngineError(Exception):
177
+ """A native call failed; `status` is the VE_* code and the message comes from ve_last_error()."""
178
+
179
+ def __init__(self, status: int, message: str) -> None:
180
+ super().__init__(message)
181
+ self.status = status
182
+
183
+
184
+ def locate_library(override: str | None, directory: Path) -> Path:
185
+ if override:
186
+ path = Path(override)
187
+ if not path.is_file():
188
+ raise ImportError(f"{LIBRARY_ENV} points to {override}, which is not a file")
189
+ return path
190
+ for name in LIBRARY_NAMES:
191
+ if (directory / name).is_file():
192
+ return directory / name
193
+ raise ImportError(
194
+ f"VolEngine native library not found: set {LIBRARY_ENV} to the library file, "
195
+ f"or place one of {', '.join(LIBRARY_NAMES)} in {directory}"
196
+ )
197
+
198
+
199
+ def load_library(environ: Mapping[str, str], directory: Path) -> ctypes.CDLL:
200
+ path = locate_library(environ.get(LIBRARY_ENV), directory)
201
+ try:
202
+ library = ctypes.CDLL(str(path))
203
+ except OSError as error:
204
+ raise ImportError(f"cannot load the VolEngine native library {path}: {error}") from error
205
+ check_abi(library, path)
206
+ declare_prototypes(library)
207
+ return library
208
+
209
+
210
+ def check_abi(library: ctypes.CDLL, path: Path) -> None:
211
+ try:
212
+ declare_function(library, "ve_abi_version")
213
+ except AttributeError as error:
214
+ raise ImportError(f"{path} does not export ve_abi_version, it is not a VolEngine library") from error
215
+ abi = library.ve_abi_version()
216
+ if abi != VE_ABI_VERSION:
217
+ raise ImportError(f"{path} has ABI version {abi}, this package needs {VE_ABI_VERSION}")
218
+
219
+
220
+ def declare_function(library: ctypes.CDLL, name: str) -> None:
221
+ restype, argtypes = PROTOTYPES[name]
222
+ function = getattr(library, name)
223
+ function.restype = restype
224
+ function.argtypes = argtypes
225
+
226
+
227
+ def declare_prototypes(library: ctypes.CDLL) -> None:
228
+ for name in PROTOTYPES:
229
+ declare_function(library, name)
230
+
231
+
232
+ lib = load_library(os.environ, LIBRARY_DIRECTORY)
233
+
234
+
235
+ def last_error() -> str:
236
+ message = cast(bytes | None, lib.ve_last_error())
237
+ if message is None:
238
+ return ""
239
+ return message.decode("utf-8", errors="replace")
240
+
241
+
242
+ def int32(value: int, name: str) -> int:
243
+ """`value` checked to fit a C int32_t, which ctypes would otherwise wrap around silently."""
244
+ if not INT32_MIN <= value <= INT32_MAX:
245
+ raise ValueError(f"{name} must fit in a 32-bit signed integer, got {value}")
246
+ return value
247
+
248
+
249
+ def check(status: int) -> None:
250
+ if status != 0:
251
+ raise VolEngineError(status, last_error())
252
+
253
+
254
+ class Handle:
255
+ """Owns one native handle and frees it exactly once."""
256
+
257
+ def __init__(self, pointer: int | None, free: Callable[[int], None]) -> None:
258
+ self._pointer: int | None = pointer
259
+ self._free = free
260
+
261
+ @property
262
+ def pointer(self) -> int:
263
+ if self._pointer is None:
264
+ raise ValueError("the native handle is already closed")
265
+ return self._pointer
266
+
267
+ def close(self) -> None:
268
+ pointer, self._pointer = self._pointer, None
269
+ if pointer is not None:
270
+ self._free(pointer)
271
+
272
+ def __enter__(self) -> Handle:
273
+ return self
274
+
275
+ def __exit__(
276
+ self,
277
+ exc_type: type[BaseException] | None,
278
+ exc: BaseException | None,
279
+ traceback: TracebackType | None,
280
+ ) -> None:
281
+ self.close()
282
+
283
+ def __del__(self) -> None:
284
+ self.close()
285
+
286
+
287
+ def create_handle(create: Callable[..., int], free: Callable[[int], None], *args: Any) -> Handle:
288
+ out = ctypes.c_void_p()
289
+ check(create(*args, ctypes.byref(out)))
290
+ if out.value is None:
291
+ raise VolEngineError(VE_INTERNAL_ERROR, "the native create call returned a null handle")
292
+ return Handle(out.value, free)
293
+
294
+
295
+ def read_scalar(ctype: Any, function: Callable[..., int], *args: Any) -> Any:
296
+ out = ctype()
297
+ check(function(*args, ctypes.byref(out)))
298
+ return out.value
299
+
300
+
301
+ ELEMENT_BYTES = np.dtype(np.float64).itemsize
302
+
303
+
304
+ def open_panel(stack: ExitStack, array: NDArray[np.float64]) -> Handle:
305
+ panel = stack.enter_context(
306
+ create_handle(lib.ve_panel_create, lib.ve_panel_free, array.ctypes.data, array.shape[0], array.shape[1])
307
+ )
308
+ stride_t, stride_n = (stride // ELEMENT_BYTES for stride in array.strides)
309
+ check(lib.ve_panel_set_strides(panel.pointer, stride_t, stride_n))
310
+ return panel
311
+
312
+
313
+ def read_only(array: NDArray[np.float64]) -> NDArray[np.float64]:
314
+ array.flags.writeable = False
315
+ return array
316
+
317
+
318
+ def native_view(owner: object, function: Callable[..., int], pointer: int) -> NDArray[np.float64]:
319
+ """The read-only view a VE_* view function returns for `pointer`; it keeps `owner` (and its handle) alive."""
320
+ data = ctypes.POINTER(ctypes.c_double)()
321
+ length = ctypes.c_size_t()
322
+ check(function(pointer, ctypes.byref(data), ctypes.byref(length)))
323
+ if length.value == 0:
324
+ return read_only(np.empty(0, dtype=np.float64))
325
+ address = ctypes.cast(data, ctypes.c_void_p).value
326
+ assert address is not None
327
+ buffer = (ctypes.c_double * length.value).from_address(address)
328
+ setattr(buffer, "_owner", owner)
329
+ return read_only(np.frombuffer(buffer, dtype=np.float64))
@@ -0,0 +1,70 @@
1
+ """Simulated paths over ve_sim 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 DccSimulation:
14
+ """Paths simulated from a DCC fit, after its last observation.
15
+
16
+ `returns` and `sigma` are read-only arrays of shape (paths, horizon, N); `R` and `H` of shape (paths, horizon,
17
+ N, N) exist only for a simulation run with `correlations=True` (VolEngineError otherwise). Each step takes R
18
+ from the exact recursion, draws a shock of covariance R (a Gaussian scale mixture for the mvt and the
19
+ mvlaplace), and turns it into the standardized shock of each series: itself in a DCC, or through the copula and
20
+ the margin of the series in a Copula-DCC. Path p reads only the stream (seed, p): the same seed gives the same
21
+ paths for any number of threads, within one version of the library. The simulation does not depend on its fit.
22
+ """
23
+
24
+ def __init__(self, handle: Handle) -> None:
25
+ self._handle = handle
26
+ self._shape = (
27
+ read_scalar(ctypes.c_size_t, lib.ve_dcc_sim_paths, handle.pointer),
28
+ read_scalar(ctypes.c_size_t, lib.ve_dcc_sim_horizon, handle.pointer),
29
+ read_scalar(ctypes.c_size_t, lib.ve_dcc_sim_series, handle.pointer),
30
+ )
31
+
32
+ @property
33
+ def returns(self) -> NDArray[np.float64]:
34
+ return native_view(self, lib.ve_dcc_sim_returns, self._handle.pointer).reshape(self._shape)
35
+
36
+ @property
37
+ def sigma(self) -> NDArray[np.float64]:
38
+ return native_view(self, lib.ve_dcc_sim_sigma, self._handle.pointer).reshape(self._shape)
39
+
40
+ @property
41
+ def R(self) -> NDArray[np.float64]:
42
+ return native_view(self, lib.ve_dcc_sim_correlation, self._handle.pointer).reshape(*self._shape, self._shape[2])
43
+
44
+ @property
45
+ def H(self) -> NDArray[np.float64]:
46
+ return native_view(self, lib.ve_dcc_sim_covariance, self._handle.pointer).reshape(*self._shape, self._shape[2])
47
+
48
+
49
+ class Simulation:
50
+ """Paths simulated from a fit (after its last observation) or from a spec with every parameter fixed.
51
+
52
+ `returns` and `sigma` are read-only arrays of shape (paths, horizon): row p is path p, `returns` the simulated
53
+ series and `sigma` its conditional standard deviation. The same seed gives the same paths for any number of
54
+ threads, but only within one version of the library. The simulation does not depend on its source.
55
+ """
56
+
57
+ def __init__(self, handle: Handle) -> None:
58
+ self._handle = handle
59
+ self._shape = (
60
+ read_scalar(ctypes.c_size_t, lib.ve_sim_paths, handle.pointer),
61
+ read_scalar(ctypes.c_size_t, lib.ve_sim_horizon, handle.pointer),
62
+ )
63
+
64
+ @property
65
+ def returns(self) -> NDArray[np.float64]:
66
+ return native_view(self, lib.ve_sim_returns, self._handle.pointer).reshape(self._shape)
67
+
68
+ @property
69
+ def sigma(self) -> NDArray[np.float64]:
70
+ return native_view(self, lib.ve_sim_sigma, self._handle.pointer).reshape(self._shape)
volengine/_spec.py ADDED
@@ -0,0 +1,240 @@
1
+ """Model specifications and fit options, validated eagerly through the native library."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from dataclasses import KW_ONLY, dataclass, field
7
+ from typing import NoReturn
8
+
9
+ from ._native import Handle, check, create_handle, int32, lib
10
+
11
+ MODELS = {"sgarch": 0, "egarch": 1, "gjrgarch": 2, "aparch": 3, "igarch": 4, "csgarch": 5}
12
+ SUBMODELS = {
13
+ "garch": 6,
14
+ "tgarch": 7,
15
+ "avgarch": 8,
16
+ "ngarch": 9,
17
+ "nagarch": 10,
18
+ "aparch": 11,
19
+ "gjrgarch": 12,
20
+ "allgarch": 13,
21
+ }
22
+ DISTRIBUTIONS = {"norm": 0, "std": 1, "ged": 2, "sstd": 3, "sged": 4, "nig": 5, "ghyp": 6, "jsu": 7}
23
+ VAR_INITS = {"sample": 0, "backcast": 1}
24
+
25
+
26
+ def code_of(table: dict[str, int], kind: str, name: str) -> int:
27
+ if name not in table:
28
+ listed = list(table)
29
+ if kind == "model":
30
+ listed.append("fgarch")
31
+ raise ValueError(f"unknown {kind} {name!r}; expected one of {sorted(listed)}")
32
+ return table[name]
33
+
34
+
35
+ class _FrozenDict(dict[str, float]):
36
+ """A dict that refuses every mutation; unlike MappingProxyType it pickles, deep-copies and works with asdict."""
37
+
38
+ def _refuse(self, *args: object, **kwargs: object) -> NoReturn:
39
+ raise TypeError(f"{type(self).__name__} is read-only")
40
+
41
+ __setitem__ = __delitem__ = clear = pop = popitem = setdefault = update = __ior__ = _refuse
42
+
43
+ def __reduce__(self) -> tuple[type[_FrozenDict], tuple[dict[str, float]]]:
44
+ return (type(self), (dict(self),))
45
+
46
+
47
+ def frozen_values(values: Mapping[str, float]) -> Mapping[str, float]:
48
+ return _FrozenDict({name: float(value) for name, value in values.items()})
49
+
50
+
51
+ def encoded_name(name: str) -> bytes:
52
+ if "\0" in name:
53
+ raise ValueError(f"parameter name {name!r} contains a NUL byte")
54
+ return name.encode()
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class Spec:
59
+ """Model specification. Unknown names raise ValueError, native rejections VolEngineError.
60
+
61
+ `p` is the ARCH order (number of alpha terms, lagged squared residuals) and `q` the GARCH order
62
+ (number of beta terms, lagged variances), as in arch's `p` and `q` and in rugarch's
63
+ `garchOrder = c(alpha order, beta order)` (rugarch calls those orders q and p, the other way
64
+ round). `arma` is the ARMA(m, n) order of the conditional mean. Orders are limited to 1..6 for
65
+ `p` and `q` and 0..6 for `arma` (VE_MAX_ORDER).
66
+
67
+ `model` is one of "sgarch", "egarch", "gjrgarch", "aparch", "igarch", "csgarch" and "fgarch".
68
+ "fgarch" requires `submodel`, one of "garch", "tgarch", "avgarch", "ngarch", "nagarch", "aparch",
69
+ "gjrgarch" and "allgarch"; every other model rejects it. The iGARCH `beta_q` is derived
70
+ (1 - sum of alphas - the other betas), not estimated: it has a name and a value, its standard error
71
+ is NaN and the criteria count the estimated parameters only.
72
+
73
+ `dist` is one of "norm", "std", "ged", "sstd", "sged", "nig", "ghyp" and "jsu", all standardized to
74
+ zero mean and unit variance. Distribution parameters come last in the fit names, in the order `skew`,
75
+ `shape`, `ghlambda`: "std" has `shape` (nu > 2), "ged" has `shape` (nu > 0); "sstd" and "sged" are
76
+ Fernandez-Steel skewed versions of those kernels with `skew` xi > 0 and the same `shape`; "nig" and
77
+ "ghyp" are the generalized hyperbolic in the standardized (rho, zeta) parametrization with `skew` rho
78
+ in (-1, 1) and `shape` zeta in (0, 1e6] ("ghyp" adds `ghlambda`, a real number; "nig" fixes it at
79
+ -1/2); "jsu" is Johnson SU with `skew` nu (a real number) and `shape` tau > 0. The "sstd" `skew` is
80
+ xi, not arch's Hansen skewness lambda in (-1, 1): xi = sqrt((1 + lambda) / (1 - lambda)), and the
81
+ standard error of lambda follows by the delta method, d xi / d lambda = 1 / (xi * (1 - lambda)**2);
82
+ the other way, lambda = (xi**2 - 1) / (xi**2 + 1) and SE(lambda) = SE(xi) * 4 * xi / (xi**2 + 1)**2.
83
+
84
+ Past zeta = 1e6 double precision no longer standardizes "nig" and "ghyp" (by then a normal to an
85
+ excess kurtosis of order 1 / zeta, 1.5e-5 for nig), and the log-likelihood is -inf there. On
86
+ near-normal data their fits drive zeta up, where rho and zeta are barely identified: the Hessian is
87
+ then often not positive definite, and every standard error is NaN.
88
+
89
+ Every parameter is estimated, fixed or derived. `fixed` maps parameter names (those of `Fit.names`:
90
+ "mu", "ar1", "omega", "alpha1", "beta1", "gamma1", "eta11", "skew", "shape", ...) to values the
91
+ fit keeps: they are not optimized, their standard errors are NaN and the criteria do not count them.
92
+ A derived parameter (the iGARCH `beta_q`, and `omega` under variance targeting) is a function of
93
+ the others: it has a name and a value, NaN standard errors, and is not counted either. `start`
94
+ maps names to starting values that replace the defaults; a start value for a derived parameter is
95
+ ignored, so `dict(zip(fit.names, fit.coef().estimate))` is a valid warm start. A start value for a
96
+ name that is also in `fixed` is ignored, because fixed wins: `Spec(fixed=..., start=dict(zip(fit.names,
97
+ fit.coef().estimate)))` is a valid warm start. A name with a NUL character is a ValueError. If the
98
+ starting point is outside the model's domain, the fit ends "failed" with NaN. A spec with every parameter fixed does not optimize: the fit evaluates the model
99
+ at the fixed point and is "converged" when the log-likelihood is finite.
100
+
101
+ `variance_targeting=True` makes `omega` derived: it is computed inside the likelihood from the
102
+ residuals of the current mean parameters over the whole sample (m2 = mean of e_t**2, P the
103
+ persistence): omega = m2 * (1 - P) for sgarch and gjrgarch (P = sum(alpha) + sum(beta), plus
104
+ pi * sum(gamma) with pi = P(z < 0) for gjrgarch); omega = log(m2) * (1 - sum(beta)) for egarch;
105
+ omega = mean(|e_t|**delta) * (1 - P) for aparch and fgarch, with delta the power and
106
+ P = sum(alpha_i * kappa_i) + sum(beta). The standard errors of the other parameters come from the
107
+ likelihood in which omega is a function of them (as in rugarch), not from the two-step covariance
108
+ of Francq, Horvath and Zakoian (2011).
109
+
110
+ `mxreg` and `vxreg` are the numbers of mean and variance regressor columns (0..8, VE_MAX_REGRESSORS) and
111
+ `archm` is ARCH-in-mean: 0 none, 1 adds xi * sigma_t to the mean, 2 adds xi * sigma_t**2. Their parameters are
112
+ named, in order, "mu", "ar1".., "ma1".., "archm", "mxreg1".., "omega", ... and "vxreg1".. before "skew". The
113
+ data go to `Context.fit(..., mxreg=, vxreg=)`. Mean: m_t = mu + sum(delta_k * x_t,k) + xi * sigma_t**p and
114
+ d_t = r_t - m_t; the ARMA acts on d_t, and the pre-sample residuals (for var_init "sample" and variance
115
+ targeting) are those with xi = 0. Variance: omega becomes omega + sum(zeta_k * v_t,k) in the scale of the
116
+ model (sigma**2, log sigma**2 or sigma**delta; in csgarch it enters q_t). Under variance targeting
117
+ omega = target - sum(zeta_k * mean(v_k)). With var_init "backcast" and `mxreg`, the backcast uses the
118
+ residuals of the OLS of r on [1 (with a mean), X], as arch does. Persistence < 1 is always enforced, unlike
119
+ rugarch, which drops it when there is `vxreg`; a sigma**2 <= 0 gives a log-likelihood of -inf.
120
+
121
+ `arfima=True` is rugarch's ARFIMA mean: the parameter "arfima", named after "ma1".. (where "archm" would be;
122
+ a spec cannot have both), is d in (1e-8, 0.5). The ARMA residuals e_t become
123
+ y_t = sum(pi_k * e_(t-k), k = 0..t), pi_0 = 1, pi_k = pi_(k-1) * (k - 1 - d) / k, the (1 - L)**d truncated at
124
+ the start of the series, at a cost of O(T**2); y_t replaces e_t everywhere after.
125
+
126
+ Rejected with VolEngineError (the message names the problem): a name the spec does not have
127
+ (`fixed={"shpe": 5.0}`, or "shape" under dist "norm"), fixing the iGARCH `beta_q` or `omega` under
128
+ variance targeting (both derived), variance targeting with "igarch" or "csgarch", and a csgarch
129
+ with `eta21` fixed and a free `beta` (fixing it together with every `beta` is accepted).
130
+ """
131
+
132
+ model: str = "sgarch"
133
+ _: KW_ONLY
134
+ p: int = 1
135
+ q: int = 1
136
+ arma: tuple[int, int] = (0, 0)
137
+ dist: str = "norm"
138
+ include_mean: bool = True
139
+ submodel: str | None = None
140
+ fixed: Mapping[str, float] = field(default_factory=dict)
141
+ start: Mapping[str, float] = field(default_factory=dict)
142
+ variance_targeting: bool = False
143
+ mxreg: int = 0
144
+ vxreg: int = 0
145
+ archm: int = 0
146
+ arfima: bool = False
147
+
148
+ def __post_init__(self) -> None:
149
+ object.__setattr__(self, "fixed", frozen_values(self.fixed))
150
+ object.__setattr__(self, "start", frozen_values(self.start))
151
+ self.native_handle().close()
152
+ object.__setattr__(self, "arma", tuple(self.arma))
153
+
154
+ def __hash__(self) -> int:
155
+ return hash(
156
+ (
157
+ self.model,
158
+ self.p,
159
+ self.q,
160
+ tuple(self.arma),
161
+ self.dist,
162
+ self.include_mean,
163
+ self.submodel,
164
+ frozenset(self.fixed.items()),
165
+ frozenset(self.start.items()),
166
+ self.variance_targeting,
167
+ self.mxreg,
168
+ self.vxreg,
169
+ self.archm,
170
+ self.arfima,
171
+ )
172
+ )
173
+
174
+ def arma_orders(self) -> tuple[int, int]:
175
+ arma = self.arma
176
+ if not isinstance(arma, tuple | list) or len(arma) != 2 or not all(type(i) is int for i in arma):
177
+ raise ValueError(f"arma must be a pair of integers (m, n), got {arma!r}")
178
+ return int32(arma[0], "arma m"), int32(arma[1], "arma n")
179
+
180
+ def model_code(self) -> int:
181
+ if self.model != "fgarch":
182
+ if self.submodel is not None:
183
+ raise ValueError(f"submodel is only for model 'fgarch', got model {self.model!r}")
184
+ return code_of(MODELS, "model", self.model)
185
+ if self.submodel is None:
186
+ raise ValueError(f"model 'fgarch' requires a submodel, one of {sorted(SUBMODELS)}")
187
+ return code_of(SUBMODELS, "submodel", self.submodel)
188
+
189
+ def native_handle(self) -> Handle:
190
+ model = self.model_code()
191
+ dist = code_of(DISTRIBUTIONS, "dist", self.dist)
192
+ arma = self.arma_orders()
193
+ handle = create_handle(lib.ve_spec_create, lib.ve_spec_free, model)
194
+ check(lib.ve_spec_set_order(handle.pointer, int32(self.p, "p"), int32(self.q, "q")))
195
+ check(lib.ve_spec_set_arma(handle.pointer, *arma))
196
+ check(lib.ve_spec_set_dist(handle.pointer, dist))
197
+ check(lib.ve_spec_set_include_mean(handle.pointer, int(self.include_mean)))
198
+ check(lib.ve_spec_set_mxreg(handle.pointer, int32(self.mxreg, "mxreg")))
199
+ check(lib.ve_spec_set_vxreg(handle.pointer, int32(self.vxreg, "vxreg")))
200
+ check(lib.ve_spec_set_archm(handle.pointer, int32(self.archm, "archm")))
201
+ check(lib.ve_spec_set_arfima(handle.pointer, int(self.arfima)))
202
+ self.apply_values(handle)
203
+ return handle
204
+
205
+ def apply_values(self, handle: Handle) -> None:
206
+ for name, value in self.fixed.items():
207
+ check(lib.ve_spec_set_fixed(handle.pointer, encoded_name(name), value))
208
+ for name, value in self.start.items():
209
+ check(lib.ve_spec_set_start(handle.pointer, encoded_name(name), value))
210
+ check(lib.ve_spec_set_variance_targeting(handle.pointer, int(self.variance_targeting)))
211
+ check(lib.ve_spec_validate(handle.pointer))
212
+
213
+
214
+ @dataclass(frozen=True)
215
+ class FitOptions:
216
+ """Optimizer settings for a fit.
217
+
218
+ standard_errors=False skips the inference at the estimate (Hessian and robust standard errors, scores,
219
+ Nyblom): Fit.coef() and Fit.nyblom() then raise VolEngineError, while Fit.estimate, the criteria, the series,
220
+ forecasts, simulations and DCC fits stay the same. Useful for a grid chosen by a criterion; refit the chosen
221
+ model with standard errors.
222
+ """
223
+
224
+ _: KW_ONLY
225
+ max_iter: int = 500
226
+ tol: float = 1e-6
227
+ var_init: str = "sample"
228
+ standard_errors: bool = True
229
+
230
+ def __post_init__(self) -> None:
231
+ self.native_handle().close()
232
+
233
+ def native_handle(self) -> Handle:
234
+ var_init = code_of(VAR_INITS, "var_init", self.var_init)
235
+ handle = create_handle(lib.ve_fit_options_create, lib.ve_fit_options_free)
236
+ check(lib.ve_fit_options_set_max_iter(handle.pointer, int32(self.max_iter, "max_iter")))
237
+ check(lib.ve_fit_options_set_tolerance(handle.pointer, self.tol))
238
+ check(lib.ve_fit_options_set_var_init(handle.pointer, var_init))
239
+ check(lib.ve_fit_options_set_standard_errors(handle.pointer, int(self.standard_errors)))
240
+ return handle