garch 1.1.0 → 1.2.1

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/README.md CHANGED
@@ -16,9 +16,11 @@ npm install garch
16
16
 
17
17
  ## API
18
18
 
19
- ### `predict(candles, interval, currentPrice?)`
19
+ ### `predict(candles, interval, currentPrice?, confidence?)`
20
20
 
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. Returns a +-1 sigma price corridor.
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
+
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.
22
24
 
23
25
  ```typescript
24
26
  import { predict } from 'garch';
@@ -26,21 +28,50 @@ import type { Candle } from 'garch';
26
28
 
27
29
  const candles: Candle[] = await fetchCandles('BTCUSDT', '4h', 200);
28
30
 
31
+ // Default: ±1σ band (~68% coverage)
29
32
  const result = predict(candles, '4h');
30
33
  // {
31
34
  // currentPrice: 97500,
32
- // sigma: 0.012, // 1.2% expected move
33
- // move: 1170, // +/-$1170 price range
34
- // upperPrice: 98670, // ceiling for next candle
35
- // lowerPrice: 96330, // floor for next candle
35
+ // sigma: 0.012, // 1.2% per-period volatility
36
+ // move: 1177, // upward expected move (upper - current)
37
+ // upperPrice: 98677, // P·exp(+σ) ceiling
38
+ // lowerPrice: 96337, // P·exp(-σ) floor
36
39
  // modelType: 'egarch',
37
40
  // reliable: true
38
41
  // }
39
42
 
40
- // Pass VWAP or any reference price as 3rd argument
43
+ // 95% VaR band (z 1.96)
44
+ const var95 = predict(candles, '4h', undefined, 0.95);
45
+
46
+ // Custom reference price (e.g. VWAP)
41
47
  const result = predict(candles, '4h', vwap);
42
48
  ```
43
49
 
50
+ **Confidence → z mapping:**
51
+
52
+ | `confidence` | z-score | Meaning |
53
+ |-------------|---------|---------|
54
+ | 0.6827 (default) | 1.00 | ±1σ, ~68% of moves captured |
55
+ | 0.90 | 1.645 | Moderate VaR |
56
+ | 0.95 | 1.96 | 95% VaR (standard) |
57
+ | 0.99 | 2.576 | Conservative VaR |
58
+
59
+ Any value in (0, 1) is valid — the table above lists common choices, but `probit` computes z for arbitrary confidence.
60
+
61
+ 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:
62
+
63
+ | `confidence` | z | upperPrice | lowerPrice | Corridor width |
64
+ |-------------|---|-----------|-----------|----------------|
65
+ | 0.6827 | 1.00 | $98,677 | $96,337 | $2,340 |
66
+ | 0.95 | 1.96 | $99,808 | $95,222 | $4,586 |
67
+ | 0.99 | 2.58 | $100,545 | $94,520 | $6,025 |
68
+
69
+ **When to use which:**
70
+
71
+ - **±1σ (default)** — typical expected move for the next candle. Good for scalping SL/TP targets and assessing whether a move is "normal" or significant
72
+ - **95% VaR** — worst reasonable scenario. Good for risk management, position sizing, and stop-losses that shouldn't be triggered by noise
73
+ - **99% VaR** — extreme tail risk. Good for stress testing and margin calculations
74
+
44
75
  **Parameters:**
45
76
 
46
77
  | Parameter | Type | Default | Description |
@@ -48,6 +79,7 @@ const result = predict(candles, '4h', vwap);
48
79
  | `candles` | `Candle[]` | required | OHLCV candle data |
49
80
  | `interval` | `CandleInterval` | required | Candle timeframe |
50
81
  | `currentPrice` | `number` | last close | Reference price to center the corridor |
82
+ | `confidence` | `number` | `0.6827` | Two-sided probability in (0,1). Controls band width via `z = probit(confidence)` |
51
83
 
52
84
  **Returns:** `PredictionResult`
53
85
 
@@ -55,9 +87,9 @@ const result = predict(candles, '4h', vwap);
55
87
  interface PredictionResult {
56
88
  currentPrice: number; // Reference price
57
89
  sigma: number; // One-period volatility (decimal, e.g. 0.012 = 1.2%)
58
- move: number; // +/- price move = currentPrice * sigma
59
- upperPrice: number; // currentPrice + move
60
- lowerPrice: number; // currentPrice - move
90
+ move: number; // Upward price move = upperPrice - currentPrice
91
+ upperPrice: number; // P · exp(+z·σ)
92
+ lowerPrice: number; // P · exp(-z·σ)
61
93
  modelType: 'garch' | 'egarch' | 'gjr-garch' | 'har-rv' | 'novas'; // Auto-selected model
62
94
  reliable: boolean; // Quality flag (convergence + persistence + Ljung-Box)
63
95
  }
