Skip to content

Commit 6a36dc7

Browse files
committed
ENH: Allow estimation of EWMA Variance parameters
Allow estimation of EWMA variance parameter along with other model parameters
1 parent 1b2be26 commit 6a36dc7

File tree

4 files changed

+89
-17
lines changed

4 files changed

+89
-17
lines changed

arch/tests/univariate/test_volatility.py

+51-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import warnings
2-
from unittest import TestCase
3-
41
import numpy as np
52
import pytest
3+
import warnings
64
from numpy.testing import assert_almost_equal, assert_equal, assert_allclose, \
75
assert_array_equal
6+
from unittest import TestCase
87

98
try:
109
from arch.univariate import _recursions as rec
@@ -697,6 +696,55 @@ def test_ewma(self):
697696
txt = ewma.__repr__()
698697
assert str(hex(id(ewma))) in txt
699698

699+
def test_ewma_estimated(self):
700+
ewma = EWMAVariance(lam=None)
701+
702+
sv = ewma.starting_values(self.resids)
703+
assert sv == 0.94
704+
assert sv.shape[0] == ewma.num_params
705+
706+
bounds = ewma.bounds(self.resids)
707+
assert len(bounds) == 1
708+
assert bounds == [(0, 1)]
709+
710+
var_bounds = ewma.variance_bounds(self.resids)
711+
712+
backcast = ewma.backcast(self.resids)
713+
w = 0.94 ** np.arange(75)
714+
assert_almost_equal(backcast,
715+
np.sum((self.resids[:75] ** 2) * (w / w.sum())))
716+
717+
parameters = np.array([0.9234])
718+
719+
var_bounds = ewma.variance_bounds(self.resids)
720+
ewma.compute_variance(parameters, self.resids, self.sigma2, backcast, var_bounds)
721+
cond_var_direct = np.zeros_like(self.sigma2)
722+
cond_var_direct[0] = backcast
723+
parameters = np.array([0, 1-parameters[0], parameters[0]])
724+
rec.garch_recursion(parameters,
725+
self.resids ** 2.0,
726+
np.sign(self.resids),
727+
cond_var_direct,
728+
1, 0, 1, self.T, backcast, var_bounds)
729+
assert_allclose(self.sigma2, cond_var_direct)
730+
assert_allclose(self.sigma2 / cond_var_direct, np.ones_like(self.sigma2))
731+
732+
names = ewma.parameter_names()
733+
names_target = ['lam']
734+
assert_equal(names, names_target)
735+
736+
a, b = ewma.constraints()
737+
a_target = np.ones((1, 1))
738+
b_target = np.zeros((1,))
739+
assert_array_equal(a, a_target)
740+
assert_array_equal(b, b_target)
741+
742+
assert_equal(ewma.num_params, 1)
743+
assert_equal(ewma.name, 'EWMA/RiskMetrics')
744+
assert isinstance(ewma.__str__(), str)
745+
txt = ewma.__repr__()
746+
assert str(hex(id(ewma))) in txt
747+
700748
def test_riskmetrics(self):
701749
rm06 = RiskMetrics2006()
702750

arch/univariate/volatility.py

+36-14
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from __future__ import absolute_import, division
77

88
import itertools
9-
109
import numpy as np
1110
from numpy import (sqrt, ones, zeros, isscalar, sign, ones_like, arange, empty, abs, array, finfo,
1211
float64, log, exp, floor)
@@ -37,6 +36,7 @@ class BootstrapRng(object):
3736
start : int
3837
Location of first forecast
3938
"""
39+
4040
def __init__(self, std_resid, start):
4141
if start <= 0 or start > std_resid.shape[0]:
4242
raise ValueError('start must be > 0 and <= len(std_resid).')
@@ -1294,8 +1294,9 @@ class EWMAVariance(VolatilityProcess):
12941294
12951295
Parameters
12961296
----------
1297-
lam : float, optional
1298-
Smoothing parameter. Default is 0.94
1297+
lam : {float, None}, optional
1298+
Smoothing parameter. Default is 0.94. Set to None to estimate lam
1299+
jointly with other model parameters
12991300
13001301
Attributes
13011302
----------
@@ -1317,38 +1318,52 @@ class EWMAVariance(VolatilityProcess):
13171318
13181319
\sigma_t^{2}=\lambda\sigma_{t-1}^2 + (1-\lambda)\epsilon^2_{t-1}
13191320
1320-
This model has no parameters since the smoothing parameter is fixed.
1321+
When lam is provided, this model has no parameters since the smoothing
1322+
parameter is treated as fixed. Sel lam to `None` to jointly estimate this
1323+
parameter when fitting the model.
13211324
"""
13221325

