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/api.py ADDED
@@ -0,0 +1,417 @@
1
+ """Public API: the fitting context and the re-exported spec, options and result types."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ctypes
6
+ import operator
7
+ from collections.abc import Sequence
8
+ from contextlib import ExitStack
9
+ from typing import TYPE_CHECKING, Any, TypedDict, overload
10
+
11
+ import numpy as np
12
+ from numpy.typing import ArrayLike, NDArray
13
+
14
+ from ._dcc import DccFit, DccSpec, fit_dcc, open_future
15
+ from ._diagnostics import DiagnosticResult, Nyblom, SignBias
16
+ from ._fit import CoefTable, Fit, future_panel
17
+ from ._forecast import DccForecast, Forecast
18
+ from ._native import (
19
+ ELEMENT_BYTES,
20
+ Handle,
21
+ VolEngineError,
22
+ check,
23
+ create_handle,
24
+ int32,
25
+ lib,
26
+ open_panel,
27
+ )
28
+ from ._simulation import DccSimulation, Simulation
29
+ from ._spec import FitOptions, Spec
30
+
31
+ if TYPE_CHECKING:
32
+ from typing import Unpack
33
+
34
+ __all__ = [
35
+ "CoefTable",
36
+ "Context",
37
+ "DccFit",
38
+ "DccForecast",
39
+ "DccSimulation",
40
+ "DccSpec",
41
+ "DiagnosticResult",
42
+ "Fit",
43
+ "FitOptions",
44
+ "Forecast",
45
+ "Nyblom",
46
+ "SignBias",
47
+ "Simulation",
48
+ "Spec",
49
+ "VolEngineError",
50
+ ]
51
+
52
+ Series1D = Sequence[float]
53
+ UINT64_END = 2**64
54
+
55
+
56
+ class FitKeywords(TypedDict, total=False):
57
+ """The optional keywords of `Context.fit`."""
58
+
59
+ options: FitOptions | None
60
+ starts: ArrayLike | None
61
+ lengths: ArrayLike | None
62
+ mxreg: ArrayLike | None
63
+ vxreg: ArrayLike | None
64
+
65
+
66
+ class FutureKeywords(TypedDict, total=False):
67
+ """The optional keywords of `Context.simulate`: the regressors of the horizon."""
68
+
69
+ mxreg: ArrayLike | None
70
+ vxreg: ArrayLike | None
71
+
72
+
73
+ class DccSimulateKeywords(TypedDict, total=False):
74
+ """The optional keywords of `Context.simulate` from a DccFit: `correlations` and one entry per series of each
75
+ regressor kind, as in `DccFit.forecast`."""
76
+
77
+ correlations: bool
78
+ mxreg: Sequence[ArrayLike | None] | None
79
+ vxreg: Sequence[ArrayLike | None] | None
80
+
81
+
82
+ def panel_array(data: ArrayLike) -> tuple[NDArray[np.float64], bool]:
83
+ array = np.asarray(data, dtype=np.float64)
84
+ if array.ndim == 1:
85
+ return array.reshape(-1, 1), True
86
+ if array.ndim == 2:
87
+ return array, False
88
+ raise ValueError(f"data must be 1D (T,) or 2D (T, N), got {array.ndim} dimensions")
89
+
90
+
91
+ def require_integral(array: NDArray[Any], name: str) -> None:
92
+ if array.dtype.kind in "iu" and not (array < 0).any():
93
+ return
94
+ if array.dtype.kind == "f" and np.isfinite(array).all() and (array >= 0).all() and (array == np.floor(array)).all():
95
+ return
96
+ raise ValueError(f"{name} must hold non-negative integers, got dtype {array.dtype} with values {array.tolist()}")
97
+
98
+
99
+ def range_array(values: ArrayLike, series_count: int, name: str) -> NDArray[np.uintp]:
100
+ array = np.asarray(values)
101
+ if array.shape != (series_count,):
102
+ raise ValueError(f"{name} must have one entry per series ({series_count}), got shape {array.shape}")
103
+ require_integral(array, name)
104
+ return np.ascontiguousarray(array, dtype=np.uintp)
105
+
106
+
107
+ def set_ranges(panel: Handle, first: NDArray[np.uintp], size: NDArray[np.uintp]) -> None:
108
+ check(lib.ve_panel_set_ranges(panel.pointer, first.ctypes.data, size.ctypes.data))
109
+
110
+
111
+ def set_jobs(batch: Handle, spec_count: int, series_count: int) -> None:
112
+ job_spec = np.repeat(np.arange(spec_count, dtype=np.uintp), series_count)
113
+ job_series = np.tile(np.arange(series_count, dtype=np.uintp), spec_count)
114
+ check(lib.ve_batch_set_jobs(batch.pointer, job_spec.ctypes.data, job_series.ctypes.data, job_spec.size))
115
+
116
+
117
+ def open_batch(stack: ExitStack, specs: Sequence[Spec], panel: Handle, options: FitOptions | None) -> Handle:
118
+ batch = stack.enter_context(create_handle(lib.ve_batch_create, lib.ve_batch_free))
119
+ spec_handles = [stack.enter_context(spec.native_handle()) for spec in specs]
120
+ pointers = (ctypes.c_void_p * len(spec_handles))(*(handle.pointer for handle in spec_handles))
121
+ check(lib.ve_batch_set_specs(batch.pointer, pointers, len(spec_handles)))
122
+ check(lib.ve_batch_set_panel(batch.pointer, panel.pointer))
123
+ if options is not None:
124
+ options_handle = stack.enter_context(options.native_handle())
125
+ check(lib.ve_batch_set_options(batch.pointer, options_handle.pointer))
126
+ return batch
127
+
128
+
129
+ def regressor_array(
130
+ values: ArrayLike | None, name: str, counts: list[int], data_shape: tuple[int, ...]
131
+ ) -> NDArray[np.float64] | None:
132
+ """`values` as (T, N, k), or None when no spec has regressors of this kind; the specs with k = 0 ignore it."""
133
+ widths = {count for count in counts if count > 0}
134
+ if not widths and values is not None:
135
+ raise ValueError(f"{name} was given but no spec has {name} > 0")
136
+ if not widths:
137
+ return None
138
+ if values is None:
139
+ raise ValueError(f"a spec has {name} > 0, so {name} is required")
140
+ array = np.asarray(values, dtype=np.float64)
141
+ for width in widths:
142
+ if array.shape != (*data_shape, width):
143
+ raise ValueError(f"{name} must have shape {(*data_shape, width)} for a spec with {name} = {width}, "
144
+ f"got {array.shape}")
145
+ if len(data_shape) == 1:
146
+ array = array[:, None, :]
147
+ if any(stride % ELEMENT_BYTES for stride in array.strides):
148
+ array = np.ascontiguousarray(array)
149
+ return array
150
+
151
+
152
+ def open_regressors(stack: ExitStack, array: NDArray[np.float64] | None, counts: list[int]) -> NDArray[np.uintp] | None:
153
+ """The panel of job j, in the order of `set_jobs`, for each spec in `counts`: series s of `array`, no copy."""
154
+ if array is None:
155
+ return None
156
+ pointers = [open_panel(stack, array[:, series, :]).pointer for series in range(array.shape[1])]
157
+ absent = [0] * len(pointers)
158
+ jobs: list[int] = []
159
+ for count in counts:
160
+ jobs += pointers if count > 0 else absent
161
+ return np.array(jobs, dtype=np.uintp)
162
+
163
+
164
+ def set_regressors(batch: Handle, mean: NDArray[np.uintp] | None, variance: NDArray[np.uintp] | None) -> None:
165
+ jobs = mean if mean is not None else variance
166
+ if jobs is None:
167
+ return
168
+ mean_data = None
169
+ if mean is not None:
170
+ mean_data = mean.ctypes.data
171
+ variance_data = None
172
+ if variance is not None:
173
+ variance_data = variance.ctypes.data
174
+ check(lib.ve_batch_set_regressors(batch.pointer, mean_data, variance_data, jobs.size))
175
+
176
+
177
+ def shaped(fits: list[Fit], shape: tuple[int, ...]) -> NDArray[np.object_]:
178
+ grid = np.empty(len(fits), dtype=object)
179
+ for position, fit in enumerate(fits):
180
+ grid[position] = fit
181
+ return grid.reshape(shape)
182
+
183
+
184
+ def bounded(value: int, name: str, low: int) -> int:
185
+ """value as an integer in [low, 2**64), the range of the size_t and uint64_t of the C API."""
186
+ value = operator.index(value)
187
+ if not low <= value < UINT64_END:
188
+ raise ValueError(f"{name} must be an integer in [{low}, 2**64), got {value}")
189
+ return value
190
+
191
+
192
+ def regressor_counts(source: Fit | Spec) -> tuple[int, int]:
193
+ if isinstance(source, Spec):
194
+ return source.mxreg, source.vxreg
195
+ mean = sum(1 for name in source.names if name.startswith("mxreg"))
196
+ variance = sum(1 for name in source.names if name.startswith("vxreg"))
197
+ return mean, variance
198
+
199
+
200
+ class Context:
201
+ """Execution context owning the native thread pool; fits do not depend on it afterwards.
202
+
203
+ `threads` is between 1 and 1024 (VE_MAX_THREADS); 0, the default, means the hardware
204
+ concurrency, capped at 1024.
205
+ """
206
+
207
+ def __init__(self, threads: int = 0) -> None:
208
+ threads = int32(threads, "threads")
209
+ self._handle = create_handle(lib.ve_context_create, lib.ve_context_free)
210
+ check(lib.ve_context_set_threads(self._handle.pointer, threads))
211
+
212
+ @overload
213
+ def fit(self, spec: Spec, data: Series1D, **keywords: Unpack[FitKeywords]) -> Fit: ...
214
+
215
+ @overload
216
+ def fit(self, spec: Spec, data: ArrayLike, **keywords: Unpack[FitKeywords]) -> Fit | NDArray[np.object_]: ...
217
+
218
+ @overload
219
+ def fit(self, spec: Sequence[Spec], data: ArrayLike, **keywords: Unpack[FitKeywords]) -> NDArray[np.object_]: ...
220
+
221
+ def fit(
222
+ self, spec: Spec | Sequence[Spec], data: ArrayLike, **keywords: Unpack[FitKeywords]
223
+ ) -> Fit | NDArray[np.object_]:
224
+ """Fit every spec on every series of `data`: (T,) or (T, N), optionally with row ranges.
225
+
226
+ One spec and a 1D series give a Fit; one spec and (T, N) data give an object array of
227
+ shape (N,); a list of specs gives (n_specs,) or (n_specs, N).
228
+
229
+ Keywords (all optional): `options` (FitOptions), `starts` and `lengths` (row ranges, given together),
230
+ `mxreg` and `vxreg`, the mean and variance regressors of the specs with mxreg > 0 or vxreg > 0:
231
+ shape (T, k) for 1D data and (T, N, k) for (T, N) data, k being the count of the spec (a different k, a
232
+ different shape or a missing array is a ValueError). Series j uses X[:, j, :] without a copy, and with
233
+ `starts` and `lengths` its rows starts[j] .. starts[j] + lengths[j] - 1 of the regressors. The specs with a
234
+ count of 0 take no regressors; an array given when every spec has a count of 0 is a ValueError.
235
+ """
236
+ unknown = sorted(keywords.keys() - FitKeywords.__annotations__.keys())
237
+ if unknown:
238
+ raise TypeError(f"fit() got an unexpected keyword argument {unknown[0]!r}")
239
+ starts, lengths = keywords.get("starts"), keywords.get("lengths")
240
+ if (starts is None) != (lengths is None):
241
+ raise ValueError("starts and lengths must be given together")
242
+ specs: list[Spec] = []
243
+ if isinstance(spec, Spec):
244
+ specs.append(spec)
245
+ if not isinstance(spec, Spec):
246
+ specs.extend(spec)
247
+ array, one_dimensional = panel_array(data)
248
+ series_count = array.shape[1]
249
+ ranges = None
250
+ if starts is not None and lengths is not None:
251
+ ranges = (range_array(starts, series_count, "starts"), range_array(lengths, series_count, "lengths"))
252
+ data_shape = array.shape
253
+ if one_dimensional:
254
+ data_shape = array.shape[:1]
255
+ regressors = (
256
+ regressor_array(keywords.get("mxreg"), "mxreg", [item.mxreg for item in specs], data_shape),
257
+ regressor_array(keywords.get("vxreg"), "vxreg", [item.vxreg for item in specs], data_shape),
258
+ )
259
+ fits = self._run(specs, array, keywords.get("options"), ranges, regressors)
260
+ if isinstance(spec, Spec) and one_dimensional:
261
+ return fits[0]
262
+ if isinstance(spec, Spec):
263
+ return shaped(fits, (series_count,))
264
+ if one_dimensional:
265
+ return shaped(fits, (len(specs),))
266
+ return shaped(fits, (len(specs), series_count))
267
+
268
+ @overload
269
+ def fit_dcc(self, spec: DccSpec, univariate: Sequence[Fit], *, standard_errors: bool = True) -> DccFit: ...
270
+
271
+ @overload
272
+ def fit_dcc(
273
+ self, spec: Sequence[DccSpec], univariate: Sequence[Fit], *, standard_errors: bool = True
274
+ ) -> list[DccFit]: ...
275
+
276
+ def fit_dcc(
277
+ self, spec: DccSpec | Sequence[DccSpec], univariate: Sequence[Fit], *, standard_errors: bool = True
278
+ ) -> DccFit | list[DccFit]:
279
+ """Fit one DCC spec or a grid over the chosen ordered univariate fits.
280
+
281
+ Fits need at least two series with the same T >= 100 and finite points.
282
+ Disable standard errors for grid selection, then call the selected result's
283
+ standard_errors(univariate). Regressor fits require this option disabled.
284
+ """
285
+ results = fit_dcc(self._handle, spec, univariate, standard_errors)
286
+ if isinstance(spec, DccSpec):
287
+ return results[0]
288
+ return results
289
+
290
+ @overload
291
+ def simulate(
292
+ self,
293
+ source: Fit | Spec,
294
+ *,
295
+ horizon: int,
296
+ paths: int,
297
+ seed: int,
298
+ **regressors: Unpack[FutureKeywords],
299
+ ) -> Simulation: ...
300
+
301
+ @overload
302
+ def simulate(
303
+ self,
304
+ source: DccFit,
305
+ *,
306
+ horizon: int,
307
+ paths: int,
308
+ seed: int,
309
+ **options: Unpack[DccSimulateKeywords],
310
+ ) -> DccSimulation: ...
311
+
312
+ def simulate(
313
+ self,
314
+ source: Fit | Spec | DccFit,
315
+ *,
316
+ horizon: int,
317
+ paths: int,
318
+ seed: int,
319
+ **regressors: Any,
320
+ ) -> Simulation | DccSimulation:
321
+ """Simulate `paths` paths of `horizon` steps from a fit or from a spec with every parameter fixed.
322
+
323
+ From a Fit the paths continue after its last observation (rugarch's ugarchsim, startMethod = "sample");
324
+ from a Spec they start at its unconditional state (rugarch's ugarchpath), which needs every parameter
325
+ fixed, no variance targeting, a stationary model and not iGARCH (VolEngineError otherwise). `mxreg` and
326
+ `vxreg` are the regressors of the horizon, as in `Fit.forecast`, the same for every path. The threads of the
327
+ context share the paths; the same seed gives the same paths for any thread count, within one version of
328
+ the library. `seed` is an integer in [0, 2**64).
329
+
330
+ From a DccFit (a result of `fit_dcc`, which keeps the final state of each series and needs no univariate
331
+ `Fit`) the paths are multivariate and give a DccSimulation: `mxreg` and `vxreg` are sequences with one entry
332
+ per series, as in `DccFit.forecast`, and `correlations=True` keeps R and H of every path and step (memory:
333
+ paths x horizon x N^2 x 2 doubles). Path p reads only the stream (seed, p), with the same version rule.
334
+ """
335
+ horizon = bounded(horizon, "horizon", 1)
336
+ paths = bounded(paths, "paths", 1)
337
+ seed = bounded(seed, "seed", 0)
338
+ if isinstance(source, DccFit):
339
+ return self._simulate_dcc(source, horizon, paths, seed, regressors)
340
+ if "correlations" in regressors:
341
+ raise TypeError("correlations is only for a DccFit")
342
+ unknown = sorted(regressors.keys() - FutureKeywords.__annotations__.keys())
343
+ if unknown:
344
+ raise TypeError(f"simulate() got an unexpected keyword argument {unknown[0]!r}")
345
+ counts = regressor_counts(source)
346
+ with ExitStack() as stack:
347
+ mean = future_panel(stack, regressors.get("mxreg"), "mxreg", counts[0], horizon)
348
+ variance = future_panel(stack, regressors.get("vxreg"), "vxreg", counts[1], horizon)
349
+ options = stack.enter_context(
350
+ create_handle(lib.ve_sim_options_create, lib.ve_sim_options_free, horizon, paths, seed)
351
+ )
352
+ check(lib.ve_sim_options_set_regressors(options.pointer, mean, variance))
353
+ if isinstance(source, Spec):
354
+ spec = stack.enter_context(source.native_handle())
355
+ return Simulation(
356
+ create_handle(
357
+ lib.ve_simulate_spec, lib.ve_sim_free, self._handle.pointer, spec.pointer, options.pointer
358
+ )
359
+ )
360
+ return Simulation(
361
+ create_handle(
362
+ lib.ve_simulate_fit, lib.ve_sim_free, self._handle.pointer, source._handle.pointer, options.pointer
363
+ )
364
+ )
365
+
366
+ def _simulate_dcc(
367
+ self, source: DccFit, horizon: int, paths: int, seed: int, options: dict[str, Any]
368
+ ) -> DccSimulation:
369
+ unknown = sorted(options.keys() - DccSimulateKeywords.__annotations__.keys())
370
+ if unknown:
371
+ raise TypeError(f"simulate() got an unexpected keyword argument {unknown[0]!r}")
372
+ with ExitStack() as stack:
373
+ future = open_future(stack, horizon, options.get("mxreg"), options.get("vxreg"))
374
+ handle = stack.enter_context(
375
+ create_handle(lib.ve_sim_options_create, lib.ve_sim_options_free, horizon, paths, seed)
376
+ )
377
+ check(lib.ve_sim_options_set_correlations(handle.pointer, int(bool(options.get("correlations", False)))))
378
+ return DccSimulation(
379
+ create_handle(
380
+ lib.ve_simulate_dcc,
381
+ lib.ve_dcc_sim_free,
382
+ self._handle.pointer,
383
+ source._handle.pointer,
384
+ handle.pointer,
385
+ future,
386
+ )
387
+ )
388
+
389
+ def _run(
390
+ self,
391
+ specs: list[Spec],
392
+ array: NDArray[np.float64],
393
+ options: FitOptions | None,
394
+ ranges: tuple[NDArray[np.uintp], NDArray[np.uintp]] | None,
395
+ regressors: tuple[NDArray[np.float64] | None, NDArray[np.float64] | None],
396
+ ) -> list[Fit]:
397
+ if any(stride % ELEMENT_BYTES for stride in array.strides):
398
+ array = np.ascontiguousarray(array)
399
+ series_count = array.shape[1]
400
+ with ExitStack() as stack:
401
+ panel = open_panel(stack, array)
402
+ if ranges is not None:
403
+ set_ranges(panel, ranges[0], ranges[1])
404
+ batch = open_batch(stack, specs, panel, options)
405
+ set_jobs(batch, len(specs), series_count)
406
+ mean = open_regressors(stack, regressors[0], [spec.mxreg for spec in specs])
407
+ variance = open_regressors(stack, regressors[1], [spec.vxreg for spec in specs])
408
+ set_regressors(batch, mean, variance)
409
+ return self._fit_batch(batch)
410
+
411
+ def _fit_batch(self, batch: Handle) -> list[Fit]:
412
+ count = ctypes.c_size_t()
413
+ check(lib.ve_batch_job_count(batch.pointer, ctypes.byref(count)))
414
+ pointers = (ctypes.c_void_p * count.value)()
415
+ check(lib.ve_fit_batch(self._handle.pointer, batch.pointer, pointers))
416
+ handles = [Handle(pointer, lib.ve_fit_free) for pointer in pointers]
417
+ return [Fit(handle) for handle in handles]
volengine/py.typed ADDED
File without changes
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.5
2
+ Name: volengine
3
+ Version: 0.9.0
4
+ Summary: GARCH volatility estimation over the VolEngine C API
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: numpy
@@ -0,0 +1,14 @@
1
+ volengine/__init__.py,sha256=KVxuLMbldIavrfwTggf3MAsSs4mc9088nyPVtyW7MOo,608
2
+ volengine/_dcc.py,sha256=-dLUE27huFh6QyWxURMYYBJkkTgCs_rJ7O5kBpGjoOs,12510
3
+ volengine/_diagnostics.py,sha256=qLaZaNwxv0peZ3IikitlvRL3iHn556c9kZy1dwSah80,1419
4
+ volengine/_fit.py,sha256=-dsMyQqTp7zVKIQrwYi5T2kQFYq-fFc7nCdoP1y9F7A,9259
5
+ volengine/_forecast.py,sha256=uKVSKmTusu26G6L8qPxYwI6awFwjiMn6q7yiCh6Bv2Y,2912
6
+ volengine/_native.py,sha256=vVC-6H8ZNDEh7MBjTGTVmx98MSmj_rybqc28QEKEHtU,14354
7
+ volengine/_simulation.py,sha256=52Ebjwr3_1zzWDPNt3j8M7nrjackF5WjpoNxPpQBhJY,3123
8
+ volengine/_spec.py,sha256=q1DbCU_8FDG75C6z4DrelCUfrfdajSelS3Zg4nhMcos,12886
9
+ volengine/api.py,sha256=38OeiXpn0apcJ5ZW03UVCfxfTUh1I31s7lqsOJgs7c4,17968
10
+ volengine/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ volengine/_lib/libvolengine.dll,sha256=gEwKxhqXHPLp1TqD-38ZsHrJ5oURZz0te43bA_xw3PA,6224908
12
+ volengine-0.9.0.dist-info/METADATA,sha256=HmzwJGEhpK8LYuy0F6v5He2MBwkHOR1TJi1wTQXrUTc,160
13
+ volengine-0.9.0.dist-info/WHEEL,sha256=lpoBGaIUIidvvdZ4qRAP61bBKYJdt4FI3WXFL50Gzyo,94
14
+ volengine-0.9.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: false
4
+ Tag: py3-none-win_amd64