@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.
Files changed (178) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +318 -2
  3. package/dist/cjs/diagnostics.d.ts +29 -0
  4. package/dist/cjs/diagnostics.js +154 -0
  5. package/dist/cjs/diagnostics.js.map +1 -0
  6. package/dist/cjs/egarch.d.ts +46 -0
  7. package/dist/cjs/egarch.js +150 -0
  8. package/dist/cjs/egarch.js.map +1 -0
  9. package/dist/cjs/fit.d.ts +84 -0
  10. package/dist/cjs/fit.js +354 -0
  11. package/dist/cjs/fit.js.map +1 -0
  12. package/dist/cjs/forecast.d.ts +8 -0
  13. package/dist/cjs/forecast.js +29 -0
  14. package/dist/cjs/forecast.js.map +1 -0
  15. package/dist/cjs/index.d.ts +14 -0
  16. package/dist/cjs/index.js +67 -0
  17. package/dist/cjs/index.js.map +1 -0
  18. package/dist/cjs/inference.d.ts +29 -0
  19. package/dist/cjs/inference.js +110 -0
  20. package/dist/cjs/inference.js.map +1 -0
  21. package/dist/cjs/jobs.d.ts +78 -0
  22. package/dist/cjs/jobs.js +107 -0
  23. package/dist/cjs/jobs.js.map +1 -0
  24. package/dist/cjs/model.d.ts +60 -0
  25. package/dist/cjs/model.js +194 -0
  26. package/dist/cjs/model.js.map +1 -0
  27. package/dist/cjs/node.d.ts +29 -0
  28. package/dist/cjs/node.js +75 -0
  29. package/dist/cjs/node.js.map +1 -0
  30. package/dist/cjs/optimize.d.ts +11 -0
  31. package/dist/cjs/optimize.js +61 -0
  32. package/dist/cjs/optimize.js.map +1 -0
  33. package/dist/cjs/package.json +1 -0
  34. package/dist/cjs/pool.d.ts +84 -0
  35. package/dist/cjs/pool.js +257 -0
  36. package/dist/cjs/pool.js.map +1 -0
  37. package/dist/cjs/quality.d.ts +24 -0
  38. package/dist/cjs/quality.js +46 -0
  39. package/dist/cjs/quality.js.map +1 -0
  40. package/dist/cjs/registry.d.ts +17 -0
  41. package/dist/cjs/registry.js +18 -0
  42. package/dist/cjs/registry.js.map +1 -0
  43. package/dist/cjs/risk.d.ts +82 -0
  44. package/dist/cjs/risk.js +165 -0
  45. package/dist/cjs/risk.js.map +1 -0
  46. package/dist/cjs/simulate.d.ts +35 -0
  47. package/dist/cjs/simulate.js +143 -0
  48. package/dist/cjs/simulate.js.map +1 -0
  49. package/dist/cjs/skewt.d.ts +45 -0
  50. package/dist/cjs/skewt.js +107 -0
  51. package/dist/cjs/skewt.js.map +1 -0
  52. package/dist/cjs/special.d.ts +13 -0
  53. package/dist/cjs/special.js +130 -0
  54. package/dist/cjs/special.js.map +1 -0
  55. package/dist/cjs/validation.d.ts +112 -0
  56. package/dist/cjs/validation.js +163 -0
  57. package/dist/cjs/validation.js.map +1 -0
  58. package/dist/cjs/web-worker-file.d.ts +17 -0
  59. package/dist/cjs/web-worker-file.js +14 -0
  60. package/dist/cjs/web-worker-file.js.map +1 -0
  61. package/dist/cjs/web.d.ts +30 -0
  62. package/dist/cjs/web.js +56 -0
  63. package/dist/cjs/web.js.map +1 -0
  64. package/dist/cjs/weighted.d.ts +81 -0
  65. package/dist/cjs/weighted.js +139 -0
  66. package/dist/cjs/weighted.js.map +1 -0
  67. package/dist/cjs/worker-core.d.ts +13 -0
  68. package/dist/cjs/worker-core.js +22 -0
  69. package/dist/cjs/worker-core.js.map +1 -0
  70. package/dist/cjs/worker-file.d.ts +1 -0
  71. package/dist/cjs/worker-file.js +12 -0
  72. package/dist/cjs/worker-file.js.map +1 -0
  73. package/dist/cjs/worker-web.d.ts +1 -0
  74. package/dist/cjs/worker-web.js +10 -0
  75. package/dist/cjs/worker-web.js.map +1 -0
  76. package/dist/cjs/worker.d.ts +1 -0
  77. package/dist/cjs/worker.js +10 -0
  78. package/dist/cjs/worker.js.map +1 -0
  79. package/dist/esm/diagnostics.d.ts +29 -0
  80. package/dist/esm/diagnostics.js +148 -0
  81. package/dist/esm/diagnostics.js.map +1 -0
  82. package/dist/esm/egarch.d.ts +46 -0
  83. package/dist/esm/egarch.js +144 -0
  84. package/dist/esm/egarch.js.map +1 -0
  85. package/dist/esm/fit.d.ts +84 -0
  86. package/dist/esm/fit.js +349 -0
  87. package/dist/esm/fit.js.map +1 -0
  88. package/dist/esm/forecast.d.ts +8 -0
  89. package/dist/esm/forecast.js +26 -0
  90. package/dist/esm/forecast.js.map +1 -0
  91. package/dist/esm/index.d.ts +14 -0
  92. package/dist/esm/index.js +14 -0
  93. package/dist/esm/index.js.map +1 -0
  94. package/dist/esm/inference.d.ts +29 -0
  95. package/dist/esm/inference.js +104 -0
  96. package/dist/esm/inference.js.map +1 -0
  97. package/dist/esm/jobs.d.ts +78 -0
  98. package/dist/esm/jobs.js +99 -0
  99. package/dist/esm/jobs.js.map +1 -0
  100. package/dist/esm/model.d.ts +60 -0
  101. package/dist/esm/model.js +183 -0
  102. package/dist/esm/model.js.map +1 -0
  103. package/dist/esm/node.d.ts +29 -0
  104. package/dist/esm/node.js +62 -0
  105. package/dist/esm/node.js.map +1 -0
  106. package/dist/esm/optimize.d.ts +11 -0
  107. package/dist/esm/optimize.js +58 -0
  108. package/dist/esm/optimize.js.map +1 -0
  109. package/dist/esm/package.json +1 -0
  110. package/dist/esm/pool.d.ts +84 -0
  111. package/dist/esm/pool.js +246 -0
  112. package/dist/esm/pool.js.map +1 -0
  113. package/dist/esm/quality.d.ts +24 -0
  114. package/dist/esm/quality.js +43 -0
  115. package/dist/esm/quality.js.map +1 -0
  116. package/dist/esm/registry.d.ts +17 -0
  117. package/dist/esm/registry.js +15 -0
  118. package/dist/esm/registry.js.map +1 -0
  119. package/dist/esm/risk.d.ts +82 -0
  120. package/dist/esm/risk.js +155 -0
  121. package/dist/esm/risk.js.map +1 -0
  122. package/dist/esm/simulate.d.ts +35 -0
  123. package/dist/esm/simulate.js +140 -0
  124. package/dist/esm/simulate.js.map +1 -0
  125. package/dist/esm/skewt.d.ts +45 -0
  126. package/dist/esm/skewt.js +99 -0
  127. package/dist/esm/skewt.js.map +1 -0
  128. package/dist/esm/special.d.ts +13 -0
  129. package/dist/esm/special.js +122 -0
  130. package/dist/esm/special.js.map +1 -0
  131. package/dist/esm/validation.d.ts +112 -0
  132. package/dist/esm/validation.js +150 -0
  133. package/dist/esm/validation.js.map +1 -0
  134. package/dist/esm/web-worker-file.d.ts +17 -0
  135. package/dist/esm/web-worker-file.js +4 -0
  136. package/dist/esm/web.d.ts +30 -0
  137. package/dist/esm/web.js +43 -0
  138. package/dist/esm/web.js.map +1 -0
  139. package/dist/esm/weighted.d.ts +81 -0
  140. package/dist/esm/weighted.js +132 -0
  141. package/dist/esm/weighted.js.map +1 -0
  142. package/dist/esm/worker-core.d.ts +13 -0
  143. package/dist/esm/worker-core.js +19 -0
  144. package/dist/esm/worker-core.js.map +1 -0
  145. package/dist/esm/worker-file.d.ts +1 -0
  146. package/dist/esm/worker-file.js +2 -0
  147. package/dist/esm/worker-web.d.ts +1 -0
  148. package/dist/esm/worker-web.js +8 -0
  149. package/dist/esm/worker-web.js.map +1 -0
  150. package/dist/esm/worker.d.ts +1 -0
  151. package/dist/esm/worker.js +8 -0
  152. package/dist/esm/worker.js.map +1 -0
  153. package/package.json +88 -4
  154. package/src/diagnostics.ts +144 -0
  155. package/src/egarch.ts +162 -0
  156. package/src/fit.ts +442 -0
  157. package/src/forecast.ts +26 -0
  158. package/src/index.ts +89 -0
  159. package/src/inference.ts +127 -0
  160. package/src/jobs.ts +174 -0
  161. package/src/model.ts +222 -0
  162. package/src/node.ts +95 -0
  163. package/src/optimize.ts +66 -0
  164. package/src/pool.ts +309 -0
  165. package/src/quality.ts +43 -0
  166. package/src/registry.ts +18 -0
  167. package/src/risk.ts +205 -0
  168. package/src/simulate.ts +165 -0
  169. package/src/skewt.ts +134 -0
  170. package/src/special.ts +112 -0
  171. package/src/validation.ts +242 -0
  172. package/src/web-worker-file.ts +21 -0
  173. package/src/web.ts +86 -0
  174. package/src/weighted.ts +228 -0
  175. package/src/worker-core.ts +22 -0
  176. package/src/worker-file.ts +9 -0
  177. package/src/worker-web.ts +13 -0
  178. 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
- # Temporary Holding Version
1
+ # @krzysztofsztompka/garch
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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":"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"}
@@ -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[];