13231326
def __init__(self, lam=0.94):
13241327
super(EWMAVariance, self).__init__()
13251328
self.lam = lam
1326-
self.num_params = 0
1327-
if not 0.0 < lam < 1.0:
1329+
self._estimate_lam = lam is None
1330+
self.num_params = 1 if self._estimate_lam else 0
1331+
if lam is not None and not 0.0 < lam < 1.0:
13281332
raise ValueError('lam must be strictly between 0 and 1')
13291333
self.name = 'EWMA/RiskMetrics'
13301334

13311335
def __str__(self):
1332-
descr = self.name + '(lam: ' + '{0:0.2f}'.format(self.lam) + ')'
1336+
if self._estimate_lam:
1337+
descr = self.name + '(lam: Estimated)'
1338+
else:
1339+
descr = self.name + '(lam: ' + '{0:0.2f}'.format(self.lam) + ')'
13331340
return descr
13341341

13351342
def starting_values(self, resids):
1336-
return np.empty((0,))
1343+
if self._estimate_lam:
1344+
return np.array([0.94])
1345+
return np.array([])
13371346

13381347
def parameter_names(self):
1348+
if self._estimate_lam:
1349+
return ['lam']
13391350
return []
13401351

1341-
def variance_bounds(self, resids, power=2.0):
1342-
return ones((resids.shape[0], 1)) * array([-1.0, finfo(float64).max])
1343-
13441352
def bounds(self, resids):
1353+
if self._estimate_lam:
1354+
return [(0, 1)]
13451355
return []
13461356

13471357
def compute_variance(self, parameters, resids, sigma2, backcast,
13481358
var_bounds):
1349-
return ewma_recursion(self.lam, resids, sigma2, resids.shape[0], backcast)
1359+
lam = parameters[0] if self._estimate_lam else self.lam
1360+
return ewma_recursion(lam, resids, sigma2, resids.shape[0], backcast)
13501361

13511362
def constraints(self):
1363+
if self._estimate_lam:
1364+
a = ones((1, 1))
1365+
b = zeros((1,))
1366+
return a, b
13521367
return np.empty((0, 0)), np.empty((0,))
13531368

13541369
def simulate(self, parameters, nobs, rng, burn=500, initial_value=None):
@@ -1362,7 +1377,11 @@ def simulate(self, parameters, nobs, rng, burn=500, initial_value=None):
13621377

13631378
sigma2[0] = initial_value
13641379
data[0] = sqrt(sigma2[0])
1365-
lam, one_m_lam = self.lam, 1.0 - self.lam
1380+
if self._estimate_lam:
1381+
lam = parameters[0]
1382+
else:
1383+
lam = self.lam
1384+
one_m_lam = 1.0 - lam
13661385
for t in range(1, nobs + burn):
13671386
sigma2[t] = lam * sigma2[t - 1] + one_m_lam * data[t - 1] ** 2.0
13681387
data[t] = np.sqrt(sigma2[t]) * errors[t]
@@ -1393,7 +1412,10 @@ def _simulation_forecast(self, parameters, resids, backcast, var_bounds, start,
13931412
paths.fill(np.nan)
13941413
shocks = np.empty((t, simulations, horizon))
13951414
shocks.fill(np.nan)
1396-
lam = self.lam
1415+
if self._estimate_lam:
1416+
lam = parameters[0]
1417+
else:
1418+
lam = self.lam
13971419

13981420
for i in range(start, t):
13991421
std_shocks = rng((simulations, horizon))

doc/source/changes/4.0.txt

+1
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ Changes since 4.0
88
a ``ZeroMean`` variance process.
99
- Fixed a bug that prevented ``fix`` from being used with a new model (:issue:`156`)
1010
- Added ``first_obs`` and ``last_obs`` parameters to ``fix`` to mimic ``fit``
11+
- Added ability to jointly estimate smoothing parameter in EWMA variance when fitting the model

examples/univariate_volatility_modeling.ipynb

+1
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,7 @@
542542
" * GJR-GARCH (`GARCH` using `o` argument) \n",
543543
" * TARCH/ZARCH (`GARCH` using `power` argument set to `1`)\n",
544544
" * Power GARCH and Asymmetric Power GARCH (`GARCH` using `power`)\n",
545+
" * Exponentially Weighted Moving Average Variance with estimated coefficient (`EWMAVariance`)\n",
545546
" * Heterogeneous ARCH (`HARCH`)\n",
546547
" * Parameterless Models\n",
547548
" * Exponentially Weighted Moving Average Variance, known as RiskMetrics (`EWMAVariance`)\n",

0 commit comments

Comments
 (0)