@krzysztofsztompka/garch 0.0.0-stage → 0.2.0
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.
- package/LICENSE +21 -0
- package/README.md +318 -2
- package/dist/cjs/diagnostics.d.ts +29 -0
- package/dist/cjs/diagnostics.js +154 -0
- package/dist/cjs/diagnostics.js.map +1 -0
- package/dist/cjs/egarch.d.ts +46 -0
- package/dist/cjs/egarch.js +150 -0
- package/dist/cjs/egarch.js.map +1 -0
- package/dist/cjs/fit.d.ts +84 -0
- package/dist/cjs/fit.js +354 -0
- package/dist/cjs/fit.js.map +1 -0
- package/dist/cjs/forecast.d.ts +8 -0
- package/dist/cjs/forecast.js +29 -0
- package/dist/cjs/forecast.js.map +1 -0
- package/dist/cjs/index.d.ts +14 -0
- package/dist/cjs/index.js +67 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/inference.d.ts +29 -0
- package/dist/cjs/inference.js +110 -0
- package/dist/cjs/inference.js.map +1 -0
- package/dist/cjs/jobs.d.ts +78 -0
- package/dist/cjs/jobs.js +107 -0
- package/dist/cjs/jobs.js.map +1 -0
- package/dist/cjs/model.d.ts +60 -0
- package/dist/cjs/model.js +194 -0
- package/dist/cjs/model.js.map +1 -0
- package/dist/cjs/node.d.ts +29 -0
- package/dist/cjs/node.js +75 -0
- package/dist/cjs/node.js.map +1 -0
- package/dist/cjs/optimize.d.ts +11 -0
- package/dist/cjs/optimize.js +61 -0
- package/dist/cjs/optimize.js.map +1 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/pool.d.ts +84 -0
- package/dist/cjs/pool.js +257 -0
- package/dist/cjs/pool.js.map +1 -0
- package/dist/cjs/quality.d.ts +24 -0
- package/dist/cjs/quality.js +46 -0
- package/dist/cjs/quality.js.map +1 -0
- package/dist/cjs/registry.d.ts +17 -0
- package/dist/cjs/registry.js +18 -0
- package/dist/cjs/registry.js.map +1 -0
- package/dist/cjs/risk.d.ts +82 -0
- package/dist/cjs/risk.js +165 -0
- package/dist/cjs/risk.js.map +1 -0
- package/dist/cjs/simulate.d.ts +35 -0
- package/dist/cjs/simulate.js +143 -0
- package/dist/cjs/simulate.js.map +1 -0
- package/dist/cjs/skewt.d.ts +45 -0
- package/dist/cjs/skewt.js +107 -0
- package/dist/cjs/skewt.js.map +1 -0
- package/dist/cjs/special.d.ts +13 -0
- package/dist/cjs/special.js +130 -0
- package/dist/cjs/special.js.map +1 -0
- package/dist/cjs/validation.d.ts +112 -0
- package/dist/cjs/validation.js +163 -0
- package/dist/cjs/validation.js.map +1 -0
- package/dist/cjs/web-worker-file.d.ts +17 -0
- package/dist/cjs/web-worker-file.js +14 -0
- package/dist/cjs/web-worker-file.js.map +1 -0
- package/dist/cjs/web.d.ts +30 -0
- package/dist/cjs/web.js +56 -0
- package/dist/cjs/web.js.map +1 -0
- package/dist/cjs/weighted.d.ts +81 -0
- package/dist/cjs/weighted.js +139 -0
- package/dist/cjs/weighted.js.map +1 -0
- package/dist/cjs/worker-core.d.ts +13 -0
- package/dist/cjs/worker-core.js +22 -0
- package/dist/cjs/worker-core.js.map +1 -0
- package/dist/cjs/worker-file.d.ts +1 -0
- package/dist/cjs/worker-file.js +12 -0
- package/dist/cjs/worker-file.js.map +1 -0
- package/dist/cjs/worker-web.d.ts +1 -0
- package/dist/cjs/worker-web.js +10 -0
- package/dist/cjs/worker-web.js.map +1 -0
- package/dist/cjs/worker.d.ts +1 -0
- package/dist/cjs/worker.js +10 -0
- package/dist/cjs/worker.js.map +1 -0
- package/dist/esm/diagnostics.d.ts +29 -0
- package/dist/esm/diagnostics.js +148 -0
- package/dist/esm/diagnostics.js.map +1 -0
- package/dist/esm/egarch.d.ts +46 -0
- package/dist/esm/egarch.js +144 -0
- package/dist/esm/egarch.js.map +1 -0
- package/dist/esm/fit.d.ts +84 -0
- package/dist/esm/fit.js +349 -0
- package/dist/esm/fit.js.map +1 -0
- package/dist/esm/forecast.d.ts +8 -0
- package/dist/esm/forecast.js +26 -0
- package/dist/esm/forecast.js.map +1 -0
- package/dist/esm/index.d.ts +14 -0
- package/dist/esm/index.js +14 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/inference.d.ts +29 -0
- package/dist/esm/inference.js +104 -0
- package/dist/esm/inference.js.map +1 -0
- package/dist/esm/jobs.d.ts +78 -0
- package/dist/esm/jobs.js +99 -0
- package/dist/esm/jobs.js.map +1 -0
- package/dist/esm/model.d.ts +60 -0
- package/dist/esm/model.js +183 -0
- package/dist/esm/model.js.map +1 -0
- package/dist/esm/node.d.ts +29 -0
- package/dist/esm/node.js +62 -0
- package/dist/esm/node.js.map +1 -0
- package/dist/esm/optimize.d.ts +11 -0
- package/dist/esm/optimize.js +58 -0
- package/dist/esm/optimize.js.map +1 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/pool.d.ts +84 -0
- package/dist/esm/pool.js +246 -0
- package/dist/esm/pool.js.map +1 -0
- package/dist/esm/quality.d.ts +24 -0
- package/dist/esm/quality.js +43 -0
- package/dist/esm/quality.js.map +1 -0
- package/dist/esm/registry.d.ts +17 -0
- package/dist/esm/registry.js +15 -0
- package/dist/esm/registry.js.map +1 -0
- package/dist/esm/risk.d.ts +82 -0
- package/dist/esm/risk.js +155 -0
- package/dist/esm/risk.js.map +1 -0
- package/dist/esm/simulate.d.ts +35 -0
- package/dist/esm/simulate.js +140 -0
- package/dist/esm/simulate.js.map +1 -0
- package/dist/esm/skewt.d.ts +45 -0
- package/dist/esm/skewt.js +99 -0
- package/dist/esm/skewt.js.map +1 -0
- package/dist/esm/special.d.ts +13 -0
- package/dist/esm/special.js +122 -0
- package/dist/esm/special.js.map +1 -0
- package/dist/esm/validation.d.ts +112 -0
- package/dist/esm/validation.js +150 -0
- package/dist/esm/validation.js.map +1 -0
- package/dist/esm/web-worker-file.d.ts +17 -0
- package/dist/esm/web-worker-file.js +4 -0
- package/dist/esm/web.d.ts +30 -0
- package/dist/esm/web.js +43 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/esm/weighted.d.ts +81 -0
- package/dist/esm/weighted.js +132 -0
- package/dist/esm/weighted.js.map +1 -0
- package/dist/esm/worker-core.d.ts +13 -0
- package/dist/esm/worker-core.js +19 -0
- package/dist/esm/worker-core.js.map +1 -0
- package/dist/esm/worker-file.d.ts +1 -0
- package/dist/esm/worker-file.js +2 -0
- package/dist/esm/worker-web.d.ts +1 -0
- package/dist/esm/worker-web.js +8 -0
- package/dist/esm/worker-web.js.map +1 -0
- package/dist/esm/worker.d.ts +1 -0
- package/dist/esm/worker.js +8 -0
- package/dist/esm/worker.js.map +1 -0
- package/package.json +88 -4
- package/src/diagnostics.ts +144 -0
- package/src/egarch.ts +162 -0
- package/src/fit.ts +442 -0
- package/src/forecast.ts +26 -0
- package/src/index.ts +89 -0
- package/src/inference.ts +127 -0
- package/src/jobs.ts +174 -0
- package/src/model.ts +222 -0
- package/src/node.ts +95 -0
- package/src/optimize.ts +66 -0
- package/src/pool.ts +309 -0
- package/src/quality.ts +43 -0
- package/src/registry.ts +18 -0
- package/src/risk.ts +205 -0
- package/src/simulate.ts +165 -0
- package/src/skewt.ts +134 -0
- package/src/special.ts +112 -0
- package/src/validation.ts +242 -0
- package/src/web-worker-file.ts +21 -0
- package/src/web.ts +86 -0
- package/src/weighted.ts +228 -0
- package/src/worker-core.ts +22 -0
- package/src/worker-file.ts +9 -0
- package/src/worker-web.ts +13 -0
- package/src/worker.ts +8 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Krzysztof Sztompka
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,319 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @krzysztofsztompka/garch
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
GARCH(1,1), GJR-GARCH(1,1) and EGARCH(1,1,1) by maximum likelihood, with
|
|
4
|
+
walk-forward validation. Pure TypeScript, **no runtime dependencies**, CommonJS
|
|
5
|
+
and ES modules. Fits match Python's [`arch`](https://github.com/bashtage/arch)
|
|
6
|
+
where the likelihood has one optimum, and find a higher one where it has several.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install @krzysztofsztompka/garch
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Node ≥ 20. The core (`@krzysztofsztompka/garch`) runs anywhere; the worker pool
|
|
13
|
+
comes as `@krzysztofsztompka/garch/node` (Node's `worker_threads`) and
|
|
14
|
+
`@krzysztofsztompka/garch/web` (Web Workers, ES modules only).
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { fitGarch, forecastVariance } from '@krzysztofsztompka/garch';
|
|
20
|
+
|
|
21
|
+
// returns in any units — e.g. ×100 log-returns; answers come in the same units squared
|
|
22
|
+
const fit = fitGarch(returns, { model: 'gjr', inference: true });
|
|
23
|
+
fit.params; // { mu, omega, alpha, gamma, beta }
|
|
24
|
+
fit.inference?.pValues.gamma; // robust, two-sided
|
|
25
|
+
forecastVariance(fit, 20); // s_{T+1} … s_{T+20}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
A walk-forward refits at every origin, which takes seconds to a minute on one
|
|
29
|
+
core for a long history. The pool spreads it over every core but one, off the
|
|
30
|
+
caller's event loop, with the same answer to the bit:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { volatilityForecastInWorkers, weightedForecastInWorkers } from '@krzysztofsztompka/garch/node';
|
|
34
|
+
|
|
35
|
+
await volatilityForecastInWorkers({ returns, model: 'gjr', horizon: 20 }, { timeoutMs: 120_000 });
|
|
36
|
+
await weightedForecastInWorkers({ returns, horizon: 20 }); // four models weighted by past loss
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
What it does not do, deliberately:
|
|
40
|
+
|
|
41
|
+
- **No prices and no percent.** It takes returns in whatever units it is given.
|
|
42
|
+
- **No interpretation.** When σ̂ is worth quoting, or what a regime means, is the caller's.
|
|
43
|
+
- **No trimming.** `quantizedUntil` says where a coarse-grid stretch of history ends; whether to fit from there is the caller's.
|
|
44
|
+
- **No EGARCH with Student-t or skewed-t innovations.** Its expected variance two steps ahead is infinite (below).
|
|
45
|
+
- **No AR or ARMA mean.** The mean is a constant or zero. Daily returns have next to no autocorrelation to model; a caller who needs it can filter the returns first and fit with `mean: 'zero'`.
|
|
46
|
+
- **No GED innovations.** Normal, t and skewed t cover thin tails, fat tails and a skew; GED adds a tail shape between normal and t that daily equity returns rarely ask for.
|
|
47
|
+
- **No GARCH(p, q) beyond (1, 1).** Higher orders rarely beat (1, 1) out of sample, and they need a different stationarity parameterisation; every model here is (1, 1).
|
|
48
|
+
|
|
49
|
+
## The model
|
|
50
|
+
|
|
51
|
+
`arch`'s default specification: a constant mean and normal innovations.
|
|
52
|
+
|
|
53
|
+
r_t = μ + e_t, e_t ~ N(0, s_t)
|
|
54
|
+
s_t = ω + α·e²_{t−1} + γ·e²_{t−1}·[e_{t−1} < 0] + β·s_{t−1} (γ = 0 for garch)
|
|
55
|
+
|
|
56
|
+
- **A zero mean** (`mean: 'zero'`, `arch`'s `mean='Zero'`) holds μ at 0 for returns already demeaned; every model takes it, and so do the walk-forward, the pool and the weighted forecast. It matches `arch` on the fixture series.
|
|
57
|
+
- **Backcast as in `arch`.** The recursion starts from an exponentially weighted (0.94) mean of the first 75 squared residuals.
|
|
58
|
+
- **Analytic forecasts.** s_{T+k} = ω + (α + γ/2 + β)·s_{T+k−1}.
|
|
59
|
+
- **AIC and BIC** as `arch` computes them, −2·ln L + 2k and −2·ln L + k·ln n, with k the free parameters (`parameterCount`): μ, ω, α, β, plus γ for gjr and egarch, plus ν for t unless it is held on a bound.
|
|
60
|
+
- **Robust standard errors.** Bollerslev–Wooldridge, `arch`'s `cov_type="robust"`: analytic scores and a Hessian taken by differencing the analytic gradient.
|
|
61
|
+
|
|
62
|
+
### Student-t innovations (`dist: 't'`)
|
|
63
|
+
|
|
64
|
+
l_t = ln Γ((ν+1)/2) − ln Γ(ν/2) − ½ ln(π(ν−2)) − ½ ln s_t − (ν+1)/2 · ln(1 + e²_t / (s_t(ν−2)))
|
|
65
|
+
|
|
66
|
+
s_t is still the variance, so the forecasts are the same recursion. ν is a
|
|
67
|
+
parameter with its analytic gradient and robust standard error.
|
|
68
|
+
|
|
69
|
+
- **ν ≥ 4 by default** (`nuMin`; `arch` allows 2.05). A t variance is scale²·ν/(ν−2), and near 2 it is barely defined. Fitted freely on daily equity returns, ν̂ ≈ 2.1–2.5 gave a variance forecast worse than naive; at 4 the kurtosis is still infinite, but the forecast is stable.
|
|
70
|
+
- **A ν the data push past a bound is held there.** Free, the logit of ν runs off and the fit never converges. The fit is refitted with ν fixed on the bound (the constrained optimum) and reports `nuBound: 'lower' | 'upper'`. A held ν has no standard error.
|
|
71
|
+
- **Exact-zero returns break it.** Prices recorded on a coarse grid make most days an exact zero: cents on a stock worth $0.08 split-adjusted, or a penny stock's cents times a 1:100 reverse split. On an exact zero the variance can run to 0 while the likelihood rises without bound. The normal likelihood punishes that on the next non-zero day (e²/s); the t likelihood only logarithmically, so the fit degenerates to ω ≈ 0 and μ ≈ 0, **and can still report `converged`**. `quantizedUntil(returns)` returns the index after the last 250-return window with more than 20 % exact zeros; fit t from there.
|
|
72
|
+
|
|
73
|
+
### Skewed-t innovations (`dist: 'skewt'`)
|
|
74
|
+
|
|
75
|
+
Hansen's skewed t, `arch`'s `SkewStudent`: standardised to mean 0 and variance
|
|
76
|
+
1, with ν (`arch`'s η) and λ ∈ (−1, 1); λ < 0 leans the mass left, a fatter
|
|
77
|
+
loss tail. Each side of the density is half a standardised t, so its CDF,
|
|
78
|
+
quantile and partial moments E[zᵏ·1{z < x}] are closed forms over the
|
|
79
|
+
incomplete beta; they agree with `arch`'s to 1e-14. The gradient is analytic
|
|
80
|
+
in λ and ν as well, λ has a robust standard error and p-value, and ν has the
|
|
81
|
+
same floor and bound handling as under t.
|
|
82
|
+
|
|
83
|
+
- **GJR's asymmetry under a skew.** A down move's share of a unit shock's variance, κ = E[z²·1{z < 0}], is ½ only for a symmetric innovation; under a skewed t it is computed exactly (`downShare`, `skewDownShare`). `persistence` and the forecast use κ: s_{T+k} = ω + (α + κγ + β)·s_{T+k−1}. `arch` uses ½ there whatever the distribution, so for a skewed GJR its analytic forecast and this one differ by that much; the simulated paths here agree with this one. The search space keeps `arch`'s constraint α + γ/2 + β < 1.
|
|
84
|
+
- **Against `arch`** on SPY: garch and gjr fits to ~1e-5, with an equal or higher likelihood, and λ's and γ's p-values within 2 %.
|
|
85
|
+
- **VaR and ES** take λ (`tailRisk({ ..., nu, lambda })`), and `simulate` draws skewed-t innovations when the parameters carry λ.
|
|
86
|
+
|
|
87
|
+
### EGARCH (`model: 'egarch'`)
|
|
88
|
+
|
|
89
|
+
`arch`'s `vol="EGARCH", p=1, o=1, q=1` with normal innovations:
|
|
90
|
+
|
|
91
|
+
ln s_t = ω + α·(|z_{t−1}| − √(2/π)) + γ·z_{t−1} + β·ln s_{t−1}, z = e / √s
|
|
92
|
+
|
|
93
|
+
- **The asymmetry is γ itself, and its sign is the other way round from GJR.** γ < 0 means a down move raises tomorrow's variance more than an up move of the same size. `persistence` is β.
|
|
94
|
+
- **The recursion starts from ln of the backcast**, with no shock term at t = 0, as in `arch`. `GarchFit.backcast` holds that log.
|
|
95
|
+
- **Parameter space.** β = σ(x2) ∈ (0, 1); ω = (1 − β)·(ln var(r) + x1); α and γ are free. `arch` lets β run in (−1, 1); a negative β is left out here. The gradient is analytic, through the recursion of d ln s.
|
|
96
|
+
- **The forecast is exact, not simulated.** E[s_{T+k}] = exp(m_k)·Π_{j<k−1} E[exp(βʲ·g(z))], with m_k = ω + β·m_{k−1}. Under the normal, E[exp(a|z| + bz)] = e^{(a+b)²/2}Φ(a+b) + e^{(a−b)²/2}Φ(a−b). Under Student-t that expectation is infinite for every a > 0, which is why a t EGARCH is refused (`GarchInputError`).
|
|
97
|
+
- **Against `arch` 8.0.0** (`test/fixtures/arch-egarch-2026-10-01.json`): the same optimum to ~1e-5 in α, γ, β, and an equal or slightly higher log-likelihood. γ's robust p-value agrees within 1 %. The closed-form forecast agrees with `arch`'s 200 000 simulated paths (seeded) to 0.02 % and 0.08 % at 20 steps.
|
|
98
|
+
- **Its walk-forward baseline is symmetric GARCH,** like GJR's: is the asymmetry worth it?
|
|
99
|
+
|
|
100
|
+
### Simulation
|
|
101
|
+
|
|
102
|
+
`simulate` runs the model's own recursion forward: `paths` paths of `steps`
|
|
103
|
+
returns, from where a fit stands (its last variance and residual, so step 1's
|
|
104
|
+
variance is the one-step forecast) or from the unconditional variance. The
|
|
105
|
+
innovations are normal, or Student-t scaled to unit variance when the
|
|
106
|
+
parameters carry ν; an EGARCH with t is refused, as in fitting. One seeded
|
|
107
|
+
stream (xoshiro128**) feeds every path, so a seed gives the same paths on any
|
|
108
|
+
platform. The paths' mean variance converges to `forecastVariance`.
|
|
109
|
+
|
|
110
|
+
### Residual diagnostics
|
|
111
|
+
|
|
112
|
+
`standardizedResiduals(fit, returns)` gives z_t = (r_t − μ)/√s_t. `ljungBox`
|
|
113
|
+
(on z for the mean, on z² for the variance) and `archLm` (Engle's LM test on
|
|
114
|
+
z) say whether the model left anything behind; each returns the statistic, its
|
|
115
|
+
χ² degrees of freedom and the p-value. They follow statsmodels'
|
|
116
|
+
`acorr_ljungbox` and `het_arch` and agree with them on the fixture series to
|
|
117
|
+
1e-3; `chiSquareSurvival` agrees with scipy to 1e-12.
|
|
118
|
+
|
|
119
|
+
### Value at risk
|
|
120
|
+
|
|
121
|
+
VaR and ES are positive losses in the returns' units at a tail probability
|
|
122
|
+
`level` (0.01 for 99 %). `tailRisk` gives the one-step answer in closed form
|
|
123
|
+
from μ, s_{T+1} and the innovations (normal, or standardised t with ν); its
|
|
124
|
+
quantiles and tail means agree with scipy. Over several steps the cumulative
|
|
125
|
+
return under GARCH is not normal, so `simulatedTailRisk` reads VaR and ES off
|
|
126
|
+
`simulate`'s paths. `varBacktest` walks a one-step VaR forward and
|
|
127
|
+
`coverageTest` grades the violations: Kupiec's unconditional coverage,
|
|
128
|
+
Christoffersen's independence, and both together.
|
|
129
|
+
|
|
130
|
+
## The optimiser, and why it has six starts
|
|
131
|
+
|
|
132
|
+
BFGS with an analytic gradient runs in an unconstrained space where every point
|
|
133
|
+
is a valid, stationary model: ω = e^{x1}·var(r), P = σ(x2) = α+γ/2+β,
|
|
134
|
+
β = P·σ(x3), γ = 2q·tanh(x4), α = q − γ/2.
|
|
135
|
+
|
|
136
|
+
It runs from **one start per persistence level** (0.5 … 0.999), plus the
|
|
137
|
+
previous origin's optimum during a walk-forward, and the highest likelihood
|
|
138
|
+
wins. Every distinct optimum the starts reached is returned as `modes`, best
|
|
139
|
+
first: on short or jumpy series (small-cap biotech, for one) the GARCH(1,1)
|
|
140
|
+
likelihood has **several modes**, and `arch`, with one start, stops in the
|
|
141
|
+
nearest one. There this package finds a higher likelihood than `arch` (+18 to
|
|
142
|
+
+56 log-likelihood on such series); where there is one mode, it lands on
|
|
143
|
+
`arch`'s numbers. `asOfFit` returns the rivals within a log-likelihood distance
|
|
144
|
+
you ask for, each with its own variance path and forecast.
|
|
145
|
+
|
|
146
|
+
What was tried and failed:
|
|
147
|
+
|
|
148
|
+
- **Nelder–Mead with a warm start** did not reach the optimum: warm and cold fits differed at most origins of a walk-forward.
|
|
149
|
+
- **A numerical gradient** reached the same optimum 4–8× slower than `arch`.
|
|
150
|
+
- **Fewer grid starts when a warm start is available.** A warm start alone misses the best optimum at ~1 % of origins on large caps and ETFs, but at 5–53 % on biotech. The grid runs at every origin.
|
|
151
|
+
|
|
152
|
+
## Walk-forward segments and the pool
|
|
153
|
+
|
|
154
|
+
A walk-forward is cut into **segments of 32 consecutive origins**
|
|
155
|
+
(`SEGMENT_ORIGINS`). Inside a segment each origin also starts from the previous
|
|
156
|
+
one's optimum; a segment's first origin starts from the grid alone. The cut
|
|
157
|
+
depends on the history only, so `walkForwardValidation` in-process and
|
|
158
|
+
`volatilityForecastInWorkers` on a pool of any size give the same answer, to
|
|
159
|
+
the bit. Each fit reads its train window as a `subarray` of one
|
|
160
|
+
`Float64Array` — no copy per origin.
|
|
161
|
+
|
|
162
|
+
`GarchPool` keeps up to *size* workers (default: every core but one), started
|
|
163
|
+
on demand and reused; an idle worker does not keep the process alive. A call's
|
|
164
|
+
tasks share one deadline: past it, or on one task's refusal, its queued tasks
|
|
165
|
+
are dropped and its running workers terminated, and the pool carries on. The
|
|
166
|
+
heaviest segments (the longest train windows) are dispatched first.
|
|
167
|
+
|
|
168
|
+
`npm run bench` measures it on SPY's closes from the `arch` fixture (8 469
|
|
169
|
+
returns, horizon 20) and asserts the pool's answer equals the in-process one.
|
|
170
|
+
On an i7-13700K, Node 24, 23 workers, one core → pool:
|
|
171
|
+
|
|
172
|
+
| job | one core | pool |
|
|
173
|
+
|---|---|---|
|
|
174
|
+
| garch | 15.8 s | 2.3 s |
|
|
175
|
+
| gjr | 42.4 s | 4.0 s |
|
|
176
|
+
| egarch | 51.7 s | 3.9 s |
|
|
177
|
+
| weighted (`-- --weighted`) | 122 s | 11.2 s |
|
|
178
|
+
|
|
179
|
+
### In the browser
|
|
180
|
+
|
|
181
|
+
`@krzysztofsztompka/garch/web` is the same pool on Web Workers: `GarchWebPool`
|
|
182
|
+
(default: every logical core but one) and the same calls —
|
|
183
|
+
`volatilityForecastInWorkers`, `fitsAtInWorkers`, `weightedForecastInWorkers`,
|
|
184
|
+
`runInWorker` — with the in-process answer. The worker is started as
|
|
185
|
+
`new Worker(new URL('./worker-web.js', import.meta.url), { type: 'module' })`,
|
|
186
|
+
written out literally, so Vite and webpack bundle it without configuration;
|
|
187
|
+
both were checked in Chrome. A bundler that needs the worker its own way passes
|
|
188
|
+
`new GarchWebPool(size, () => new Worker(...))`. Node's pool and this one share
|
|
189
|
+
one implementation of the queue, deadline and cancellation (`WorkerPool`).
|
|
190
|
+
|
|
191
|
+
## Several models, weighted by their past loss
|
|
192
|
+
|
|
193
|
+
`weightedForecast` / `weightedForecastInWorkers` fit {garch, gjr} × {normal, t}
|
|
194
|
+
(`WEIGHTED_MODELS`). Each model is walked forward, and the models are combined
|
|
195
|
+
at every origin by weights ∝ 1 / the model's mean QLIKE over the windows that
|
|
196
|
+
had **ended** by that origin. The weights are equal until each model has 20
|
|
197
|
+
such windows (`WEIGHT_BURN_IN`). The as-of forecast and variance path are
|
|
198
|
+
combined with the weights from every window.
|
|
199
|
+
|
|
200
|
+
The answer carries each model's as-of fit, its own validation and its weight,
|
|
201
|
+
next to the weighted forecast and its validation against the naive baseline —
|
|
202
|
+
and `walkForward`: at every origin, each model's fit (parameters, last
|
|
203
|
+
variance, last residual) and the weights in force there, so a simulation from
|
|
204
|
+
each origin needs no second walk-forward.
|
|
205
|
+
|
|
206
|
+
Why weights and not a pick: on 15 daily series, picking the model with the
|
|
207
|
+
lowest past loss beat the default on 8 and cost up to +47 % where the ranking
|
|
208
|
+
flipped; weighting averaged −5 % with a worst case of +4 %. Trim quantized
|
|
209
|
+
history (`quantizedUntil`) before putting t in the mix.
|
|
210
|
+
|
|
211
|
+
## Against the npm `garch` package
|
|
212
|
+
|
|
213
|
+
Judged on 49 daily series by four conclusions drawn from the output — is σ̂
|
|
214
|
+
worth quoting, the volatility regime, an implied-vs-realised variance verdict,
|
|
215
|
+
and the coverage of a forecast range — against what happened:
|
|
216
|
+
|
|
217
|
+
| class | npm `garch` 2.0.0 | this package |
|
|
218
|
+
|---|---|---|
|
|
219
|
+
| large cap (14) | fails three of four | passes |
|
|
220
|
+
| small/mid-cap biotech (18) | fails all four | fails on 4 names, all where `arch`'s lower-likelihood mode happened to read the future better |
|
|
221
|
+
| small-cap tools (4) | fails one | passes |
|
|
222
|
+
| ETF / index (6) | passes | passes |
|
|
223
|
+
|
|
224
|
+
npm `garch`'s variance forecast collapses to ≈ 0 at single origins (QLIKE 10¹⁰).
|
|
225
|
+
|
|
226
|
+
## API
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
import { archLm, egarchForecast, fitGarch, forecastVariance, ljungBox, quantizedUntil, simulate, simulatedTailRisk, standardizedResiduals, tailRisk, varBacktest, walkForwardValidation, volatilityForecast, fitsAt } from '@krzysztofsztompka/garch';
|
|
230
|
+
import { fitsAtInWorkers, runInWorker, volatilityForecastInWorkers, weightedForecastInWorkers } from '@krzysztofsztompka/garch/node';
|
|
231
|
+
|
|
232
|
+
const fit = fitGarch(returns, { model: 'gjr', inference: true });
|
|
233
|
+
fit.modes; // every optimum reached, best first
|
|
234
|
+
fit.aic; fit.bic; // as arch reports them; a ν held on a bound is not counted
|
|
235
|
+
fitGarch(returns, { model: 'egarch', inference: true }); // γ < 0: leverage; normal innovations only
|
|
236
|
+
fitGarch(returns, { model: 'gjr', mean: 'zero' }); // μ held at 0
|
|
237
|
+
fitGarch(returns, { model: 'gjr', dist: 'skewt', inference: true }); // → params.nu, params.lambda; λ < 0: a fatter loss tail
|
|
238
|
+
fitGarch(returns.slice(quantizedUntil(returns)), { dist: 't' }); // Student-t on the fine-grained history
|
|
239
|
+
// → params.nu, nuBound: 'lower' | 'upper' | null
|
|
240
|
+
|
|
241
|
+
// paths forward from where the fit stands (seeded); a FitAt from fitsAt spreads in as it is
|
|
242
|
+
simulate({ model: 'gjr', params: fit.params, lastVariance: fit.conditionalVariance.at(-1), lastResidual: fit.lastResidual,
|
|
243
|
+
steps: 20, paths: 10_000, seed: 1 }); // → { returns, variance }: Float64Array per path
|
|
244
|
+
|
|
245
|
+
const z = standardizedResiduals(fit, returns);
|
|
246
|
+
ljungBox(z.map((v) => v * v), 10); // → { statistic, df, pValue }: ARCH left in the residuals?
|
|
247
|
+
archLm(z, 10);
|
|
248
|
+
|
|
249
|
+
tailRisk({ mean: fit.params.mu, variance: forecastVariance(fit, 1)[0], nu: fit.params.nu, level: 0.01 });
|
|
250
|
+
simulatedTailRisk(simulate({ ... , steps: 10, paths: 100_000, seed: 1 }), 0.01); // 10-day VaR, ES
|
|
251
|
+
varBacktest(returns, { model: 'gjr', dist: 't', level: 0.01, step: 1 }); // → violations, rate, Kupiec, Christoffersen
|
|
252
|
+
|
|
253
|
+
// Node only: the walk-forward's segments across a pool of workers — the in-process answer, faster
|
|
254
|
+
await volatilityForecastInWorkers({ returns, model: 'gjr', horizon: 20, rivalsWithin: 1.92 }, { timeoutMs: 120_000 });
|
|
255
|
+
await fitsAtInWorkers({ returns, ends: [999, 1499] });
|
|
256
|
+
await weightedForecastInWorkers({ returns, horizon: 20 });
|
|
257
|
+
// → forecast, conditionalVariance, models[i].{spec, asOf, validation, weight}, validation, walkForward[j].{origin, weights, fits}
|
|
258
|
+
await runInWorker('asOfFit', { returns, horizon: 20 }); // any one job on one worker
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
`fitGarch` throws `GarchInputError` on input it cannot fit. The pool's calls
|
|
262
|
+
reject with one of three errors, so a caller can word each:
|
|
263
|
+
|
|
264
|
+
- `GarchJobInputError`: the input cannot be fitted.
|
|
265
|
+
- `GarchJobTimeoutError`: the call ran past its deadline, and its workers are terminated.
|
|
266
|
+
- `GarchJobFailedError`: any other failure.
|
|
267
|
+
|
|
268
|
+
## Tests
|
|
269
|
+
|
|
270
|
+
`npm test` compiles `src/` and `test/` with tsc and runs them with `node --test`.
|
|
271
|
+
There is no test framework dependency.
|
|
272
|
+
`npm run coverage` (Node ≥ 22.8) adds Node's own coverage of the compiled
|
|
273
|
+
code and fails under 95 % of lines, 85 % of branches or 80 % of functions; CI
|
|
274
|
+
runs it on Node 22 and 24. Code that runs only inside a worker is counted where
|
|
275
|
+
the tests also run it in-process.
|
|
276
|
+
|
|
277
|
+
- **The analytic gradient** matches its finite difference.
|
|
278
|
+
- **Simulated paths.** Parameters are recovered on simulated GARCH and GJR paths. A real leverage term is significant and an absent one is not.
|
|
279
|
+
- **Every fit is a valid, stationary model.** Inputs that cannot be fitted are refused.
|
|
280
|
+
- **Forecasts** decay to the unconditional variance.
|
|
281
|
+
- **Walk-forward windows** never leak from test into train.
|
|
282
|
+
- **Simulation.** A seed gives the same paths; the paths' mean variance matches the analytic forecast for GJR (normal and t) and EGARCH, and their innovations have unit variance.
|
|
283
|
+
- **Value at risk.** Normal and t quantiles, VaR and ES against scipy; the one-step VaR read off simulated paths; Kupiec and Christoffersen statistics against a hand computation; a walked-forward VaR covers at its level when the model is the truth.
|
|
284
|
+
- **Residual diagnostics.** Ljung–Box and ARCH-LM against statsmodels on the fixture series; the χ² tail against scipy; ARCH found in a GARCH path and none left in its standardised residuals.
|
|
285
|
+
- **Against `arch`.** `test/fixtures/arch-2026-09-25.json` holds real IOVA and SPY closes (to 2026-09-23) and `arch`'s own answers. The package must match the parameters and σ̂ to 1e-3 / 1e-4, γ's p-value to 2 %, and IOVA's walk-forward QLIKE to 1e-3.
|
|
286
|
+
- **The fixtures are regenerated, not hand-written.** `scripts/arch-fixtures/generate.py` reads the closes from the fixture and rewrites `arch`'s answers in both files, with the versions in its `requirements.txt` (`arch` 8.0.0) and a seeded simulation, recording both in each file's `source`:
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
python3 -m venv .venv && .venv/bin/pip install -r scripts/arch-fixtures/requirements.txt
|
|
290
|
+
.venv/bin/python scripts/arch-fixtures/generate.py
|
|
291
|
+
```
|
|
292
|
+
- **Worker round trip.** The same answer as in-process, an input refusal kept as an input refusal, and a timeout that terminates the worker.
|
|
293
|
+
- **The modes.** Best first, the fit being the first; the warm start's optimum marked; rivals only within the asked distance.
|
|
294
|
+
- **Student-t.** The gradient against finite differences, ν included. ν is recovered on a simulated t-GARCH path. A ν below the floor is held and converges, and its standard error is NaN. Robust SE come with a free ν.
|
|
295
|
+
- **Skewed t.** `arch`'s `SkewStudent` quantiles and partial moments to 1e-12; the gradient against finite differences, ν and λ included; ν and λ recovered on a simulated path; SPY's garch and gjr fits against `arch`'s; the skewed VaR and ES against the model's own paths; the simulated variance against the forecast with the exact down share.
|
|
296
|
+
- **Weighting.** Equal weights before the burn-in; the better model dominates after it; changing a forecast whose window ends after an origin does not move that origin's weights; the pool's answer equals the in-process one.
|
|
297
|
+
- **EGARCH.** The gradient against finite differences. A simulated path's parameters, leverage sign included. The closed-form forecast against its own Monte Carlo. `arch` 8.0.0's optimum, γ p-value and simulated forecast. A t EGARCH refused.
|
|
298
|
+
- **Quantized history.** On IOVA's frozen closes: the cut lands where the reverse-split cents end, t degenerates before it (ω ≈ 0 while `converged`), and fits cleanly after it.
|
|
299
|
+
- **The web pool.** Through a Web Worker made of a worker thread: the walk-forward identical to in-process, an input refusal and a deadline carried as on Node.
|
|
300
|
+
- **The pool.** The walk-forward and `fitsAt` bit-identical to in-process on pools of 1 and 3 workers; one refused job fails its whole call; the pool keeps working after a call timed out.
|
|
301
|
+
|
|
302
|
+
## Build
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
npm install && npm run build # → dist/cjs and dist/esm, each with .d.ts
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## Releasing
|
|
309
|
+
|
|
310
|
+
Move `CHANGELOG.md`'s `Unreleased` entries under the new version, set
|
|
311
|
+
`package.json`'s version to match, commit, then push a tag `v<version>`. The
|
|
312
|
+
`release` workflow refuses a tag that differs from the version or has no
|
|
313
|
+
changelog section; otherwise it tests, publishes to npm with provenance and
|
|
314
|
+
makes the GitHub Release from that section. npm trusts the workflow as a
|
|
315
|
+
trusted publisher (`uhlryk/garch`, `release.yml`), so no token is stored.
|
|
316
|
+
|
|
317
|
+
## Licence
|
|
318
|
+
|
|
319
|
+
MIT
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { GarchFit } from './fit.js';
|
|
2
|
+
/**
|
|
3
|
+
* Is anything left for the model to explain? A fitted model's standardised
|
|
4
|
+
* residuals z_t = (r_t − μ) / √s_t should be serially uncorrelated, and so
|
|
5
|
+
* should z²_t — what remains of an ARCH effect shows up there.
|
|
6
|
+
*/
|
|
7
|
+
/** z_t = (r_t − μ) / √s_t for every observation of the fit. */
|
|
8
|
+
export declare function standardizedResiduals(fit: GarchFit, returns: ArrayLike<number>): Float64Array;
|
|
9
|
+
export type TestResult = {
|
|
10
|
+
readonly statistic: number;
|
|
11
|
+
/** Degrees of freedom of the χ² it is referred to. */
|
|
12
|
+
readonly df: number;
|
|
13
|
+
readonly pValue: number;
|
|
14
|
+
};
|
|
15
|
+
/** P(χ²_df > x), computed in the upper tail (no 1 − P cancellation for a large statistic). */
|
|
16
|
+
export declare function chiSquareSurvival(x: number, df: number): number;
|
|
17
|
+
/**
|
|
18
|
+
* Ljung–Box Q = n(n + 2)·Σ_{k ≤ lags} ρ̂²_k / (n − k) against χ²(lags − modelDf),
|
|
19
|
+
* as statsmodels' `acorr_ljungbox` (autocorrelations of the demeaned series
|
|
20
|
+
* over its total sum of squares). Run it on z for the mean, on z² for what
|
|
21
|
+
* remains of the variance dynamics.
|
|
22
|
+
*/
|
|
23
|
+
export declare function ljungBox(series: ArrayLike<number>, lags: number, modelDf?: number): TestResult;
|
|
24
|
+
/**
|
|
25
|
+
* Engle's ARCH-LM test: e²_t regressed on a constant and e²_{t−1} … e²_{t−lags};
|
|
26
|
+
* LM = (n − lags)·R² against χ²(lags), as statsmodels' `het_arch`. On the
|
|
27
|
+
* standardised residuals, a small p-value says the model left ARCH behind.
|
|
28
|
+
*/
|
|
29
|
+
export declare function archLm(series: ArrayLike<number>, lags: number): TestResult;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.standardizedResiduals = standardizedResiduals;
|
|
4
|
+
exports.chiSquareSurvival = chiSquareSurvival;
|
|
5
|
+
exports.ljungBox = ljungBox;
|
|
6
|
+
exports.archLm = archLm;
|
|
7
|
+
const inference_js_1 = require("./inference.js");
|
|
8
|
+
const special_js_1 = require("./special.js");
|
|
9
|
+
/**
|
|
10
|
+
* Is anything left for the model to explain? A fitted model's standardised
|
|
11
|
+
* residuals z_t = (r_t − μ) / √s_t should be serially uncorrelated, and so
|
|
12
|
+
* should z²_t — what remains of an ARCH effect shows up there.
|
|
13
|
+
*/
|
|
14
|
+
/** z_t = (r_t − μ) / √s_t for every observation of the fit. */
|
|
15
|
+
function standardizedResiduals(fit, returns) {
|
|
16
|
+
const s = fit.conditionalVariance;
|
|
17
|
+
if (returns.length !== s.length)
|
|
18
|
+
throw new RangeError(`the fit has ${s.length} variances, the returns are ${returns.length}`);
|
|
19
|
+
const z = new Float64Array(s.length);
|
|
20
|
+
for (let t = 0; t < s.length; t += 1)
|
|
21
|
+
z[t] = (returns[t] - fit.params.mu) / Math.sqrt(s[t]);
|
|
22
|
+
return z;
|
|
23
|
+
}
|
|
24
|
+
/** The regularised lower incomplete gamma P(a, x): its series below a + 1, its continued fraction above. */
|
|
25
|
+
function lowerGamma(a, x) {
|
|
26
|
+
if (x <= 0)
|
|
27
|
+
return 0;
|
|
28
|
+
const front = Math.exp(-x + a * Math.log(x) - (0, special_js_1.logGamma)(a));
|
|
29
|
+
if (x < a + 1) {
|
|
30
|
+
let term = 1 / a;
|
|
31
|
+
let sum = term;
|
|
32
|
+
for (let n = 1; n < 1000; n += 1) {
|
|
33
|
+
term *= x / (a + n);
|
|
34
|
+
sum += term;
|
|
35
|
+
if (Math.abs(term) < Math.abs(sum) * 1e-16)
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
return sum * front;
|
|
39
|
+
}
|
|
40
|
+
return 1 - upperFraction(a, x) * front;
|
|
41
|
+
}
|
|
42
|
+
/** Lentz's continued fraction for Γ(a, x) / (e^{−x} x^a / Γ(a)). */
|
|
43
|
+
function upperFraction(a, x) {
|
|
44
|
+
const tiny = 1e-300;
|
|
45
|
+
let b = x + 1 - a;
|
|
46
|
+
let c = 1 / tiny;
|
|
47
|
+
let d = 1 / b;
|
|
48
|
+
let h = d;
|
|
49
|
+
for (let i = 1; i < 1000; i += 1) {
|
|
50
|
+
const an = -i * (i - a);
|
|
51
|
+
b += 2;
|
|
52
|
+
d = an * d + b;
|
|
53
|
+
if (Math.abs(d) < tiny)
|
|
54
|
+
d = tiny;
|
|
55
|
+
c = b + an / c;
|
|
56
|
+
if (Math.abs(c) < tiny)
|
|
57
|
+
c = tiny;
|
|
58
|
+
d = 1 / d;
|
|
59
|
+
const delta = d * c;
|
|
60
|
+
h *= delta;
|
|
61
|
+
if (Math.abs(delta - 1) < 1e-16)
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
return h;
|
|
65
|
+
}
|
|
66
|
+
/** P(χ²_df > x), computed in the upper tail (no 1 − P cancellation for a large statistic). */
|
|
67
|
+
function chiSquareSurvival(x, df) {
|
|
68
|
+
if (!(df > 0))
|
|
69
|
+
throw new RangeError(`df must be positive, got ${df}`);
|
|
70
|
+
if (!(x > 0))
|
|
71
|
+
return 1;
|
|
72
|
+
const a = df / 2;
|
|
73
|
+
const h = x / 2;
|
|
74
|
+
if (h < a + 1)
|
|
75
|
+
return 1 - lowerGamma(a, h);
|
|
76
|
+
return upperFraction(a, h) * Math.exp(-h + a * Math.log(h) - (0, special_js_1.logGamma)(a));
|
|
77
|
+
}
|
|
78
|
+
const checkLags = (lags, n) => {
|
|
79
|
+
if (!(Number.isInteger(lags) && lags >= 1))
|
|
80
|
+
throw new RangeError(`lags must be a positive integer, got ${lags}`);
|
|
81
|
+
if (lags >= n)
|
|
82
|
+
throw new RangeError(`${lags} lags need more than ${n} observations`);
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Ljung–Box Q = n(n + 2)·Σ_{k ≤ lags} ρ̂²_k / (n − k) against χ²(lags − modelDf),
|
|
86
|
+
* as statsmodels' `acorr_ljungbox` (autocorrelations of the demeaned series
|
|
87
|
+
* over its total sum of squares). Run it on z for the mean, on z² for what
|
|
88
|
+
* remains of the variance dynamics.
|
|
89
|
+
*/
|
|
90
|
+
function ljungBox(series, lags, modelDf = 0) {
|
|
91
|
+
const n = series.length;
|
|
92
|
+
checkLags(lags, n);
|
|
93
|
+
if (!(Number.isInteger(modelDf) && modelDf >= 0 && modelDf < lags))
|
|
94
|
+
throw new RangeError(`modelDf must be an integer in [0, lags), got ${modelDf}`);
|
|
95
|
+
let mean = 0;
|
|
96
|
+
for (let t = 0; t < n; t += 1)
|
|
97
|
+
mean += series[t];
|
|
98
|
+
mean /= n;
|
|
99
|
+
let ss = 0;
|
|
100
|
+
for (let t = 0; t < n; t += 1)
|
|
101
|
+
ss += (series[t] - mean) ** 2;
|
|
102
|
+
let q = 0;
|
|
103
|
+
for (let k = 1; k <= lags; k += 1) {
|
|
104
|
+
let c = 0;
|
|
105
|
+
for (let t = k; t < n; t += 1)
|
|
106
|
+
c += (series[t] - mean) * (series[t - k] - mean);
|
|
107
|
+
q += (c / ss) ** 2 / (n - k);
|
|
108
|
+
}
|
|
109
|
+
const statistic = n * (n + 2) * q;
|
|
110
|
+
const df = lags - modelDf;
|
|
111
|
+
return { statistic, df, pValue: chiSquareSurvival(statistic, df) };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Engle's ARCH-LM test: e²_t regressed on a constant and e²_{t−1} … e²_{t−lags};
|
|
115
|
+
* LM = (n − lags)·R² against χ²(lags), as statsmodels' `het_arch`. On the
|
|
116
|
+
* standardised residuals, a small p-value says the model left ARCH behind.
|
|
117
|
+
*/
|
|
118
|
+
function archLm(series, lags) {
|
|
119
|
+
const n = series.length;
|
|
120
|
+
checkLags(lags, n);
|
|
121
|
+
const m = lags + 1;
|
|
122
|
+
const xtx = Array.from({ length: m }, () => new Array(m).fill(0));
|
|
123
|
+
const xty = new Array(m).fill(0);
|
|
124
|
+
const row = new Array(m);
|
|
125
|
+
let sumY = 0;
|
|
126
|
+
let sumY2 = 0;
|
|
127
|
+
for (let t = lags; t < n; t += 1) {
|
|
128
|
+
const y = series[t] ** 2;
|
|
129
|
+
row[0] = 1;
|
|
130
|
+
for (let k = 1; k <= lags; k += 1)
|
|
131
|
+
row[k] = series[t - k] ** 2;
|
|
132
|
+
for (let i = 0; i < m; i += 1) {
|
|
133
|
+
xty[i] += row[i] * y;
|
|
134
|
+
for (let j = 0; j <= i; j += 1)
|
|
135
|
+
xtx[i][j] += row[i] * row[j];
|
|
136
|
+
}
|
|
137
|
+
sumY += y;
|
|
138
|
+
sumY2 += y * y;
|
|
139
|
+
}
|
|
140
|
+
for (let i = 0; i < m; i += 1)
|
|
141
|
+
for (let j = i + 1; j < m; j += 1)
|
|
142
|
+
xtx[i][j] = xtx[j][i];
|
|
143
|
+
const inverse = (0, inference_js_1.invert)(xtx);
|
|
144
|
+
if (!inverse)
|
|
145
|
+
throw new RangeError('the lagged squares are collinear');
|
|
146
|
+
const beta = inverse.map((r) => r.reduce((a, v, j) => a + v * xty[j], 0));
|
|
147
|
+
const nobs = n - lags;
|
|
148
|
+
// R² = (β'X'y − n·ȳ²) / (y'y − n·ȳ²)
|
|
149
|
+
const explained = beta.reduce((a, b, i) => a + b * xty[i], 0) - (sumY * sumY) / nobs;
|
|
150
|
+
const total = sumY2 - (sumY * sumY) / nobs;
|
|
151
|
+
const statistic = nobs * (explained / total);
|
|
152
|
+
return { statistic, df: lags, pValue: chiSquareSurvival(statistic, lags) };
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=diagnostics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diagnostics.js","sourceRoot":"","sources":["../../src/diagnostics.ts"],"names":[],"mappings":";;AAWA,sDAOC;AAiDD,8CAOC;AAaD,4BAmBC;AAOD,wBA8BC;AA9ID,iDAAwC;AACxC,6CAAwC;AAExC;;;;GAIG;AAEH,+DAA+D;AAC/D,SAAgB,qBAAqB,CAAC,GAAa,EAAE,OAA0B;IAC7E,MAAM,CAAC,GAAG,GAAG,CAAC,mBAAmB,CAAC;IAClC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM;QAC7B,MAAM,IAAI,UAAU,CAAC,eAAe,CAAC,CAAC,MAAM,+BAA+B,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC/F,MAAM,CAAC,GAAG,IAAI,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;IACrC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC;QAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5F,OAAO,CAAC,CAAC;AACX,CAAC;AASD,4GAA4G;AAC5G,SAAS,UAAU,CAAC,CAAS,EAAE,CAAS;IACtC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC;IACrB,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,IAAA,qBAAQ,EAAC,CAAC,CAAC,CAAC,CAAC;IAC3D,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;QACd,IAAI,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QACjB,IAAI,GAAG,GAAG,IAAI,CAAC;QACf,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;YACjC,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YACpB,GAAG,IAAI,IAAI,CAAC;YACZ,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK;gBAAE,MAAM;QACpD,CAAC;QACD,OAAO,GAAG,GAAG,KAAK,CAAC;IACrB,CAAC;IACD,OAAO,CAAC,GAAG,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,KAAK,CAAC;AACzC,CAAC;AAED,oEAAoE;AACpE,SAAS,aAAa,CAAC,CAAS,EAAE,CAAS;IACzC,MAAM,IAAI,GAAG,MAAM,CAAC;IACpB,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAClB,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;IACjB,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACd,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACjC,MAAM,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QACxB,CAAC,IAAI,CAAC,CAAC;QACP,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;QACf,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI;YAAE,CAAC,GAAG,IAAI,CAAC;QACjC,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QACf,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI;YAAE,CAAC,GAAG,IAAI,CAAC;QACjC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACV,MAAM,KAAK,GAAG,CAAC,GAAG,CAAC,CAAC;QACpB,CAAC,IAAI,KAAK,CAAC;QACX,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,KAAK;YAAE,MAAM;IACzC,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED,8FAA8F;AAC9F,SAAgB,iBAAiB,CAAC,CAAS,EAAE,EAAU;IACrD,IAAI,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,4BAA4B,EAAE,EAAE,CAAC,CAAC;IACtE,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QAAE,OAAO,CAAC,CAAC;IACvB,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IACjB,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAChB,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;QAAE,OAAO,CAAC,GAAG,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC3C,OAAO,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,IAAA,qBAAQ,EAAC,CAAC,CAAC,CAAC,CAAC;AAC5E,CAAC;AAED,MAAM,SAAS,GAAG,CAAC,IAAY,EAAE,CAAS,EAAE,EAAE;IAC5C,IAAI,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,wCAAwC,IAAI,EAAE,CAAC,CAAC;IACjH,IAAI,IAAI,IAAI,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,GAAG,IAAI,wBAAwB,CAAC,eAAe,CAAC,CAAC;AACvF,CAAC,CAAC;AAEF;;;;;GAKG;AACH,SAAgB,QAAQ,CAAC,MAAyB,EAAE,IAAY,EAAE,OAAO,GAAG,CAAC;IAC3E,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC;IACxB,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IACnB,IAAI,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,OAAO,IAAI,CAAC,IAAI,OAAO,GAAG,IAAI,CAAC;QAChE,MAAM,IAAI,UAAU,CAAC,gDAAgD,OAAO,EAAE,CAAC,CAAC;IAClF,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC;QAAE,IAAI,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC;IACjD,IAAI,IAAI,CAAC,CAAC;IACV,IAAI,EAAE,GAAG,CAAC,CAAC;IACX,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC;QAAE,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC;IAC7D,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAClC,IAAI,CAAC,GAAG,CAAC,CAAC;QACV,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC;YAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;QAChF,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAC/B,CAAC;IACD,MAAM,SAAS,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;IAClC,MAAM,EAAE,GAAG,IAAI,GAAG,OAAO,CAAC;IAC1B,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,EAAE,iBAAiB,CAAC,SAAS,EAAE,EAAE,CAAC,EAAE,CAAC;AACrE,CAAC;AAED;;;;GAIG;AACH,SAAgB,MAAM,CAAC,MAAyB,EAAE,IAAY;IAC5D,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC;IACxB,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IACnB,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC;IACnB,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,IAAI,KAAK,CAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1E,MAAM,GAAG,GAAG,IAAI,KAAK,CAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACzC,MAAM,GAAG,GAAG,IAAI,KAAK,CAAS,CAAC,CAAC,CAAC;IACjC,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,IAAI,CAAC,GAAG,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACjC,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACzB,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QACX,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,IAAI,CAAC;YAAE,GAAG,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC;QAC/D,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;YAC9B,GAAG,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;YACrB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC;gBAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;QAC/D,CAAC;QACD,IAAI,IAAI,CAAC,CAAC;QACV,KAAK,IAAI,CAAC,GAAG,CAAC,CAAC;IACjB,CAAC;IACD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC;QAAE,KAAK,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC;YAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACxF,MAAM,OAAO,GAAG,IAAA,qBAAM,EAAC,GAAG,CAAC,CAAC;IAC5B,IAAI,CAAC,OAAO;QAAE,MAAM,IAAI,UAAU,CAAC,kCAAkC,CAAC,CAAC;IACvE,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC1E,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC;IACtB,qCAAqC;IACrC,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;IACrF,MAAM,KAAK,GAAG,KAAK,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;IAC3C,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,SAAS,GAAG,KAAK,CAAC,CAAC;IAC7C,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,iBAAiB,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,CAAC;AAC7E,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { GarchParams } from './model.js';
|
|
2
|
+
/**
|
|
3
|
+
* EGARCH(1,1,1) — Nelson's exponential GARCH with a constant mean and normal
|
|
4
|
+
* innovations, `arch`'s specification (`vol="EGARCH", p=1, o=1, q=1`):
|
|
5
|
+
*
|
|
6
|
+
* r_t = μ + e_t, e_t ~ N(0, s_t), z_t = e_t / √s_t
|
|
7
|
+
* ln s_t = ω + α·(|z_{t−1}| − √(2/π)) + γ·z_{t−1} + β·ln s_{t−1}
|
|
8
|
+
*
|
|
9
|
+
* The log keeps s_t positive with no sign constraint on α or γ, and the
|
|
10
|
+
* asymmetry is γ itself: γ < 0 means a down move raises tomorrow's variance
|
|
11
|
+
* more than an up move of the same size. The recursion starts from the log of
|
|
12
|
+
* `arch`'s backcast, with no shock term at t = 0 (as `arch`).
|
|
13
|
+
*
|
|
14
|
+
* **Normal innovations only.** The multi-step variance forecast is
|
|
15
|
+
* E[exp(Σ βʲ·g(z))], and for Student-t innovations E[exp(a·|z|)] is infinite
|
|
16
|
+
* for every a > 0 — the expected variance two steps ahead does not exist.
|
|
17
|
+
* `arch` simulates it and gets a finite, unstable number; here a t EGARCH is
|
|
18
|
+
* refused instead.
|
|
19
|
+
*/
|
|
20
|
+
/** E|z| for a standard normal. */
|
|
21
|
+
export declare const EGARCH_ABS_MEAN: number;
|
|
22
|
+
/**
|
|
23
|
+
* Runs the log-variance recursion from `lnBackcast`. Fills `variance` (s_t)
|
|
24
|
+
* and, when given, `contributions`; returns the log-likelihood (−∞ when the
|
|
25
|
+
* recursion leaves the floating-point range).
|
|
26
|
+
*/
|
|
27
|
+
export declare function egarchLogLikelihood(returns: ArrayLike<number>, p: GarchParams, lnBackcast: number, variance: Float64Array, contributions?: Float64Array): number;
|
|
28
|
+
/**
|
|
29
|
+
* The log-likelihood and its analytic gradient with respect to
|
|
30
|
+
* (μ, ω, α, γ, β); with `scores`, each observation's gradient, a row per
|
|
31
|
+
* parameter.
|
|
32
|
+
*
|
|
33
|
+
* With L = ln s and k = α·sign(z) + γ, the recursion's derivative is
|
|
34
|
+
* dL_t = ∂_direct + (β − ½·k·z_{t−1})·dL_{t−1} + k·e^{−L_{t−1}/2}·de_{t−1}
|
|
35
|
+
* (de/dμ = −1), and dl/dL = −½(1 − e²/s).
|
|
36
|
+
*/
|
|
37
|
+
export declare function egarchLogLikelihoodGradient(returns: ArrayLike<number>, p: GarchParams, lnBackcast: number, scores?: Float64Array[]): {
|
|
38
|
+
value: number;
|
|
39
|
+
gradient: number[];
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* s_{T+1} … s_{T+h}, exact under normal innovations: the first step from the
|
|
43
|
+
* observed last shock; then E[s_{T+k}] = exp(m_k)·Π_{j=0}^{k−2} E[exp(βʲ·g(z))],
|
|
44
|
+
* m_k = ω + β·m_{k−1}, m_1 = ln s_{T+1}.
|
|
45
|
+
*/
|
|
46
|
+
export declare function egarchForecast(p: GarchParams, lastVariance: number, lastResidual: number, horizon: number): number[];
|