@@ -65,9 +97,9 @@ interface PredictionResult {
65
97
 
66
98
  ---
67
99
 
68
- ### `predictRange(candles, interval, steps, currentPrice?)`
100
+ ### `predictRange(candles, interval, steps, currentPrice?, confidence?)`
69
101
 
70
- Forecast cumulative expected price range over multiple candles. Cumulative sigma = sqrt(sigma_1^2 + sigma_2^2 + ... + sigma_n^2). Use for swing trades where you hold across multiple periods.
102
+ Forecast cumulative expected price range over multiple candles. Cumulative sigma = sqrt(sigma_1^2 + sigma_2^2 + ... + sigma_n^2). Uses the same log-normal bands as `predict`. Use for swing trades where you hold across multiple periods.
71
103
 
72
104
  ```typescript
73
105
  import { predictRange } from 'garch';
@@ -76,12 +108,15 @@ const range = predictRange(candles, '4h', 5);
76
108
  // {
77
109
  // currentPrice: 97500,
78
110
  // sigma: 0.027, // cumulative ~2.7% over 5 candles
79
- // move: 2632, // +/-$2632 total range
80
- // upperPrice: 100132,
81
- // lowerPrice: 94868,
111
+ // move: 2669, // upward expected move
112
+ // upperPrice: 100169, // P·exp(+z·σ)
113
+ // lowerPrice: 94901, // P·exp(-z·σ)
82
114
  // modelType: 'egarch',
83
115
  // reliable: true
84
116
  // }
117
+
118
+ // 95% VaR over 5 candles
119
+ const var95 = predictRange(candles, '4h', 5, undefined, 0.95);
85
120
  ```
86
121
 
87
122
  **Parameters:**
@@ -92,20 +127,25 @@ const range = predictRange(candles, '4h', 5);
92
127
  | `interval` | `CandleInterval` | required | Candle timeframe |
93
128
  | `steps` | `number` | required | Number of candles to forecast over |
94
129
  | `currentPrice` | `number` | last close | Reference price |
130
+ | `confidence` | `number` | `0.6827` | Two-sided probability in (0,1) |
95
131
 
96
132
  **Returns:** `PredictionResult` (same structure as `predict`)
97
133
 
98
134
  ---
99
135
 
100
- ### `backtest(candles, interval, requiredPercent?)`
136
+ ### `backtest(candles, interval, confidence?, requiredPercent?)`
101
137
 
102
- Walk-forward validation of `predict`. Uses 75% of candles for fitting, 25% for testing. Checks if the model's +-1 sigma corridor captures actual price moves at the required hit rate.
138
+ 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.
139
+
140
+ `confidence` and `requiredPercent` are independent: `confidence` controls the **band width** (via `probit`), `requiredPercent` controls the **pass/fail threshold**.
103
141
 
104
142
  ```typescript
105
143
  import { backtest } from 'garch';
106
144
 
107
- backtest(candles, '4h'); // true -- hit rate >= 68% (default)
108
- backtest(candles, '4h', 50); // true -- hit rate >= 50% (custom)
145
+ backtest(candles, '4h'); // ±1σ band, hit rate >= 68%
146
+ backtest(candles, '4h', 0.95); // 95% VaR band, hit rate >= 68%
147
+ backtest(candles, '4h', 0.95, 90); // 95% VaR band, hit rate >= 90%
148
+ backtest(candles, '4h', undefined, 50); // ±1σ band, hit rate >= 50%
109
149
  ```
110
150
 
111
151
  **Parameters:**
@@ -114,7 +154,8 @@ backtest(candles, '4h', 50); // true -- hit rate >= 50% (custom)
114
154
  |-----------|------|---------|-------------|
115
155
  | `candles` | `Candle[]` | required | OHLCV candle data |
116
156
  | `interval` | `CandleInterval` | required | Candle timeframe |
117
- | `requiredPercent` | `number` | `68` | Minimum hit rate (+-1 sigma ~ 68% theoretically) |
157
+ | `confidence` | `number` | `0.6827` | Two-sided probability in (0,1) for the prediction band |
158
+ | `requiredPercent` | `number` | `68` | Minimum hit rate (0–100) to pass |
118
159
 
119
160
  **Returns:** `boolean`
120
161
 
@@ -402,6 +443,69 @@ sigma^2_GK = (1/n) * sum[ 0.5 * ln(H/L)^2 - (2*ln2 - 1) * ln(C/O)^2 ]
402
443
 
403
444
  ~5x more efficient than close-to-close variance.
404
445
 
446
+ ### Log-Normal Price Bands
447
+
448
+ GARCH models volatility of **log-returns**, not absolute price changes. The correct mapping from log-return volatility back to price space uses the exponential:
449
+
450
+ ```
451
+ upperPrice = P · exp(+z · sigma)
452
+ lowerPrice = P · exp(-z · sigma)
453
+ ```
454
+
455
+ 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`.
456
+
457
+ The previous linear approximation `P · (1 ± sigma)` is a first-order Taylor expansion of `exp(±sigma)`. The difference grows with sigma:
458
+
459
+ | sigma | Linear `1 + sigma` | Exact `exp(sigma)` | Error |
460
+ |-------|--------------------|--------------------|-------|
461
+ | 0.02 | 1.0200 | 1.0202 | ~0.01% |
462
+ | 0.10 | 1.1000 | 1.1052 | ~0.5% |
463
+ | 0.30 | 1.3000 | 1.3499 | ~3.8% |
464
+
465
+ ### Probit (Inverse Normal CDF)
466
+
467
+ `probit(confidence)` computes the inverse of the standard normal CDF (Phi^{-1}). It converts a two-sided probability to a z-score:
468
+
469
+ ```
470
+ confidence = P(-z < Z < z), Z ~ N(0,1)
471
+ z = probit(confidence)
472
+ ```
473
+
474
+ There is no closed-form solution for Phi^{-1} — it is a transcendental equation. The implementation uses **Acklam's rational approximation** (Peter J. Acklam, 2002) with max relative error < 1.15 x 10^{-9}.
475
+
476
+ **Step 1** — convert two-sided confidence to upper-tail probability:
477
+
478
+ ```
479
+ p = (1 + confidence) / 2
480
+ ```
481
+
482
+ **Step 2** — piecewise rational approximation over three regions:
483
+
484
+ **Central region** (0.02425 <= p <= 0.97575) — covers ~95% of inputs:
485
+
486
+ ```
487
+ q = p - 0.5
488
+ r = q^2
489
+ z = (a1·r^5 + a2·r^4 + a3·r^3 + a4·r^2 + a5·r + a6) · q
490
+ / (b1·r^5 + b2·r^4 + b3·r^3 + b4·r^2 + b5·r + 1)
491
+ ```
492
+
493
+ **Tails** (p < 0.02425 or p > 0.97575):
494
+
495
+ ```
496
+ q = sqrt(-2·ln(p)) // left tail
497
+ z = (c1·q^5 + ... + c6) / (d1·q^4 + ... + 1)
498
+ ```
499
+
500
+ For the right tail: `q = sqrt(-2·ln(1-p))`, result is negated. The 16 coefficients (a1–a6, b1–b5, c1–c6, d1–d4) are minimax-optimal rational approximation constants.
501
+
502
+ | `confidence` | `p` | `z` | Meaning |
503
+ |-------------|-----|-----|---------|
504
+ | 0.6827 | 0.8413 | 1.000 | ±1 sigma (default) |
505
+ | 0.90 | 0.9500 | 1.645 | Moderate VaR |
506
+ | 0.95 | 0.9750 | 1.960 | 95% VaR |
507
+ | 0.99 | 0.9950 | 2.576 | Conservative VaR |
508
+
405
509
  ### Student-t Log-Likelihood
406
510
 
407
511
  All five models use a **Student-t** distribution for log-likelihood computation. Financial return series exhibit fat tails (excess kurtosis), and the Student-t captures this with an additional **degrees of freedom (df)** parameter:
@@ -452,13 +556,13 @@ where RV_t is Parkinson per-candle realized variance and sigma_t^2 is the model'
452
556
 
453
557
  ## Tests
454
558
 
455
- **923 tests** across **22 test files**. All passing.
559
+ **932 tests** across **22 test files**. All passing.
456
560
 
457
561
  | Category | Files | Tests | What's covered |
458
562
  |----------|-------|-------|----------------|
459
563
  | Mathematical formulas | `math.test.ts` | 45 | GARCH/EGARCH variance recursion, log-likelihood, forecast formulas, AIC/BIC, QLIKE, Yang-Zhang, Garman-Klass, Ljung-Box, chi-squared |
460
564
  | Math coverage | `math-coverage.test.ts` | 79 | Parkinson formula verification, rv↔returns alignment, H=L fallback, Parkinson-based forecast, candle validation, reliable flag cascade, backtest validity, numerical precision, cross-model consistency, Realized GARCH/EGARCH/GJR-GARCH Candle[] vs number[], perCandleParkinson shared function |
461
- | Full pipeline coverage | `plan-coverage.test.ts` | 73 | End-to-end: fit, forecast, predict, predictRange, backtest, model selection |
565
+ | Full pipeline coverage | `plan-coverage.test.ts` | 82 | End-to-end: fit, forecast, predict, predictRange, backtest, model selection |
462
566
  | GARCH unit | `garch.test.ts` | 10 | Parameter estimation, variance series, forecast convergence, candle vs price input |
463
567
  | EGARCH unit | `egarch.test.ts` | 11 | Leverage detection, asymmetric volatility, model comparison |
464
568
  | GJR-GARCH unit | `gjr-garch.test.ts` | 86 | Variance recursion (r² and Parkinson), indicator function I(r<0), forecast formula (one-step + multi-step), constraint barriers, computed fields, AIC/BIC numParams=5, estimation properties (perturbation, determinism), numerical stability, degenerate params, Realized path (Candle[] vs number[], flat candles, bad OHLC), options forwarding, immutability, instance isolation, cross-model consistency, scale invariance, property-based fuzz, predict/predictRange/backtest integration |
package/build/index.cjs CHANGED
@@ -454,6 +454,61 @@ function qlike(varianceSeries, rv) {
454
454
  }
455
455
  return count > 0 ? sum / count : Infinity;
456
456
  }
457
+ /**
458
+ * Inverse standard normal CDF (probit function).
459
+ * Converts a two-sided confidence level (e.g. 0.95) to the corresponding
460
+ * z-score (e.g. 1.96).
461
+ *
462
+ * Uses Acklam's rational approximation (max relative error < 1.15e-9).
463
+ */
464
+ function probit(confidence) {
465
+ if (confidence <= 0 || confidence >= 1) {
466
+ throw new Error(`confidence must be in (0, 1), got ${confidence}`);
467
+ }
468
+ // Convert two-sided confidence to upper-tail probability
469
+ const p = (1 + confidence) / 2;
470
+ // Acklam's inverse normal approximation
471
+ const a1 = -39.69683028665376;
472
+ const a2 = 2.209460984245205e+02;
473
+ const a3 = -275.9285104469687;
474
+ const a4 = 1.383577518672690e+02;
475
+ const a5 = -30.66479806614716;
476
+ const a6 = 2.506628277459239e+00;
477
+ const b1 = -54.47609879822406;
478
+ const b2 = 1.615858368580409e+02;
479
+ const b3 = -155.6989798598866;
480
+ const b4 = 6.680131188771972e+01;
481
+ const b5 = -13.28068155288572;
482
+ const c1 = -0.007784894002430293;
483
+ const c2 = -0.3223964580411365;
484
+ const c3 = -2.400758277161838;
485
+ const c4 = -2.549732539343734;
486
+ const c5 = 4.374664141464968e+00;
487
+ const c6 = 2.938163982698783e+00;
488
+ const d1 = 7.784695709041462e-03;
489
+ const d2 = 3.224671290700398e-01;
490
+ const d3 = 2.445134137142996e+00;
491
+ const d4 = 3.754408661907416e+00;
492
+ const pLow = 0.02425;
493
+ const pHigh = 1 - pLow;
494
+ let q, r;
495
+ if (p < pLow) {
496
+ q = Math.sqrt(-2 * Math.log(p));
497
+ return (((((c1 * q + c2) * q + c3) * q + c4) * q + c5) * q + c6) /
498
+ ((((d1 * q + d2) * q + d3) * q + d4) * q + 1);
499
+ }
500
+ else if (p <= pHigh) {
501
+ q = p - 0.5;
502
+ r = q * q;
503
+ return (((((a1 * r + a2) * r + a3) * r + a4) * r + a5) * r + a6) * q /
504
+ (((((b1 * r + b2) * r + b3) * r + b4) * r + b5) * r + 1);
505
+ }
506
+ else {
507
+ q = Math.sqrt(-2 * Math.log(1 - p));
508
+ return -(((((c1 * q + c2) * q + c3) * q + c4) * q + c5) * q + c6) /
509
+ ((((d1 * q + d2) * q + d3) * q + d4) * q + 1);
510
+ }
511
+ }
457
512
 
458
513
  /**
459
514
  * GARCH(1,1) model
@@ -1594,9 +1649,7 @@ function assertMinCandles(candles, interval) {
1594
1649
  }
1595
1650
  }
1596
1651
  const recommended = RECOMMENDED_CANDLES[interval];
1597
- if (candles.length < recommended) {
1598
- console.warn(`[garch] ${interval}: ${candles.length} candles provided, recommend ≥${recommended} for reliable results. Check reliable: true in output.`);
1599
- }
1652
+ if (candles.length < recommended) ;
1600
1653
  }
1601
1654
  function fitGarchFamily(candles, periodsPerYear, steps) {
1602
1655
  // Fit all three GARCH-family models and pick the best by AIC
@@ -1722,21 +1775,25 @@ function checkReliable(fit) {
1722
1775
  /**
1723
1776
  * Forecast expected price range for t+1 (next candle).
1724
1777
  *
1725
- * Auto-selects GARCH or EGARCH based on leverage effect.
1726
- * Returns ±1σ price corridor so you can set SL/TP yourself.
1778
+ * Auto-selects the best volatility model via QLIKE.
1779
+ * Uses log-normal price bands: P·exp(±z·σ), where z = probit(confidence).
1780
+ * @param confidence — two-sided probability in (0,1). Default ≈0.6827 (±1σ).
1781
+ * Common values: 0.90 → z=1.645, 0.95 → z=1.96, 0.99 → z=2.576.
1727
1782
  */
1728
- function predict(candles, interval, currentPrice = candles[candles.length - 1].close) {
1783
+ function predict(candles, interval, currentPrice = candles[candles.length - 1].close, confidence = 0.6827) {
1729
1784
  assertMinCandles(candles, interval);
1785
+ const z = probit(confidence);
1730
1786
  const fit = fitModel(candles, INTERVALS_PER_YEAR[interval], 1);
1731
1787
  const sigma = fit.forecast.volatility[0];
1732
- const move = currentPrice * sigma;
1788
+ const upperPrice = currentPrice * Math.exp(z * sigma);
1789
+ const lowerPrice = currentPrice * Math.exp(-z * sigma);
1733
1790
  return {
1734
1791
  modelType: fit.modelType,
1735
1792
  currentPrice,
1736
1793
  sigma,
1737
- move,
1738
- upperPrice: currentPrice + move,
1739
- lowerPrice: currentPrice - move,
1794
+ move: upperPrice - currentPrice,
1795
+ upperPrice,
1796
+ lowerPrice,
1740
1797
  reliable: checkReliable(fit),
1741
1798
  };
1742
1799
  }
@@ -1744,26 +1801,28 @@ function predict(candles, interval, currentPrice = candles[candles.length - 1].c
1744
1801
  * Forecast expected price range over multiple candles.
1745
1802
  *
1746
1803
  * Cumulative σ = √(σ₁² + σ₂² + ... + σₙ²) — total expected move over N periods.
1747
- * Use for swing trades where you hold across multiple candles.
1804
+ * Uses log-normal price bands: P·exp(±z·σ), where z = probit(confidence).
1805
+ * @param confidence — two-sided probability in (0,1). Default ≈0.6827 (±1σ).
1748
1806
  */
1749
- function predictRange(candles, interval, steps, currentPrice = candles[candles.length - 1].close) {
1807
+ function predictRange(candles, interval, steps, currentPrice = candles[candles.length - 1].close, confidence = 0.6827) {
1750
1808
  assertMinCandles(candles, interval);
1809
+ const z = probit(confidence);
1751
1810
  const fit = fitModel(candles, INTERVALS_PER_YEAR[interval], steps);
1752
1811
  const cumulativeVariance = fit.forecast.variance.reduce((sum, v) => sum + v, 0);
1753
1812
  const sigma = Math.sqrt(cumulativeVariance);
1754
- const move = currentPrice * sigma;
1813
+ const upperPrice = currentPrice * Math.exp(z * sigma);
1814
+ const lowerPrice = currentPrice * Math.exp(-z * sigma);
1755
1815
  return {
1756
1816
  modelType: fit.modelType,
1757
1817
  currentPrice,
1758
1818
  sigma,
1759
- move,
1760
- upperPrice: currentPrice + move,
1761
- lowerPrice: currentPrice - move,
1819
+ move: upperPrice - currentPrice,
1820
+ upperPrice,
1821
+ lowerPrice,
1762
1822
  reliable: checkReliable(fit),
1763
1823
  };
1764
1824
  }
1765
1825
  // ── Backtest ──────────────────────────────────────────────────
1766
- const BACKTEST_REQUIRED_PERCENT = 68;
1767
1826
  const BACKTEST_WINDOW_RATIO = 0.75;
1768
1827
  /**
1769
1828
  * Walk-forward backtest of predict.
@@ -1771,16 +1830,22 @@ const BACKTEST_WINDOW_RATIO = 0.75;
1771
1830
  * Window is computed automatically: 75% of candles for fitting, 25% for testing.
1772
1831
  * Throws if not enough candles for the given interval.
1773
1832
  * Returns true if the model's hit rate meets the required threshold.
1774
- * Default threshold is 68% (±1σ should contain ~68% of moves).
1833
+ * @param confidence two-sided probability in (0,1) for the prediction band.
1834
+ * Default ≈0.6827 (±1σ).
1835
+ * @param requiredPercent — minimum hit rate (0–100) to pass. Default 68.
1775
1836
  */
1776
- function backtest(candles, interval, requiredPercent = BACKTEST_REQUIRED_PERCENT) {
1837
+ function backtest(candles, interval, confidence = 0.6827, requiredPercent = 68) {
1777
1838
  assertMinCandles(candles, interval);
1839
+ if (requiredPercent <= 0)
1840
+ return true;
1841
+ if (requiredPercent >= 100)
1842
+ return false;
1778
1843
  const window = Math.max(MIN_CANDLES[interval], Math.floor(candles.length * BACKTEST_WINDOW_RATIO));
1779
1844
  let hits = 0;
1780
1845
  let total = 0;
1781
1846
  for (let i = window; i < candles.length - 1; i++) {
1782
1847
  const slice = candles.slice(i - window, i + 1);
1783
- const predicted = predict(slice, interval);
1848
+ const predicted = predict(slice, interval, slice[slice.length - 1].close, confidence);
1784
1849
  const actual = candles[i + 1].close;
1785
1850
  if (actual >= predicted.lowerPrice && actual <= predicted.upperPrice) {
1786
1851
  hits++;
@@ -1814,6 +1879,7 @@ exports.nelderMeadMultiStart = nelderMeadMultiStart;
1814
1879
  exports.perCandleParkinson = perCandleParkinson;
1815
1880
  exports.predict = predict;
1816
1881
  exports.predictRange = predictRange;
1882
+ exports.probit = probit;
1817
1883
  exports.profileStudentTDf = profileStudentTDf;
1818
1884
  exports.qlike = qlike;
1819
1885
  exports.sampleVariance = sampleVariance;
package/build/index.mjs CHANGED
@@ -452,6 +452,61 @@ function qlike(varianceSeries, rv) {
452
452
  }
453
453
  return count > 0 ? sum / count : Infinity;
454
454
  }
455
+ /**
456
+ * Inverse standard normal CDF (probit function).
457
+ * Converts a two-sided confidence level (e.g. 0.95) to the corresponding
458
+ * z-score (e.g. 1.96).
459
+ *
460
+ * Uses Acklam's rational approximation (max relative error < 1.15e-9).
461
+ */
462
+ function probit(confidence) {
463
+ if (confidence <= 0 || confidence >= 1) {
464
+ throw new Error(`confidence must be in (0, 1), got ${confidence}`);
465
+ }
466
+ // Convert two-sided confidence to upper-tail probability
467
+ const p = (1 + confidence) / 2;
468
+ // Acklam's inverse normal approximation
469
+ const a1 = -39.69683028665376;
470
+ const a2 = 2.209460984245205e+02;
471
+ const a3 = -275.9285104469687;
472
+ const a4 = 1.383577518672690e+02;
473
+ const a5 = -30.66479806614716;
474
+ const a6 = 2.506628277459239e+00;
475
+ const b1 = -54.47609879822406;
476
+ const b2 = 1.615858368580409e+02;
477
+ const b3 = -155.6989798598866;
478
+ const b4 = 6.680131188771972e+01;
479
+ const b5 = -13.28068155288572;
480
+ const c1 = -0.007784894002430293;
481
+ const c2 = -0.3223964580411365;
482
+ const c3 = -2.400758277161838;
483
+ const c4 = -2.549732539343734;
484
+ const c5 = 4.374664141464968e+00;
485
+ const c6 = 2.938163982698783e+00;
486
+ const d1 = 7.784695709041462e-03;
487
+ const d2 = 3.224671290700398e-01;
488
+ const d3 = 2.445134137142996e+00;
489
+ const d4 = 3.754408661907416e+00;
490
+ const pLow = 0.02425;
491
+ const pHigh = 1 - pLow;
492
+ let q, r;
493
+ if (p < pLow) {
494
+ q = Math.sqrt(-2 * Math.log(p));
495
+ return (((((c1 * q + c2) * q + c3) * q + c4) * q + c5) * q + c6) /
496
+ ((((d1 * q + d2) * q + d3) * q + d4) * q + 1);
497
+ }
498
+ else if (p <= pHigh) {
499
+ q = p - 0.5;
500
+ r = q * q;
501
+ return (((((a1 * r + a2) * r + a3) * r + a4) * r + a5) * r + a6) * q /
502
+ (((((b1 * r + b2) * r + b3) * r + b4) * r + b5) * r + 1);
503
+ }
504
+ else {
505
+ q = Math.sqrt(-2 * Math.log(1 - p));
506
+ return -(((((c1 * q + c2) * q + c3) * q + c4) * q + c5) * q + c6) /
507
+ ((((d1 * q + d2) * q + d3) * q + d4) * q + 1);
508
+ }
509
+ }
455
510
 
456
511
  /**
457
512
  * GARCH(1,1) model
@@ -1592,9 +1647,7 @@ function assertMinCandles(candles, interval) {
1592
1647
  }
1593
1648
  }
1594
1649
  const recommended = RECOMMENDED_CANDLES[interval];
1595
- if (candles.length < recommended) {
1596
- console.warn(`[garch] ${interval}: ${candles.length} candles provided, recommend ≥${recommended} for reliable results. Check reliable: true in output.`);
1597
- }
1650
+ if (candles.length < recommended) ;
1598
1651
  }
1599
1652
  function fitGarchFamily(candles, periodsPerYear, steps) {
1600
1653
  // Fit all three GARCH-family models and pick the best by AIC
@@ -1720,21 +1773,25 @@ function checkReliable(fit) {
1720
1773
  /**
1721
1774
  * Forecast expected price range for t+1 (next candle).
1722
1775
  *
1723
- * Auto-selects GARCH or EGARCH based on leverage effect.
1724
- * Returns ±1σ price corridor so you can set SL/TP yourself.
1776
+ * Auto-selects the best volatility model via QLIKE.
1777
+ * Uses log-normal price bands: P·exp(±z·σ), where z = probit(confidence).
1778
+ * @param confidence — two-sided probability in (0,1). Default ≈0.6827 (±1σ).
1779
+ * Common values: 0.90 → z=1.645, 0.95 → z=1.96, 0.99 → z=2.576.
1725
1780
  */
1726
- function predict(candles, interval, currentPrice = candles[candles.length - 1].close) {
1781
+ function predict(candles, interval, currentPrice = candles[candles.length - 1].close, confidence = 0.6827) {
1727
1782
  assertMinCandles(candles, interval);
1783
+ const z = probit(confidence);
1728
1784
  const fit = fitModel(candles, INTERVALS_PER_YEAR[interval], 1);
1729
1785
  const sigma = fit.forecast.volatility[0];
1730
- const move = currentPrice * sigma;
1786
+ const upperPrice = currentPrice * Math.exp(z * sigma);
1787
+ const lowerPrice = currentPrice * Math.exp(-z * sigma);
1731
1788
  return {
1732
1789
  modelType: fit.modelType,
1733
1790
  currentPrice,
1734
1791
  sigma,
1735
- move,
1736
- upperPrice: currentPrice + move,
1737
- lowerPrice: currentPrice - move,
1792
+ move: upperPrice - currentPrice,
1793
+ upperPrice,
1794
+ lowerPrice,
1738
1795
  reliable: checkReliable(fit),
1739
1796
  };
1740
1797
  }
@@ -1742,26 +1799,28 @@ function predict(candles, interval, currentPrice = candles[candles.length - 1].c
1742
1799
  * Forecast expected price range over multiple candles.
1743
1800
  *
1744
1801
  * Cumulative σ = √(σ₁² + σ₂² + ... + σₙ²) — total expected move over N periods.
1745
- * Use for swing trades where you hold across multiple candles.
1802
+ * Uses log-normal price bands: P·exp(±z·σ), where z = probit(confidence).
1803
+ * @param confidence — two-sided probability in (0,1). Default ≈0.6827 (±1σ).
1746
1804
  */
1747
- function predictRange(candles, interval, steps, currentPrice = candles[candles.length - 1].close) {
1805
+ function predictRange(candles, interval, steps, currentPrice = candles[candles.length - 1].close, confidence = 0.6827) {
1748
1806
  assertMinCandles(candles, interval);
1807
+ const z = probit(confidence);
1749
1808
  const fit = fitModel(candles, INTERVALS_PER_YEAR[interval], steps);
1750
1809
  const cumulativeVariance = fit.forecast.variance.reduce((sum, v) => sum + v, 0);
1751
1810
  const sigma = Math.sqrt(cumulativeVariance);
1752
- const move = currentPrice * sigma;
1811
+ const upperPrice = currentPrice * Math.exp(z * sigma);
1812
+ const lowerPrice = currentPrice * Math.exp(-z * sigma);
1753
1813
  return {
1754
1814
  modelType: fit.modelType,
1755
1815
  currentPrice,
1756
1816
  sigma,
1757
- move,
1758
- upperPrice: currentPrice + move,
1759
- lowerPrice: currentPrice - move,
1817
+ move: upperPrice - currentPrice,
1818
+ upperPrice,
1819
+ lowerPrice,
1760
1820
  reliable: checkReliable(fit),
1761
1821
  };
1762
1822
  }
1763
1823
  // ── Backtest ──────────────────────────────────────────────────
1764
- const BACKTEST_REQUIRED_PERCENT = 68;
1765
1824
  const BACKTEST_WINDOW_RATIO = 0.75;
1766
1825
  /**
1767
1826
  * Walk-forward backtest of predict.
@@ -1769,16 +1828,22 @@ const BACKTEST_WINDOW_RATIO = 0.75;
1769
1828
  * Window is computed automatically: 75% of candles for fitting, 25% for testing.
1770
1829
  * Throws if not enough candles for the given interval.
1771
1830
  * Returns true if the model's hit rate meets the required threshold.
1772
- * Default threshold is 68% (±1σ should contain ~68% of moves).
1831
+ * @param confidence two-sided probability in (0,1) for the prediction band.
1832
+ * Default ≈0.6827 (±1σ).
1833
+ * @param requiredPercent — minimum hit rate (0–100) to pass. Default 68.
1773
1834
  */
1774
- function backtest(candles, interval, requiredPercent = BACKTEST_REQUIRED_PERCENT) {
1835
+ function backtest(candles, interval, confidence = 0.6827, requiredPercent = 68) {
1775
1836
  assertMinCandles(candles, interval);
1837
+ if (requiredPercent <= 0)
1838
+ return true;
1839
+ if (requiredPercent >= 100)
1840
+ return false;
1776
1841
  const window = Math.max(MIN_CANDLES[interval], Math.floor(candles.length * BACKTEST_WINDOW_RATIO));
1777
1842
  let hits = 0;
1778
1843
  let total = 0;
1779
1844
  for (let i = window; i < candles.length - 1; i++) {
1780
1845
  const slice = candles.slice(i - window, i + 1);
1781
- const predicted = predict(slice, interval);
1846
+ const predicted = predict(slice, interval, slice[slice.length - 1].close, confidence);
1782
1847
  const actual = candles[i + 1].close;
1783
1848
  if (actual >= predicted.lowerPrice && actual <= predicted.upperPrice) {
1784
1849
  hits++;
@@ -1788,4 +1853,4 @@ function backtest(candles, interval, requiredPercent = BACKTEST_REQUIRED_PERCENT
1788
1853
  return (hits / total) * 100 >= requiredPercent;
1789
1854
  }
1790
1855
 
1791
- export { EXPECTED_ABS_NORMAL, Egarch, Garch, GjrGarch, HarRv, NoVaS, backtest, calculateReturns, calculateReturnsFromPrices, calibrateEgarch, calibrateGarch, calibrateGjrGarch, calibrateHarRv, calibrateNoVaS, checkLeverageEffect, expectedAbsStudentT, garmanKlassVariance, ljungBox, logGamma, nelderMead, nelderMeadMultiStart, perCandleParkinson, predict, predictRange, profileStudentTDf, qlike, sampleVariance, sampleVarianceWithMean, studentTNegLL, yangZhangVariance };
1856
+ export { EXPECTED_ABS_NORMAL, Egarch, Garch, GjrGarch, HarRv, NoVaS, backtest, calculateReturns, calculateReturnsFromPrices, calibrateEgarch, calibrateGarch, calibrateGjrGarch, calibrateHarRv, calibrateNoVaS, checkLeverageEffect, expectedAbsStudentT, garmanKlassVariance, ljungBox, logGamma, nelderMead, nelderMeadMultiStart, perCandleParkinson, predict, predictRange, probit, profileStudentTDf, qlike, sampleVariance, sampleVarianceWithMean, studentTNegLL, yangZhangVariance };
package/package.json CHANGED
@@ -1,48 +1,48 @@
1
- {
2
- "name": "garch",
3
- "version": "1.1.0",
4
- "description": "GARCH and EGARCH volatility models for TypeScript",
5
- "type": "module",
6
- "main": "./build/index.cjs",
7
- "module": "./build/index.mjs",
8
- "types": "./types.d.ts",
9
- "exports": {
10
- ".": {
11
- "types": "./types.d.ts",
12
- "import": "./build/index.mjs",
13
- "require": "./build/index.cjs"
14
- }
15
- },
16
- "files": [
17
- "build",
18
- "types.d.ts",
19
- "README.md"
20
- ],
21
- "scripts": {
22
- "build": "rollup -c",
23
- "test": "vitest run",
24
- "test:watch": "vitest",
25
- "prepublishOnly": "npm run build"
26
- },
27
- "keywords": [
28
- "garch",
29
- "egarch",
30
- "volatility",
31
- "finance",
32
- "econometrics",
33
- "time-series"
34
- ],
35
- "author": "",
36
- "license": "MIT",
37
- "devDependencies": {
38
- "@rollup/plugin-typescript": "^12.3.0",
39
- "@types/node": "^20.10.0",
40
- "@vitest/coverage-v8": "^1.6.1",
41
- "rollup": "^4.57.1",
42
- "rollup-plugin-dts": "^6.3.0",
43
- "rollup-plugin-peer-deps-external": "^2.2.4",
44
- "tslib": "^2.8.1",
45
- "typescript": "^5.3.0",
46
- "vitest": "^1.0.0"
47
- }
48
- }
1
+ {
2
+ "name": "garch",
3
+ "version": "1.2.1",
4
+ "description": "GARCH and EGARCH volatility models for TypeScript",
5
+ "type": "module",
6
+ "main": "./build/index.cjs",
7
+ "module": "./build/index.mjs",
8
+ "types": "./types.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./types.d.ts",
12
+ "import": "./build/index.mjs",
13
+ "require": "./build/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "build",
18
+ "types.d.ts",
19
+ "README.md"
20
+ ],
21
+ "scripts": {
22
+ "build": "rollup -c",
23
+ "test": "vitest run",
24
+ "test:watch": "vitest",
25
+ "prepublishOnly": "npm run build"
26
+ },
27
+ "keywords": [
28
+ "garch",
29
+ "egarch",
30
+ "volatility",
31
+ "finance",
32
+ "econometrics",
33
+ "time-series"
34
+ ],
35
+ "author": "",
36
+ "license": "MIT",
37
+ "devDependencies": {
38
+ "@rollup/plugin-typescript": "^12.3.0",
39
+ "@types/node": "^20.10.0",
40
+ "@vitest/coverage-v8": "^1.6.1",
41
+ "rollup": "^4.57.1",
42
+ "rollup-plugin-dts": "^6.3.0",
43
+ "rollup-plugin-peer-deps-external": "^2.2.4",
44
+ "tslib": "^2.8.1",
45
+ "typescript": "^5.3.0",
46
+ "vitest": "^1.0.0"
47
+ }
48
+ }
package/types.d.ts CHANGED
@@ -487,6 +487,14 @@ declare function profileStudentTDf(returns: number[], varianceSeries: number[]):
487
487
  * of how the model was calibrated (MLE, OLS, D², etc.).
488
488
  */
489
489
  declare function qlike(varianceSeries: number[], rv: number[]): number;
490
+ /**
491
+ * Inverse standard normal CDF (probit function).
492
+ * Converts a two-sided confidence level (e.g. 0.95) to the corresponding
493
+ * z-score (e.g. 1.96).
494
+ *
495
+ * Uses Acklam's rational approximation (max relative error < 1.15e-9).
496
+ */
497
+ declare function probit(confidence: number): number;
490
498
 
491
499
  type CandleInterval = '1m' | '3m' | '5m' | '15m' | '30m' | '1h' | '2h' | '4h' | '6h' | '8h';
492
500
  interface PredictionResult {
@@ -501,26 +509,31 @@ interface PredictionResult {
501
509
  /**
502
510
  * Forecast expected price range for t+1 (next candle).
503
511
  *
504
- * Auto-selects GARCH or EGARCH based on leverage effect.
505
- * Returns ±1σ price corridor so you can set SL/TP yourself.
512
+ * Auto-selects the best volatility model via QLIKE.
513
+ * Uses log-normal price bands: P·exp(±z·σ), where z = probit(confidence).
514
+ * @param confidence — two-sided probability in (0,1). Default ≈0.6827 (±1σ).
515
+ * Common values: 0.90 → z=1.645, 0.95 → z=1.96, 0.99 → z=2.576.
506
516
  */
507
- declare function predict(candles: Candle[], interval: CandleInterval, currentPrice?: number): PredictionResult;
517
+ declare function predict(candles: Candle[], interval: CandleInterval, currentPrice?: number, confidence?: number): PredictionResult;
508
518
  /**
509
519
  * Forecast expected price range over multiple candles.
510
520
  *
511
521
  * Cumulative σ = √(σ₁² + σ₂² + ... + σₙ²) — total expected move over N periods.
512
- * Use for swing trades where you hold across multiple candles.
522
+ * Uses log-normal price bands: P·exp(±z·σ), where z = probit(confidence).
523
+ * @param confidence — two-sided probability in (0,1). Default ≈0.6827 (±1σ).
513
524
  */
514
- declare function predictRange(candles: Candle[], interval: CandleInterval, steps: number, currentPrice?: number): PredictionResult;
525
+ declare function predictRange(candles: Candle[], interval: CandleInterval, steps: number, currentPrice?: number, confidence?: number): PredictionResult;
515
526
  /**
516
527
  * Walk-forward backtest of predict.
517
528
  *
518
529
  * Window is computed automatically: 75% of candles for fitting, 25% for testing.
519
530
  * Throws if not enough candles for the given interval.
520
531
  * Returns true if the model's hit rate meets the required threshold.
521
- * Default threshold is 68% (±1σ should contain ~68% of moves).
532
+ * @param confidence two-sided probability in (0,1) for the prediction band.
533
+ * Default ≈0.6827 (±1σ).
534
+ * @param requiredPercent — minimum hit rate (0–100) to pass. Default 68.
522
535
  */
523
- declare function backtest(candles: Candle[], interval: CandleInterval, requiredPercent?: number): boolean;
536
+ declare function backtest(candles: Candle[], interval: CandleInterval, confidence?: number, requiredPercent?: number): boolean;
524
537
 
525
538
  declare function nelderMead(fn: (x: number[]) => number, x0: number[], options?: {
526
539
  maxIter?: number;
@@ -536,5 +549,5 @@ declare function nelderMeadMultiStart(fn: (x: number[]) => number, x0: number[],
536
549
  restarts?: number;
537
550
  }): OptimizerResult;
538
551
 
539
- export { EXPECTED_ABS_NORMAL, Egarch, Garch, GjrGarch, HarRv, NoVaS, backtest, calculateReturns, calculateReturnsFromPrices, calibrateEgarch, calibrateGarch, calibrateGjrGarch, calibrateHarRv, calibrateNoVaS, checkLeverageEffect, expectedAbsStudentT, garmanKlassVariance, ljungBox, logGamma, nelderMead, nelderMeadMultiStart, perCandleParkinson, predict, predictRange, profileStudentTDf, qlike, sampleVariance, sampleVarianceWithMean, studentTNegLL, yangZhangVariance };
552
+ export { EXPECTED_ABS_NORMAL, Egarch, Garch, GjrGarch, HarRv, NoVaS, backtest, calculateReturns, calculateReturnsFromPrices, calibrateEgarch, calibrateGarch, calibrateGjrGarch, calibrateHarRv, calibrateNoVaS, checkLeverageEffect, expectedAbsStudentT, garmanKlassVariance, ljungBox, logGamma, nelderMead, nelderMeadMultiStart, perCandleParkinson, predict, predictRange, probit, profileStudentTDf, qlike, sampleVariance, sampleVarianceWithMean, studentTNegLL, yangZhangVariance };
540
553
  export type { CalibrationResult, Candle, CandleInterval, EgarchOptions, EgarchParams, GarchOptions, GarchParams, GjrGarchOptions, GjrGarchParams, HarRvOptions, HarRvParams, LeverageStats, NoVaSOptions, NoVaSParams, OptimizerResult, PredictionResult, VolatilityForecast };