-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathHNGarch.py
571 lines (447 loc) · 17.5 KB
/
HNGarch.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
from numpy.random import normal
from math import log, pi, sqrt, exp
from statistics import variance
from scipy.stats import norm
from scipy.optimize import minimize
import numpy as np
#############
# GENERAL #
#############
def llhngarch(par, ts=None, r_f=0., symmetric=False):
'''
Parameters
----------
par : list
List of parameters of the function.
ts : list, optional
timeseries of log-returns. The default is None.
r_f : float, optional
risk-free interest rate. The default is 0..
symmetric : bool, optional
boolean, if True the gamma parameter is set to 0. The default is False.
Returns
-------
float
returns the negative log-likelihood of the Heston-Nandi GARCH model with the given parameters.
'''
# init
h_vec = []
z_vec = []
ll = []
r = r_f
omega = 1/(1+exp(-par[0]))
alpha = 1/(1+exp(-par[1]))
beta = 1/(1+exp(-par[2]))
if not symmetric : gam = par[3]
else: gam = 0
lam = par[4]
# barrier function to ensure stationarity
if beta + (alpha * gam * gam) > 1: return 1e50
h = variance(ts)
h_vec.append(h)
z = (ts[0] - r - lam * h)/sqrt(h)
z_vec.append(z)
try:
l = log(norm.pdf(z)/sqrt(h))
except:
l = 1e50
ll.append(l)
for i in range(1, len(ts)):
h = omega + alpha * pow(z - gam * sqrt(h),2) + beta * h
h_vec.append(h)
z = (ts[i] - r - lam * h)/sqrt(h)
z_vec.append(z)
try:
l = log(norm.pdf(z)/sqrt(h))
except:
l = 1e50
ll.append(l)
return -1 * sum(ll)
def A_loop(a, b, r_f, phi, omega, alpha):
a = a + phi * r_f + b *omega - 0.5*np.log(1 - 2 * alpha * b)
return a
def B_loop(b, phi, alpha, beta, gam, lam):
b = phi * (lam + gam) - 0.5 * gam**2 + beta * b + (0.5*(phi - gam)**2)/(1 - 2 * alpha * b)
return b
########################
# Heston-Nandi GARCH #
########################
class HNGarch(object):
'''
Attributes
----------
timeseries : list
timeseries over which the parameters will be estimated. The default is None.
r_f : float, optional
risk-free rate. The default is 0..
omega : float, optional
parameter omega of the Heston-Nandi GARCH model, represents the constant. The default is None.
alpha : float, optional
parameter alpha of the Heston-Nandi GARCH model, determines the kurtosis of the distribution. The default is None.
beta : float, optional
parameter beta of the Heston-Nandi GARCH model. The default is None.
gamma : TYPE, optional
parameter gamma of the Heston-Nandi GARCH model, represents the skewness of the distribution. The default is None.
p_lambda : TYPE, optional
parameter lambda of the Heston-Nandi GARCH model. The default is None.
lr_var : TYPE, optional
long-run variance of the estimated GARCH model. The default is None.
gamma_star : TYPE, optional
risk-neutral gamma parameter for Heston-Nandi GARCH forecasting. The default is None.
h_t0 : TYPE, optional
terminal value of the variance process of the estimated Heston-Nandi GARCH model. The default is None.
Methods
-------
GARCH_fit(self)
estimate model parameters through maximum-likelihood estimation.
lr_variance(self)
computes long-run variance of the process, returns long-run variance.
lr_vol(self)
returns long-run volatility of the process.
get_std_errors(self)
returns the standard errors of the estimated parameters omega, alpha, beta, gamma, lambda.
ts_var(self, vec)
computes estimated GARCH variance process of the time-series, returns last value or whole list.
GARCH_single_fc(self, n_steps, ret_vec)
computes n_steps days forward estimation, returns the last price or the list of prices.
montecarlo_sim(self, n_reps, n_steps)
performs montecarlo simulation of the price of the asset with n_reps repetitions.
hist_simulation(self, std)
estimates fitted model over the period and returns residuals.
pdf_func(self, x, steps, r, up_lim, low_lim, prec)
returns the probability mass point of the model at x (log-price) at a given point in the future (nsteps)
cdf_func(self, x, steps, r, up_lim, low_lim, prec)
returns the cumulative probability of the log-price being less or eqaul than x at a given point in the future (nsteps)
'''
def __init__(self, timeseries=None, r_f=0.):
self.timeseries = list(timeseries)
self.omega = None
self.alpha = None
self.beta = None
self.gamma = None
self.p_lambda = None
self.lr_var = None
self.r_f = r_f
self.gamma_star = None
self.h_t0 = None
self.std_errors = None
# class method that estimates the parameters of the model
def GARCH_fit(self, symmetric=False):
'''
Parameters
----------
symmetric : bool, optional
if True the GARCH model is symmetric, the gamma parameter is held 0. The default False.
Returns
-------
None.
'''
r = self.r_f
ts = self.timeseries.copy()
r_t = [log(ts[i]/ts[i-1]) for i in range(1,len(ts))]
omega = variance(r_t)
alpha = 0.1 * variance(r_t)
beta = 0.1
gam = 0.
lam = -0.5
omega = -log((1-omega)/omega)
alpha = -log((1-alpha)/alpha)
beta = -log((1-beta)/beta)
par = [omega, alpha, beta, gam, lam]
res = minimize(llhngarch, par, args=(r_t, r, False), method='L-BFGS-B')
# computing standard errors of garch model through the inverse of the hessian matrix
hess_inv = res.hess_inv.todense()
diag = np.diag(hess_inv)
self.std_errors = np.sqrt(diag)
omega = (1/(1 + exp(-res.x[0])))
alpha = (1/(1 + exp(-res.x[1])))
beta = (1/(1 + exp(-res.x[2])))
lam = res.x[4]
if symmetric: gam =0.
else: gam = res.x[3]
self.gamma_star = lam + gam + 0.5
self.omega, self.alpha, self.beta, self.gamma, self.p_lambda = omega, alpha, beta, gam, lam
print('Estimation results:')
print('-------------------')
print('Omega: ' + str(omega))
print('Alpha: ' + str(alpha))
print('Beta: '+str(beta))
if not symmetric: print('Gamma: ' + str(gam))
print('Lambda: ' + str(lam))
persistence = beta + alpha * pow(gam,2)
print('\n')
print('Model persistence: ' + str(round(persistence,6)))
self.lr_var = (omega + alpha)/(1-persistence)
print('Long-run variance: ' + str(round(self.lr_var,6)))
print('\n')
# class method that returns the long-run variance of the GARCH model
def lr_variance(self):
'''
Returns
-------
float
long-run variance of the estimated process.
'''
print('Model variance')
print('----------------')
print('Daily variance: ' + str(round(self.lr_var*100,4)) + '%')
return self.lr_var
# class method that returns the long-run volatility of the GARCH model (sqrt of the variance)
def lr_vol(self):
'''
Returns
-------
float
long-run volatility of the estimated process.
'''
print('Model volatility')
print('----------------')
print('Annualized volatility: ' + str(round(sqrt(self.lr_var)*sqrt(252)*100,4)) + '%')
print('Daily volatility: ' + str(round(sqrt(self.lr_var)*100,4)) + '%')
return sqrt(self.lr_var)
def get_std_errors(self):
'''
Returns
-------
array
standard errors of the estimates computed using inverse hessian matrix.
'''
print('Standard errors for the estimates:')
print('----------------------------------')
print('omega: ' + str(self.std_errors[0]))
print('alpha: ' + str(self.std_errors[1]))
print('beta: ' + str(self.std_errors[2]))
print('gamma: ' + str(self.std_errors[3]))
print('lambda: ' + str(self.std_errors[4]))
return self.std_errors
# class method that estimates the final variance of the ts of log returns
def ts_var(self, vec=False):
'''
Parameters
----------
vec : bool, optional
if True returns the whole process of the estimated variance. The default is False.
Returns
-------
float or list
returns the terminal variance of the process, if vec = True returns the whole process of the estimated variance.
'''
ts = self.timeseries.copy()
r_f = self.r_f
params = [self.omega, self.alpha, self.beta, self.gamma, self.p_lambda]
r_t = [log(ts[i]/ts[i-1]) for i in range(1,len(ts))]
h_vec = []
h_t = variance(r_t)
h_vec.append(h_t)
n = len(r_t)
for i in range(1,n+1):
z_t = pow((r_t[i-1] - r_f - params[4] * h_t - params[3] * h_t), 2)
h_t = params[0] + params[1] * z_t / h_t + params[2] * h_t
h_vec.append(h_t)
self.h_t0 = h_vec[-1]
# before
# if vec:
# return h_vec
# changed because gives problems with filtering as we don't have any record for the (final day+1)
# but we can theoretically compute the variance
if vec:
return h_vec[:-1]
else:
return h_vec[-1]
# function to get standrardized residuals is residuals/sqrt(h_t)
def hist_simulation(self):
'''
Returns
-------
residuals : list
returns standardized residuals of the fitted time series.
'''
# init
ts = self.timeseries
r_f = self.r_f
params = [self.omega, self.alpha, self.beta, self.gamma, self.p_lambda]
r_t = [log(ts[i]/ts[i-1]) for i in range(1,len(ts))]
h_vec =[]
r_vec = []
# h_0 is the sample variance
h_t = variance(r_t)
z = (r_t[0] - r_f - params[4] * h_t) / sqrt(h_t)
for i in range(1,len(r_t)):
h_t = params[0] + params[2] * h_t + params[1]*pow(z - params[3]*sqrt(h_t),2)
h_vec.append(h_t)
z = (r_t[i] - r_f - params[4] * h_t) / sqrt(h_t)
r_vec.append(z)
residuals = [r/sqrt(v) for r,v in zip(r_vec, h_vec)]
return residuals
# can be updated to accomodate montecarlo simulation
def GARCH_single_fc(self, n_steps=252, vec=False):
'''
Parameters
----------
n_steps : int, optional
number of future periods to forecast. The default is 252 (trading days in a year).
vec : bool, optional
if True returns the whole price process. The default is False.
Returns
-------
float or list
returns the terminal price, if vec = True returns the whole price process.
'''
params = [self.omega, self.alpha, self.beta, self.gamma_star, self.p_lambda]
h_t = self.h_t0
z_star = normal(0,1)
s_t = log(self.timeseries[-1])
s = []
for i in range(n_steps):
# volatility process
h_t = params[0] + params[2] * h_t + params[1] * pow(z_star - params[3] * sqrt(h_t), 2)
# logreturns process
z_star = normal(0,1)
s_t = s_t + self.r_f - 0.5 * h_t + sqrt(h_t) * z_star
s.append(exp(s_t))
if vec:
return s
else:
return s[-1]
def montecarlo_sim(self, n_reps=5e3, n_steps=252):
'''
Parameters
----------
n_reps : int, optional
number of simulations. The default is 5e3.
n_steps : int, optional
number of future periods to forecast. The default is 252 (trading days in a year).
Returns
-------
float
average terminal value of the asset.
'''
res = []
for i in np.arange(n_reps):
s = HNGarch.GARCH_single_fc(self, n_steps, vec=False)
res.append(s)
return sum(res)/n_reps
########################
# Heston-Nandi GARCH #
# Num. Integration #
########################
def pdf_func(self, x, steps, r=0., up_lim=100, low_lim=0, prec=10000):
'''
Parameters
----------
x : float/array
point/s of evaluation of the probability density function.
steps : int
number of days to expiration.
r : float, optional
risk-free interest rate. The default is 0..
up_lim : float, optional
superior bound of the integration interval. The default is 100.
low_lim : TYPE, optional
inferior bound of the integration interval. The default is 0.
prec : int, optional
number of points for integral computation. The default is 10000.
Returns
-------
float/array
value of the probability density function in x, or array of pdfs (if x is an array).
'''
# init
omega = self.omega
alpha = self.alpha
beta = self.beta
gam = self.gamma_star
lam = -0.5
s = self.timeseries[-1]
h = self.ts_var(vec=False)
hi = float(up_lim-low_lim)/prec
t = np.linspace(low_lim + hi/2, up_lim- hi/2, prec)
t[t == 0] = 1e-10
a = np.zeros(len(t))
b = np.zeros(len(t))
it = t * np.complex(0,1)
for i in range(steps):
a = A_loop(a, b, r, it, omega, alpha)
b = B_loop(b, it, alpha, beta, gam, lam)
vec = x.reshape(-1,1)
f = s**it
g = np.exp(a + b * h)
func = np.real(np.exp(-it*vec) * f * g)
area = func*hi
return area.sum(axis=1)/np.pi
def cdf_func(self, x, steps, r=0., up_lim=100, low_lim=0, prec=10000):
'''
Parameters
----------
x : float/array
point/s of evaluation of the cumulative density function.
steps : int
number of days to expiration.
r : float, optional
risk-free interest rate. The default is 0..
up_lim : float, optional
superior bound of the integration interval. The default is 100.
low_lim : TYPE, optional
inferior bound of the integration interval. The default is 0.
prec : int, optional
number of points for integral computation. The default is 10000.
Returns
-------
float/array
value of the cumulative density function in x, or array of cdfs (if x is an array).
'''
# init
omega = self.omega
alpha = self.alpha
beta = self.beta
gam = self.gamma_star
lam = -0.5
s = self.timeseries[-1]
h = self.ts_var(vec=False)
hi = float(up_lim-low_lim)/prec
t = np.linspace(low_lim + hi/2, up_lim- hi/2, prec)
t[t == 0] = 1e-10
a = np.zeros(len(t))
b = np.zeros(len(t))
it = t * np.complex(0,1)
for i in range(steps):
a = A_loop(a, b, r, it, omega, alpha)
b = B_loop(b, it, alpha, beta, gam, lam)
vec = x.reshape(-1,1)
f = s**it
g = np.exp(a + b * h)
j = np.exp(-it*vec) * f * g
func = np.real(j/it)
area = func*hi
return 0.5 - area.sum(axis=1)/np.pi
#%% MAIN
if __name__ == '__main__':
import time
import pandas as pd
from matplotlib import pyplot as plt
# importing timeseries as list of prices
ts = pd.read_csv(r'C:\Users\edoardo_berton\Desktop\copula_op\code\WTI_ts.csv')['Close']
# init of the class
model = HNGarch(ts)
# fitting the model
model.GARCH_fit()
vec1 = model.hist_simulation()
x = np.arange(-5, np.log(7000), 0.01)
x[x==0] = 1e-10
pdf = model.pdf_func(x, 90)
cdf = model.cdf_func(x, 90)
#%%
std_errs = model.get_std_errors()
model.lr_vol()
# vector of variances of the estimated model
vec = model.ts_var(vec=True)
# # plot the simulated garch model over a 252d future interval
j = pd.DataFrame()
# how many simulations to plot
number_scenarios = 10
for i in range(number_scenarios):
y = pd.DataFrame(model.GARCH_single_fc(252,True))
j = pd.concat([j,y], axis=1)
j.plot(legend=False)
# montecarlo simulation on the price of the underlying (num_simulations, days period)
avg_price = model.montecarlo_sim(10000, 252)