garch 1.2.4 → 2.0.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 (6) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +191 -26
  3. package/build/index.cjs +2039 -275
  4. package/build/index.mjs +2023 -276
  5. package/package.json +53 -53
  6. package/types.d.ts +341 -14
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Petr Tripolsky
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.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Petr Tripolsky
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
@@ -20,7 +20,9 @@ npm install garch
20
20
 
21
21
  Forecast expected price range for the next candle (t+1). Auto-selects the best model (GARCH, EGARCH, GJR-GARCH, HAR-RV or NoVaS) by QLIKE forecast-error comparison.
22
22
 
23
- Uses **log-normal price bands**: `P·exp(±z·σ)`, where `z = probit(confidence)`. This correctly maps log-return volatility back to price space — the corridor is asymmetric (upside > downside in absolute terms) and `lowerPrice` can never go negative.
23
+ Uses **log-normal price bands**: `P·exp(±z·σ)`. This correctly maps log-return volatility back to price space — the corridor is asymmetric (upside > downside in absolute terms) and `lowerPrice` can never go negative.
24
+
25
+ The multiplier `z` is **calibrated on the data itself**, not taken from a Gaussian table: the model's variance series is rescaled so standardized residuals have unit variance, then `z` blends the empirical |z| quantile of those residuals with the fitted Student-t(df) quantile (the empirical part dominates where the sample supports the tail, the t part takes over in the far tail). The actual multiplier used is returned as `zScore`.
24
26
 
25
27
  ```typescript
26
28
  import { predict } from 'garch';
@@ -35,37 +37,33 @@ const result = predict(candles, '4h');
35
37
  // sigma: 0.012, // 1.2% per-period volatility
36
38
  // move: 1177, // upward expected move (upper - current), in dollars
37
39
  // movePercent: 1.21, // upward expected move, in percent (0–100)
38
- // upperPrice: 98677, // P·exp() — ceiling
39
- // lowerPrice: 96337, // P·exp() — floor
40
+ // upperPrice: 98677, // P·exp(+z·σ) — ceiling
41
+ // lowerPrice: 96337, // P·exp(-z·σ) — floor
40
42
  // modelType: 'egarch',
43
+ // df: 6.3, // fitted Student-t tail thickness
44
+ // zScore: 0.97, // corridor multiplier actually used
41
45
  // reliable: true
42
46
  // }
43
47
 
44
- // 95% VaR band (z ≈ 1.96)
48
+ // 95% VaR band
45
49
  const var95 = predict(candles, '4h', undefined, 0.95);
46
50
 
47
51
  // Custom reference price (e.g. VWAP)
48
52
  const result = predict(candles, '4h', vwap);
49
53
  ```
50
54
 
51
- **Confidence → z mapping:**
52
-
53
- | `confidence` | z-score | Meaning |
54
- |-------------|---------|---------|
55
- | 0.6827 (default) | 1.00 | ±1σ, ~68% of moves captured |
56
- | 0.90 | 1.645 | Moderate VaR |
57
- | 0.95 | 1.96 | 95% VaR (standard) |
58
- | 0.99 | 2.576 | Conservative VaR |
55
+ **Confidence → z (Gaussian reference only — actual z is data-driven):**
59
56
 
60
- Any value in (0, 1) is valid the table above lists common choices, but `probit` computes z for arbitrary confidence.
57
+ | `confidence` | Gaussian z | Typical fat-tail z (df≈5) | Meaning |
58
+ |-------------|-----------|---------------------------|---------|
59
+ | 0.6827 (default) | 1.00 | ≈0.86 | ±1σ, ~68% of moves captured |
60
+ | 0.90 | 1.645 | ≈1.56 | Moderate VaR |
61
+ | 0.95 | 1.96 | ≈1.99 | 95% VaR (standard) |
62
+ | 0.99 | 2.576 | ≈3.12 | Conservative VaR |
61
63
 
62
- Higher confidence = wider corridor. `sigma` stays the same (it's the model's volatility estimate), only the z-multiplier changes. Example with sigma=1.2% and P=$97,500:
64
+ Any value in (0, 1) is valid. With fat-tailed market data the Gaussian table is systematically wrong — too wide in the center, dangerously narrow in the tails — which is why the corridor uses the data-calibrated multiplier instead. Check `result.zScore` for the value actually applied.
63
65
 
