garch 1.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.
@@ -0,0 +1,516 @@
1
+ function nelderMead(fn, x0, options = {}) {
2
+ const { maxIter = 1000, tol = 1e-8, alpha = 1, // reflection
3
+ gamma = 2, // expansion
4
+ rho = 0.5, // contraction
5
+ sigma = 0.5, // shrink
6
+ } = options;
7
+ const n = x0.length;
8
+ // Initialize simplex
9
+ const simplex = [x0.slice()];
10
+ for (let i = 0; i < n; i++) {
11
+ const point = x0.slice();
12
+ const delta = point[i] === 0 ? 0.00025 : point[i] * 0.05;
13
+ point[i] += delta;
14
+ simplex.push(point);
15
+ }
16
+ let values = simplex.map(fn);
17
+ let iterations = 0;
18
+ let converged = false;
19
+ for (iterations = 0; iterations < maxIter; iterations++) {
20
+ // Sort simplex by function values
21
+ const indices = values.map((_, i) => i).sort((a, b) => values[a] - values[b]);
22
+ const sortedSimplex = indices.map(i => simplex[i]);
23
+ const sortedValues = indices.map(i => values[i]);
24
+ for (let i = 0; i <= n; i++) {
25
+ simplex[i] = sortedSimplex[i];
26
+ values[i] = sortedValues[i];
27
+ }
28
+ // Check convergence
29
+ const range = values[n] - values[0];
30
+ if (range < tol) {
31
+ converged = true;
32
+ break;
33
+ }
34
+ // Centroid of all points except worst
35
+ const centroid = new Array(n).fill(0);
36
+ for (let i = 0; i < n; i++) {
37
+ for (let j = 0; j < n; j++) {
38
+ centroid[j] += simplex[i][j] / n;
39
+ }
40
+ }
41
+ // Reflection
42
+ const reflected = centroid.map((c, j) => c + alpha * (c - simplex[n][j]));
43
+ const fr = fn(reflected);
44
+ if (fr < values[0]) {
45
+ // Expansion
46
+ const expanded = centroid.map((c, j) => c + gamma * (reflected[j] - c));
47
+ const fe = fn(expanded);
48
+ if (fe < fr) {
49
+ simplex[n] = expanded;
50
+ values[n] = fe;
51
+ }
52
+ else {
53
+ simplex[n] = reflected;
54
+ values[n] = fr;
55
+ }
56
+ }
57
+ else if (fr < values[n - 1]) {
58
+ simplex[n] = reflected;
59
+ values[n] = fr;
60
+ }
61
+ else {
62
+ // Contraction
63
+ if (fr < values[n]) {
64
+ // Outside contraction
65
+ const contracted = centroid.map((c, j) => c + rho * (reflected[j] - c));
66
+ const fc = fn(contracted);
67
+ if (fc <= fr) {
68
+ simplex[n] = contracted;
69
+ values[n] = fc;
70
+ }
71
+ else {
72
+ // Shrink
73
+ shrink(simplex, values, sigma, fn, n);
74
+ }
75
+ }
76
+ else {
77
+ // Inside contraction
78
+ const contracted = centroid.map((c, j) => c + rho * (simplex[n][j] - c));
79
+ const fc = fn(contracted);
80
+ if (fc < values[n]) {
81
+ simplex[n] = contracted;
82
+ values[n] = fc;
83
+ }
84
+ else {
85
+ // Shrink
86
+ shrink(simplex, values, sigma, fn, n);
87
+ }
88
+ }
89
+ }
90
+ }
91
+ return {
92
+ x: simplex[0],
93
+ fx: values[0],
94
+ iterations,
95
+ converged,
96
+ };
97
+ }
98
+ function shrink(simplex, values, sigma, fn, n) {
99
+ for (let i = 1; i <= n; i++) {
100
+ for (let j = 0; j < n; j++) {
101
+ simplex[i][j] = simplex[0][j] + sigma * (simplex[i][j] - simplex[0][j]);
102
+ }
103
+ values[i] = fn(simplex[i]);
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Calculate log returns from candles
109
+ */
110
+ function calculateReturns(candles) {
111
+ const returns = [];
112
+ for (let i = 1; i < candles.length; i++) {
113
+ if (candles[i].close <= 0 || candles[i - 1].close <= 0) {
114
+ throw new Error(`Invalid close price at index ${i}`);
115
+ }
116
+ returns.push(Math.log(candles[i].close / candles[i - 1].close));
117
+ }
118
+ return returns;
119
+ }
120
+ /**
121
+ * Calculate log returns from price array
122
+ */
123
+ function calculateReturnsFromPrices(prices) {
124
+ const returns = [];
125
+ for (let i = 1; i < prices.length; i++) {
126
+ if (prices[i] <= 0 || prices[i - 1] <= 0) {
127
+ throw new Error(`Invalid price at index ${i}`);
128
+ }
129
+ returns.push(Math.log(prices[i] / prices[i - 1]));
130
+ }
131
+ return returns;
132
+ }
133
+ /**
134
+ * Calculate sample variance (mean-zero assumption)
135
+ */
136
+ function sampleVariance(returns) {
137
+ return returns.reduce((sum, r) => sum + r * r, 0) / returns.length;
138
+ }
139
+ /**
140
+ * Calculate sample variance with mean adjustment
141
+ */
142
+ function sampleVarianceWithMean(returns) {
143
+ const mean = returns.reduce((sum, r) => sum + r, 0) / returns.length;
144
+ return returns.reduce((sum, r) => sum + (r - mean) ** 2, 0) / (returns.length - 1);
145
+ }
146
+ /**
147
+ * Check for leverage effect (asymmetry in volatility)
148
+ */
149
+ function checkLeverageEffect(returns) {
150
+ const negative = returns.filter(r => r < 0);
151
+ const positive = returns.filter(r => r > 0);
152
+ if (negative.length === 0 || positive.length === 0) {
153
+ return {
154
+ negativeVol: 0,
155
+ positiveVol: 0,
156
+ ratio: 1,
157
+ recommendation: 'garch',
158
+ };
159
+ }
160
+ const negativeVol = Math.sqrt(negative.reduce((s, r) => s + r * r, 0) / negative.length);
161
+ const positiveVol = Math.sqrt(positive.reduce((s, r) => s + r * r, 0) / positive.length);
162
+ const ratio = negativeVol / positiveVol;
163
+ return {
164
+ negativeVol,
165
+ positiveVol,
166
+ ratio,
167
+ recommendation: ratio > 1.2 ? 'egarch' : 'garch',
168
+ };
169
+ }
170
+ /**
171
+ * Expected value of |Z| where Z ~ N(0,1)
172
+ * E[|Z|] = sqrt(2/π)
173
+ */
174
+ const EXPECTED_ABS_NORMAL = Math.sqrt(2 / Math.PI);
175
+ /**
176
+ * Calculate AIC (Akaike Information Criterion)
177
+ */
178
+ function calculateAIC(logLikelihood, numParams) {
179
+ return 2 * numParams - 2 * logLikelihood;
180
+ }
181
+ /**
182
+ * Calculate BIC (Bayesian Information Criterion)
183
+ */
184
+ function calculateBIC(logLikelihood, numParams, numObs) {
185
+ return numParams * Math.log(numObs) - 2 * logLikelihood;
186
+ }
187
+
188
+ /**
189
+ * GARCH(1,1) model
190
+ *
191
+ * σ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁
192
+ *
193
+ * where:
194
+ * - ω (omega) > 0: constant term
195
+ * - α (alpha) ≥ 0: ARCH parameter (reaction to shocks)
196
+ * - β (beta) ≥ 0: GARCH parameter (persistence)
197
+ * - α + β < 1: stationarity condition
198
+ */
199
+ class Garch {
200
+ returns;
201
+ periodsPerYear;
202
+ initialVariance;
203
+ constructor(data, options = {}) {
204
+ this.periodsPerYear = options.periodsPerYear ?? 252;
205
+ if (data.length < 50) {
206
+ throw new Error('Need at least 50 data points for GARCH estimation');
207
+ }
208
+ // Determine if input is candles or prices
209
+ if (typeof data[0] === 'number') {
210
+ this.returns = calculateReturnsFromPrices(data);
211
+ }
212
+ else {
213
+ this.returns = calculateReturns(data);
214
+ }
215
+ this.initialVariance = sampleVariance(this.returns);
216
+ }
217
+ /**
218
+ * Calibrate GARCH(1,1) parameters using Maximum Likelihood Estimation
219
+ */
220
+ fit(options = {}) {
221
+ const { maxIter = 1000, tol = 1e-8 } = options;
222
+ const returns = this.returns;
223
+ const n = returns.length;
224
+ const initVar = this.initialVariance;
225
+ // Negative log-likelihood function
226
+ function negLogLikelihood(params) {
227
+ const [omega, alpha, beta] = params;
228
+ // Constraints
229
+ if (omega <= 1e-12)
230
+ return 1e10;
231
+ if (alpha < 0 || beta < 0)
232
+ return 1e10;
233
+ if (alpha + beta >= 0.9999)
234
+ return 1e10;
235
+ let variance = initVar;
236
+ let ll = 0;
237
+ for (let i = 0; i < n; i++) {
238
+ if (i > 0) {
239
+ variance = omega + alpha * returns[i - 1] ** 2 + beta * variance;
240
+ }
241
+ if (variance <= 1e-12)
242
+ return 1e10;
243
+ // Gaussian log-likelihood (dropping constant)
244
+ ll += Math.log(variance) + (returns[i] ** 2) / variance;
245
+ }
246
+ return ll / 2;
247
+ }
248
+ // Initial guesses
249
+ const omega0 = initVar * 0.05;
250
+ const alpha0 = 0.1;
251
+ const beta0 = 0.85;
252
+ const result = nelderMead(negLogLikelihood, [omega0, alpha0, beta0], { maxIter, tol });
253
+ const [omega, alpha, beta] = result.x;
254
+ const persistence = alpha + beta;
255
+ const unconditionalVariance = omega / (1 - persistence);
256
+ const annualizedVol = Math.sqrt(unconditionalVariance * this.periodsPerYear) * 100;
257
+ const logLikelihood = -result.fx * 2;
258
+ const numParams = 3;
259
+ return {
260
+ params: {
261
+ omega,
262
+ alpha,
263
+ beta,
264
+ persistence,
265
+ unconditionalVariance,
266
+ annualizedVol,
267
+ },
268
+ diagnostics: {
269
+ logLikelihood,
270
+ aic: calculateAIC(logLikelihood, numParams),
271
+ bic: calculateBIC(logLikelihood, numParams, n),
272
+ iterations: result.iterations,
273
+ converged: result.converged,
274
+ },
275
+ };
276
+ }
277
+ /**
278
+ * Calculate conditional variance series given parameters
279
+ */
280
+ getVarianceSeries(params) {
281
+ const { omega, alpha, beta } = params;
282
+ const variance = [];
283
+ for (let i = 0; i < this.returns.length; i++) {
284
+ if (i === 0) {
285
+ variance.push(this.initialVariance);
286
+ }
287
+ else {
288
+ const v = omega + alpha * this.returns[i - 1] ** 2 + beta * variance[i - 1];
289
+ variance.push(v);
290
+ }
291
+ }
292
+ return variance;
293
+ }
294
+ /**
295
+ * Forecast variance forward
296
+ */
297
+ forecast(params, steps = 1) {
298
+ const { omega, alpha, beta } = params;
299
+ const variance = [];
300
+ // Get last variance
301
+ const varianceSeries = this.getVarianceSeries(params);
302
+ const lastVariance = varianceSeries[varianceSeries.length - 1];
303
+ const lastReturn = this.returns[this.returns.length - 1];
304
+ // One-step ahead
305
+ let v = omega + alpha * lastReturn ** 2 + beta * lastVariance;
306
+ variance.push(v);
307
+ // Multi-step ahead (converges to unconditional variance)
308
+ for (let h = 1; h < steps; h++) {
309
+ v = omega + (alpha + beta) * v;
310
+ variance.push(v);
311
+ }
312
+ return {
313
+ variance,
314
+ volatility: variance.map(v => Math.sqrt(v)),
315
+ annualized: variance.map(v => Math.sqrt(v * this.periodsPerYear) * 100),
316
+ };
317
+ }
318
+ /**
319
+ * Get the return series
320
+ */
321
+ getReturns() {
322
+ return [...this.returns];
323
+ }
324
+ /**
325
+ * Get initial variance estimate
326
+ */
327
+ getInitialVariance() {
328
+ return this.initialVariance;
329
+ }
330
+ }
331
+ /**
332
+ * Convenience function to calibrate GARCH(1,1) from candles
333
+ */
334
+ function calibrateGarch(data, options = {}) {
335
+ const model = new Garch(data, options);
336
+ return model.fit(options);
337
+ }
338
+
339
+ /**
340
+ * EGARCH(1,1) model (Nelson, 1991)
341
+ *
342
+ * ln(σ²ₜ) = ω + α·(|zₜ₋₁| - E[|z|]) + γ·zₜ₋₁ + β·ln(σ²ₜ₋₁)
343
+ *
344
+ * where:
345
+ * - zₜ = εₜ/σₜ (standardized residual)
346
+ * - ω (omega): constant term
347
+ * - α (alpha): magnitude effect
348
+ * - γ (gamma): leverage effect (typically negative)
349
+ * - β (beta): persistence
350
+ * - E[|z|] = √(2/π) for standard normal
351
+ */
352
+ class Egarch {
353
+ returns;
354
+ periodsPerYear;
355
+ initialVariance;
356
+ constructor(data, options = {}) {
357
+ this.periodsPerYear = options.periodsPerYear ?? 252;
358
+ if (data.length < 50) {
359
+ throw new Error('Need at least 50 data points for EGARCH estimation');
360
+ }
361
+ if (typeof data[0] === 'number') {
362
+ this.returns = calculateReturnsFromPrices(data);
363
+ }
364
+ else {
365
+ this.returns = calculateReturns(data);
366
+ }
367
+ this.initialVariance = sampleVariance(this.returns);
368
+ }
369
+ /**
370
+ * Calibrate EGARCH(1,1) parameters using Maximum Likelihood Estimation
371
+ */
372
+ fit(options = {}) {
373
+ const { maxIter = 1000, tol = 1e-8 } = options;
374
+ const returns = this.returns;
375
+ const n = returns.length;
376
+ const initLogVar = Math.log(this.initialVariance);
377
+ function negLogLikelihood(params) {
378
+ const [omega, alpha, gamma, beta] = params;
379
+ // EGARCH allows negative gamma, but beta should ensure stationarity
380
+ if (Math.abs(beta) >= 0.9999)
381
+ return 1e10;
382
+ let logVariance = initLogVar;
383
+ let variance = Math.exp(logVariance);
384
+ let ll = 0;
385
+ for (let i = 0; i < n; i++) {
386
+ if (i > 0) {
387
+ const sigma = Math.sqrt(variance);
388
+ const z = returns[i - 1] / sigma;
389
+ logVariance = omega
390
+ + alpha * (Math.abs(z) - EXPECTED_ABS_NORMAL)
391
+ + gamma * z
392
+ + beta * logVariance;
393
+ // Prevent extreme values
394
+ logVariance = Math.max(-50, Math.min(50, logVariance));
395
+ variance = Math.exp(logVariance);
396
+ }
397
+ if (variance <= 1e-12 || !isFinite(variance))
398
+ return 1e10;
399
+ ll += Math.log(variance) + (returns[i] ** 2) / variance;
400
+ }
401
+ return ll / 2;
402
+ }
403
+ // Initial guesses
404
+ // omega approximates log of unconditional variance when other params are small
405
+ const omega0 = initLogVar * 0.1;
406
+ const alpha0 = 0.1;
407
+ const gamma0 = -0.05; // Negative for typical leverage effect
408
+ const beta0 = 0.95;
409
+ const result = nelderMead(negLogLikelihood, [omega0, alpha0, gamma0, beta0], { maxIter, tol });
410
+ const [omega, alpha, gamma, beta] = result.x;
411
+ // For EGARCH, unconditional variance: E[ln(σ²)] = ω/(1-β)
412
+ // So E[σ²] ≈ exp(ω/(1-β)) when α and γ effects average out
413
+ const unconditionalLogVar = omega / (1 - beta);
414
+ const unconditionalVariance = Math.exp(unconditionalLogVar);
415
+ const annualizedVol = Math.sqrt(unconditionalVariance * this.periodsPerYear) * 100;
416
+ const logLikelihood = -result.fx * 2;
417
+ const numParams = 4;
418
+ return {
419
+ params: {
420
+ omega,
421
+ alpha,
422
+ gamma,
423
+ beta,
424
+ persistence: beta, // In EGARCH, persistence is primarily driven by beta
425
+ unconditionalVariance,
426
+ annualizedVol,
427
+ leverageEffect: gamma,
428
+ },
429
+ diagnostics: {
430
+ logLikelihood,
431
+ aic: calculateAIC(logLikelihood, numParams),
432
+ bic: calculateBIC(logLikelihood, numParams, n),
433
+ iterations: result.iterations,
434
+ converged: result.converged,
435
+ },
436
+ };
437
+ }
438
+ /**
439
+ * Calculate conditional variance series given parameters
440
+ */
441
+ getVarianceSeries(params) {
442
+ const { omega, alpha, gamma, beta } = params;
443
+ const variance = [];
444
+ let logVariance = Math.log(this.initialVariance);
445
+ for (let i = 0; i < this.returns.length; i++) {
446
+ if (i === 0) {
447
+ variance.push(this.initialVariance);
448
+ }
449
+ else {
450
+ const sigma = Math.sqrt(variance[i - 1]);
451
+ const z = this.returns[i - 1] / sigma;
452
+ logVariance = omega
453
+ + alpha * (Math.abs(z) - EXPECTED_ABS_NORMAL)
454
+ + gamma * z
455
+ + beta * logVariance;
456
+ logVariance = Math.max(-50, Math.min(50, logVariance));
457
+ variance.push(Math.exp(logVariance));
458
+ }
459
+ }
460
+ return variance;
461
+ }
462
+ /**
463
+ * Forecast variance forward
464
+ *
465
+ * Note: EGARCH forecasts are more complex because they depend on
466
+ * the path of shocks. This provides an approximation assuming
467
+ * expected values of future shocks.
468
+ */
469
+ forecast(params, steps = 1) {
470
+ const { omega, alpha, gamma, beta } = params;
471
+ const variance = [];
472
+ const varianceSeries = this.getVarianceSeries(params);
473
+ const lastVariance = varianceSeries[varianceSeries.length - 1];
474
+ const lastReturn = this.returns[this.returns.length - 1];
475
+ // One-step ahead using actual last return
476
+ const sigma = Math.sqrt(lastVariance);
477
+ const z = lastReturn / sigma;
478
+ let logVariance = omega
479
+ + alpha * (Math.abs(z) - EXPECTED_ABS_NORMAL)
480
+ + gamma * z
481
+ + beta * Math.log(lastVariance);
482
+ variance.push(Math.exp(logVariance));
483
+ // Multi-step: assume E[z] = 0, E[|z|] = √(2/π)
484
+ // So the α and γ terms contribute 0 on average
485
+ for (let h = 1; h < steps; h++) {
486
+ logVariance = omega + beta * logVariance;
487
+ variance.push(Math.exp(logVariance));
488
+ }
489
+ return {
490
+ variance,
491
+ volatility: variance.map(v => Math.sqrt(v)),
492
+ annualized: variance.map(v => Math.sqrt(v * this.periodsPerYear) * 100),
493
+ };
494
+ }
495
+ /**
496
+ * Get the return series
497
+ */
498
+ getReturns() {
499
+ return [...this.returns];
500
+ }
501
+ /**
502
+ * Get initial variance estimate
503
+ */
504
+ getInitialVariance() {
505
+ return this.initialVariance;
506
+ }
507
+ }
508
+ /**
509
+ * Convenience function to calibrate EGARCH(1,1) from candles
510
+ */
511
+ function calibrateEgarch(data, options = {}) {
512
+ const model = new Egarch(data, options);
513
+ return model.fit(options);
514
+ }
515
+
516
+ export { EXPECTED_ABS_NORMAL, Egarch, Garch, calculateReturns, calculateReturnsFromPrices, calibrateEgarch, calibrateGarch, checkLeverageEffect, nelderMead, sampleVariance, sampleVarianceWithMean };
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "garch",
3
+ "version": "1.0.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
+ "rollup": "^4.57.1",
41
+ "rollup-plugin-dts": "^6.3.0",
42
+ "rollup-plugin-peer-deps-external": "^2.2.4",
43
+ "tslib": "^2.8.1",
44
+ "typescript": "^5.3.0",
45
+ "vitest": "^1.0.0"
46
+ }
47
+ }