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