64
- | `confidence` | z | upperPrice | lowerPrice | Corridor width |
65
- |-------------|---|-----------|-----------|----------------|
66
- | 0.6827 | 1.00 | $98,677 | $96,337 | $2,340 |
67
- | 0.95 | 1.96 | $99,808 | $95,222 | $4,586 |
68
- | 0.99 | 2.58 | $100,545 | $94,520 | $6,025 |
66
+ Higher confidence = wider corridor. `sigma` stays the same (it's the model's volatility estimate), only the z-multiplier changes.
69
67
 
70
68
  **When to use which:**
71
69
 
@@ -80,7 +78,7 @@ Higher confidence = wider corridor. `sigma` stays the same (it's the model's vol
80
78
  | `candles` | `Candle[]` | required | OHLCV candle data |
81
79
  | `interval` | `CandleInterval` | required | Candle timeframe |
82
80
  | `currentPrice` | `number` | last close | Reference price to center the corridor |
83
- | `confidence` | `number` | `0.6827` | Two-sided probability in (0,1). Controls band width via `z = probit(confidence)` |
81
+ | `confidence` | `number` | `0.6827` | Two-sided probability in (0,1). Controls band width via the data-calibrated `zScore` |
84
82
 
85
83
  **Returns:** `PredictionResult`
86
84
 
@@ -93,6 +91,8 @@ interface PredictionResult {
93
91
  upperPrice: number; // P · exp(+z·σ)
94
92
  lowerPrice: number; // P · exp(-z·σ)
95
93
  modelType: 'garch' | 'egarch' | 'gjr-garch' | 'har-rv' | 'novas'; // Auto-selected model
94
+ df: number; // Fitted Student-t degrees of freedom (tail thickness)
95
+ zScore: number; // Corridor multiplier actually used for the bands
96
96
  reliable: boolean; // Quality flag (convergence + persistence + Ljung-Box)
97
97
  }
98
98
  ```
@@ -140,7 +140,7 @@ const var95 = predictRange(candles, '4h', 5, undefined, 0.95);
140
140
 
141
141
  Walk-forward validation of `predict`. Uses 75% of candles for fitting, 25% for testing. Checks if the model's price corridor captures actual price moves at the required hit rate.
142
142
 
143
- `confidence` and `requiredPercent` are independent: `confidence` controls the **band width** (via `probit`), `requiredPercent` controls the **pass/fail threshold**.
143
+ `confidence` and `requiredPercent` are independent: `confidence` controls the **band width** (via the data-calibrated `zScore`), `requiredPercent` controls the **pass/fail threshold**.
144
144
 
145
145
  ```typescript
146
146
  import { backtest } from 'garch';
@@ -164,6 +164,169 @@ backtest(candles, '4h', undefined, 50); // ±1σ band, hit rate >= 50%
164
164
 
165
165
  ---
166
166
 
167
+ ### `backtestStats(candles, interval, confidence?)`
168
+
169
+ Same walk-forward loop as `backtest`, but returns the raw calibration numbers instead of a pass/fail flag. A well-calibrated setup has `hitRate ≈ confidence · 100` — run this on your own market data before trusting the corridor.
170
+
171
+ ```typescript
172
+ import { backtestStats } from 'garch';
173
+
174
+ const stats = backtestStats(candles, '4h', 0.95);
175
+ // {
176
+ // hits: 61, total: 66, hitRate: 92.4,
177
+ // verdict: 'well-calibrated', // Kupiec test: is 92.4 vs 95 failure or noise?
178
+ // pValue: 0.38,
179
+ // message: 'Coverage 92.4% vs nominal 95.0% over 66 points is consistent with a calibrated corridor (Kupiec p=0.3800).'
180
+ // }
181
+
182
+ // Every test point refits all 5 models, so by default the test span is
183
+ // subsampled to at most ~100 refits. Control it explicitly:
184
+ backtestStats(candles, '4h', 0.95, { stride: 1 }); // every candle (slow)
185
+ backtestStats(candles, '4h', 0.95, { stride: 10 }); // every 10th candle
186
+ ```
187
+
188
+ **Returns:** `BacktestStats`
189
+
190
+ ```typescript
191
+ interface BacktestStats {
192
+ hits: number; // closes that landed inside the corridor
193
+ total: number; // walk-forward predictions made
194
+ hitRate: number; // hits/total · 100 — compare with confidence · 100
195
+ verdict: 'well-calibrated' | 'too-narrow' | 'too-wide' | 'inconclusive';
196
+ pValue: number; // Kupiec POF test — probability the gap is noise
197
+ message: string; // plain-language interpretation with the numbers filled in
198
+ }
199
+ ```
200
+
201
+ `verdict` answers the question a raw hit rate cannot: 63% observed against 68%
202
+ nominal is *fine* on 100 points (noise) and *broken* on 2,000 points. `too-narrow`
203
+ means real risk exceeds what the corridor shows; `too-wide` means decisions based
204
+ on it are overly conservative.
205
+
206
+ ---
207
+
208
+ ### `checkData(candles, interval)`
209
+
210
+ Pre-flight check for a new data source — run it once before wiring a feed into
211
+ `predict`. Errors are conditions `predict` would throw on; warnings degrade
212
+ quality but do not block.
213
+
214
+ ```typescript
215
+ import { checkData } from 'garch';
216
+
217
+ const report = checkData(candles, '1h');
218
+ // {
219
+ // ok: false,
220
+ // issues: [
221
+ // { code: 'UNSORTED', severity: 'error',
222
+ // message: 'Candles are not sorted by timestamp (index 812) — sort ascending before calling. Check your data feed.' },
223
+ // { code: 'DATA_GAPS', severity: 'warning',
224
+ // message: '~17 missing bars (2.1%) detected from timestamps — the seasonal profile and lag structure may be distorted. Check your feed for outages.' },
225
+ // ],
226
+ // recommendedCandles: 500,
227
+ // }
228
+ ```
229
+
230
+ Detects: too few candles, broken OHLC (with the failing index), unsorted or
231
+ duplicated timestamps, gaps, candle spacing that contradicts the `interval`
232
+ argument, and feeds where too many candles have `high === low`.
233
+
234
+ ---
235
+
236
+ ### `createPredictor(interval)`
237
+
238
+ Stateful predictor for rolling use (bots, backtest loops). Each call warm-starts
239
+ every optimizer from the previous window's optimum — same math, roughly 3× faster
240
+ than calling `predict` in a loop. State is per-instrument: one predictor per symbol.
241
+
242
+ ```typescript
243
+ import { createPredictor } from 'garch';
244
+
245
+ const predictor = createPredictor('1h');
246
+ setInterval(async () => {
247
+ const candles = await fetchCandles('BTCUSDT', '1h', 500);
248
+ const res = predictor.predict(candles, { confidence: 0.9 });
249
+ // ...
250
+ }, 60 * 60 * 1000);
251
+ ```
252
+
253
+ ---
254
+
255
+ ## Warnings & errors
256
+
257
+ `predict` explains itself instead of failing silently. Every result carries
258
+ `warnings` (plain-language, with a `critical` flag — `reliable` is simply
259
+ "no critical warning fired"), `modelWeights` (which model families are driving
260
+ the forecast) and `seasonalityDetected`:
261
+
262
+ ```typescript
263
+ const res = predict(candles, '1h', { confidence: 0.9 });
264
+ if (!res.reliable) {
265
+ for (const w of res.warnings) console.log(`[${w.code}] ${w.message}`);
266
+ // [RESIDUAL_AUTOCORRELATION] Squared residuals stay autocorrelated (Ljung-Box p=0.012) —
267
+ // the model did not fully capture volatility clustering; the corridor may understate risk.
268
+ }
269
+ console.log(res.modelWeights); // { garch: 0.58, 'har-rv': 0.42 }
270
+ ```
271
+
272
+ Errors are typed — catch by class, not by parsing messages:
273
+
274
+ ```typescript
275
+ import { NotEnoughDataError, BadDataError, InvalidArgumentError } from 'garch';
276
+
277
+ try {
278
+ const res = predict(candles, '1h');
279
+ } catch (e) {
280
+ if (e instanceof NotEnoughDataError) await fetchMoreHistory();
281
+ else if (e instanceof BadDataError) alertDataPipeline(e.message); // broken OHLC, unsorted/duplicate timestamps
282
+ else if (e instanceof InvalidArgumentError) throw e; // bug at the call site
283
+ else throw e;
284
+ }
285
+ ```
286
+
287
+ ## Recipes
288
+
289
+ **Stop-loss / take-profit from the corridor.** `lowerPrice` at confidence 0.90 is
290
+ the level price stays above with ~95% probability (each tail carries 5%):
291
+
292
+ ```typescript
293
+ const res = predictor.predict(candles, { confidence: 0.9 });
294
+ if (res.reliable) {
295
+ placeOrder({ side: 'buy', stopLoss: res.lowerPrice, takeProfit: res.upperPrice });
296
+ }
297
+ ```
298
+
299
+ **Position sizing by volatility.** Risk a fixed dollar amount per corridor width
300
+ instead of a fixed position size:
301
+
302
+ ```typescript
303
+ const riskPerTrade = 100; // dollars you accept losing if price hits the lower band
304
+ const size = riskPerTrade / (res.currentPrice - res.lowerPrice);
305
+ ```
306
+
307
+ **Volatility filter.** Skip entries when the model cannot be trusted or the
308
+ expected move is too small to cover fees:
309
+
310
+ ```typescript
311
+ const feePct = 0.1; // round-trip, percent
312
+ if (!res.reliable || res.movePercent < feePct * 2) return; // stand aside
313
+ ```
314
+
315
+ **Calibration monitoring.** Once a day, verify the corridor still tells the truth
316
+ on your instrument — and read the verdict, not the raw rate:
317
+
318
+ ```typescript
319
+ const stats = backtestStats(candles, '1h', 0.9);
320
+ if (stats.verdict === 'too-narrow') notify(stats.message); // real risk > shown risk
321
+ ```
322
+
323
+ **Multi-candle horizon.** Corridor for "where will price be 12 hours from now"
324
+ on 1h candles:
325
+
326
+ ```typescript
327
+ const res = predictor.predictRange(candles, 12, { confidence: 0.95 });
328
+ ```
329
+
167
330
  ## Supported Intervals
168
331
 
169
332
  | Interval | Min Candles | Recommended | Periods/Year | Coverage |
@@ -179,12 +342,14 @@ backtest(candles, '4h', undefined, 50); // ±1σ band, hit rate >= 50%
179
342
  | `6h` | 150 | 300 | 1,460 | ~37 days |
180
343
  | `8h` | 150 | 300 | 1,095 | ~50 days |
181
344
 
182
- For intervals below 1h, per-candle Parkinson RV is noisier — more data helps OLS and QLIKE model selection. A `console.warn` is emitted when candle count is below the recommended value. Always check `reliable: true` in the output.
345
+ For intervals below 1h, per-candle Parkinson RV is noisier — more data helps OLS and QLIKE model selection. When the candle count is below the recommended value the result carries a non-critical `LOW_SAMPLE` warning. Always check `reliable: true` (and read `warnings`) in the output.
183
346
 
184
347
  ## Timeframes
185
348
 
186
349
  The `periodsPerYear` value controls annualization of volatility. When using `predict`/`predictRange`/`backtest`, this is handled automatically via the `interval` parameter. When using `Garch`/`Egarch` classes directly, pass `periodsPerYear` manually.
187
350
 
351
+ > **Scale caveat for direct class usage:** with `Candle[]` input the model recursions are driven by range-based Parkinson RV, so `params.unconditionalVariance` / `params.annualizedVol` are on the *realized-variance* scale, which can differ from the close-to-close return variance when ranges and returns disagree (gaps, thin wicks). `predict`/`predictRange` correct this automatically (standardized residuals are rescaled to unit variance before the corridor is built); raw `calibrate*` output is not corrected.
352
+
188
353
  | Timeframe | `periodsPerYear` | Notes |
189
354
  |-----------|-----------------|-------|
190
355
  | **1m** | `525,600` | 1440/day x 365 |
@@ -379,7 +544,7 @@ After D^2 optimization, weights are **rescaled via OLS** on the realized varianc
379
544
  forecast_sigma_t^2 = beta_0 + beta_1 * sigma_t^2(D^2)
380
545
  ```
381
546
 
382
- D^2 acts as a data-driven smoother over RV lags — more flexible than HAR-RV's fixed rolling means (1, 5, 22). OLS rescaling adjusts for bias with only 2 parameters, keeping the model robust on small samples with noisy per-candle RV.
547
+ D^2 acts as a data-driven smoother over RV lags — more flexible than HAR-RV's three rolling means. OLS rescaling adjusts for bias with only 2 parameters, keeping the model robust on small samples with noisy per-candle RV. Inside `predict` the NoVaS lag order grows with the sample size (~n^(1/3)) instead of being fixed at 10.
383
548
 
384
549
  Multi-step forecast: replace future innovations with sigma^2 (since E[RV] = E[X^2] = sigma^2), then rescale:
385
550
 
@@ -424,7 +589,7 @@ The library fits all five models on every call and picks the best by QLIKE. Each
424
589
 
425
590
  - **GJR-GARCH** — Same idea as EGARCH (red candles increase vol more than green ones) but the effect is milder and simpler: when the return is negative, a bonus term `gamma * epsilon^2` is added to variance. When positive — nothing extra. A binary switch rather than a continuous asymmetry. Common in altcoins and less panic-prone markets.
426
591
 
427
- - **HAR-RV** — Volatility has memory at multiple horizons. The model takes a single timeframe of candles and internally builds three scales: last candle's RV (short, lag 1), rolling average over 5 candles (medium), and rolling average over 22 candles (long). These three components are combined via OLS regression. Works well when different types of participants (scalpers, swing traders, institutions) all influence the same market at different speeds. If your asset has visible "rhythm" across day/week/month — HAR-RV will likely beat GARCH family (sideways, daily patterns).
592
+ - **HAR-RV** — Volatility has memory at multiple horizons. The model takes a single timeframe of candles and internally builds three scales: last candle's RV (short), a medium rolling average, and a long rolling average, combined via OLS regression. The `HarRv` class defaults to the textbook lags (1, 5, 22 — day/week/month in *trading days*), but `predict` chooses the lag horizons from the candle interval itself (one bar / one day / one week in bars, capped by sample size) and picks the winner by QLIKE the daily-equity convention is not assumed for 24/7 intraday markets. Works well when different types of participants (scalpers, swing traders, institutions) all influence the same market at different speeds.
428
593
 
429
594
  - **NoVaS** — Volatility drifts or cycles without clear shock-and-decay patterns. Slow trend changes, regime shifts, compression/expansion phases, or "breathing" patterns that don't fit any parametric formula. Model-free: finds weights `a_0...a_p` that make the normalized series as close to Gaussian as possible (minimizes D^2 = skewness^2 + (kurtosis - 3)^2). No assumptions about the distribution shape.
430
595
 
@@ -455,7 +620,7 @@ upperPrice = P · exp(+z · sigma)
455
620
  lowerPrice = P · exp(-z · sigma)
456
621
  ```
457
622
 
458
- where `z = probit(confidence)` is the z-score corresponding to the desired two-sided confidence level. This produces **asymmetric** bands (upside > downside in absolute terms) and guarantees `lowerPrice > 0`.
623
+ where `z` is the corridor multiplier for the desired two-sided confidence level (returned as `zScore`). It is calibrated on the data: the empirical quantile of the standardized residuals |r/σ| blended with the fitted Student-t(df) quantile, so it needs neither the Gaussian assumption nor a fixed z table. This produces **asymmetric** bands (upside > downside in absolute terms) and guarantees `lowerPrice > 0`.
459
624
 
460
625
  The previous linear approximation `P · (1 ± sigma)` is a first-order Taylor expansion of `exp(±sigma)`. The difference grows with sigma:
461
626
 
@@ -467,7 +632,7 @@ The previous linear approximation `P · (1 ± sigma)` is a first-order Taylor ex
467
632
 
468
633
  ### Probit (Inverse Normal CDF)
469
634
 
470
- `probit(confidence)` computes the inverse of the standard normal CDF (Phi^{-1}). It converts a two-sided probability to a z-score:
635
+ `probit(confidence)` computes the inverse of the standard normal CDF (Phi^{-1}). It is the Gaussian limit of the corridor multiplier (the fitted Student-t quantile `studentTProbit(confidence, df)` converges to it as df grows). It converts a two-sided probability to a z-score:
471
636
 
472
637
  ```
473
638
  confidence = P(-z < Z < z), Z ~ N(0,1)