multiarch 0.2.0__py3-none-any.whl
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.
- multiarch/__init__.py +18 -0
- multiarch/_kernels.py +235 -0
- multiarch/_numdiff.py +43 -0
- multiarch/dcc.py +731 -0
- multiarch/diagnostics.py +115 -0
- multiarch/ewma.py +26 -0
- multiarch/gogarch.py +220 -0
- multiarch/rolling.py +69 -0
- multiarch/simulate.py +43 -0
- multiarch/univariate.py +213 -0
- multiarch-0.2.0.dist-info/METADATA +204 -0
- multiarch-0.2.0.dist-info/RECORD +15 -0
- multiarch-0.2.0.dist-info/WHEEL +5 -0
- multiarch-0.2.0.dist-info/licenses/LICENSE +21 -0
- multiarch-0.2.0.dist-info/top_level.txt +1 -0
multiarch/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""multiarch: fast multivariate GARCH (CCC / DCC / aDCC / copula-DCC /
|
|
2
|
+
GO-GARCH) for Python."""
|
|
3
|
+
|
|
4
|
+
from .dcc import DCC, DCCResult
|
|
5
|
+
from .diagnostics import constant_correlation_test, hosking_test, var_backtest
|
|
6
|
+
from .ewma import ewma_covariance
|
|
7
|
+
from .gogarch import GOGARCH, GOGARCHResult
|
|
8
|
+
from .rolling import RollingForecast, rolling_forecast
|
|
9
|
+
from .simulate import simulate_dcc
|
|
10
|
+
from .univariate import UnivariateSpec, fit_univariate
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"DCC", "DCCResult", "GOGARCH", "GOGARCHResult", "RollingForecast",
|
|
14
|
+
"UnivariateSpec", "constant_correlation_test", "ewma_covariance",
|
|
15
|
+
"fit_univariate", "hosking_test", "rolling_forecast", "simulate_dcc",
|
|
16
|
+
"var_backtest",
|
|
17
|
+
]
|
|
18
|
+
__version__ = "0.2.0"
|
multiarch/_kernels.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""Numba kernels for the inner recursions. Everything here is O(T) loops that
|
|
2
|
+
would be slow in pure Python; the public API lives in the other modules."""
|
|
3
|
+
|
|
4
|
+
import math
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from numba import njit
|
|
8
|
+
|
|
9
|
+
LOG_2PI = math.log(2.0 * math.pi)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# ---------------------------------------------------------------- univariate
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@njit(cache=True)
|
|
16
|
+
def garch_filter(omega, alpha, gamma, beta, resid, sigma2_0):
|
|
17
|
+
"""GJR-GARCH(1,1) variance recursion (gamma=0 gives GARCH), given the
|
|
18
|
+
first conditional variance sigma2_0."""
|
|
19
|
+
n = resid.shape[0]
|
|
20
|
+
sigma2 = np.empty(n)
|
|
21
|
+
sigma2[0] = sigma2_0
|
|
22
|
+
for t in range(1, n):
|
|
23
|
+
e = resid[t - 1]
|
|
24
|
+
e2 = e * e
|
|
25
|
+
s = omega + alpha * e2 + beta * sigma2[t - 1]
|
|
26
|
+
if e < 0.0:
|
|
27
|
+
s += gamma * e2
|
|
28
|
+
sigma2[t] = s
|
|
29
|
+
return sigma2
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@njit(cache=True)
|
|
33
|
+
def normal_loglik_obs(resid, sigma2):
|
|
34
|
+
return -0.5 * (LOG_2PI + np.log(sigma2) + resid * resid / sigma2)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@njit(cache=True)
|
|
38
|
+
def std_t_loglik_obs(resid, sigma2, nu):
|
|
39
|
+
"""Unit-variance Student-t, as in arch's StudentsT."""
|
|
40
|
+
c = (
|
|
41
|
+
math.lgamma(0.5 * (nu + 1.0))
|
|
42
|
+
- math.lgamma(0.5 * nu)
|
|
43
|
+
- 0.5 * math.log(math.pi * (nu - 2.0))
|
|
44
|
+
)
|
|
45
|
+
return c - 0.5 * np.log(sigma2) - 0.5 * (nu + 1.0) * np.log(
|
|
46
|
+
1.0 + resid * resid / (sigma2 * (nu - 2.0))
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ---------------------------------------------------------------- correlation
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@njit(cache=True)
|
|
54
|
+
def _mv_logpdf(z_t, R, nu):
|
|
55
|
+
"""Log density of z_t ~ N(0, R) (nu <= 0) or unit-variance MV-t(nu)."""
|
|
56
|
+
n = z_t.shape[0]
|
|
57
|
+
L = np.linalg.cholesky(R)
|
|
58
|
+
logdet = 0.0
|
|
59
|
+
for i in range(n):
|
|
60
|
+
logdet += 2.0 * math.log(L[i, i])
|
|
61
|
+
# forward substitution: y = L^{-1} z, quad = y'y
|
|
62
|
+
y = np.empty(n)
|
|
63
|
+
quad = 0.0
|
|
64
|
+
for i in range(n):
|
|
65
|
+
s = z_t[i]
|
|
66
|
+
for k in range(i):
|
|
67
|
+
s -= L[i, k] * y[k]
|
|
68
|
+
y[i] = s / L[i, i]
|
|
69
|
+
quad += y[i] * y[i]
|
|
70
|
+
if nu <= 0.0:
|
|
71
|
+
return -0.5 * (n * LOG_2PI + logdet + quad)
|
|
72
|
+
return (
|
|
73
|
+
math.lgamma(0.5 * (nu + n))
|
|
74
|
+
- math.lgamma(0.5 * nu)
|
|
75
|
+
- 0.5 * n * math.log(math.pi * (nu - 2.0))
|
|
76
|
+
- 0.5 * logdet
|
|
77
|
+
- 0.5 * (nu + n) * math.log(1.0 + quad / (nu - 2.0))
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@njit(cache=True)
|
|
82
|
+
def _q_to_r(Q, R):
|
|
83
|
+
n = Q.shape[0]
|
|
84
|
+
for i in range(n):
|
|
85
|
+
for j in range(n):
|
|
86
|
+
R[i, j] = Q[i, j] / math.sqrt(Q[i, i] * Q[j, j])
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@njit(cache=True)
|
|
90
|
+
def dcc_loglik_obs(z, Qbar, Nbar, a, b, g, nu, Q0):
|
|
91
|
+
"""Per-observation log density of standardised residuals under (A)DCC.
|
|
92
|
+
|
|
93
|
+
Q_t = (1-a-b) Qbar - g Nbar + a z z' + g n n' + b Q_{t-1}, Q_0 = Qbar,
|
|
94
|
+
with n = z * 1{z<0}. g = 0 gives plain DCC; Q0 is the Q used for the
|
|
95
|
+
first observation (normally Qbar).
|
|
96
|
+
"""
|
|
97
|
+
T, n = z.shape
|
|
98
|
+
out = np.empty(T)
|
|
99
|
+
C = (1.0 - a - b) * Qbar - g * Nbar
|
|
100
|
+
Q = Q0.copy()
|
|
101
|
+
R = np.empty((n, n))
|
|
102
|
+
for t in range(T):
|
|
103
|
+
if t > 0:
|
|
104
|
+
zp = z[t - 1]
|
|
105
|
+
for i in range(n):
|
|
106
|
+
ni = zp[i] if zp[i] < 0.0 else 0.0
|
|
107
|
+
for j in range(i, n):
|
|
108
|
+
nj = zp[j] if zp[j] < 0.0 else 0.0
|
|
109
|
+
v = C[i, j] + a * zp[i] * zp[j] + g * ni * nj + b * Q[i, j]
|
|
110
|
+
Q[i, j] = v
|
|
111
|
+
Q[j, i] = v
|
|
112
|
+
_q_to_r(Q, R)
|
|
113
|
+
out[t] = _mv_logpdf(z[t], R, nu)
|
|
114
|
+
return out
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@njit(cache=True)
|
|
118
|
+
def dcc_filter(z, Qbar, Nbar, a, b, g, Q0):
|
|
119
|
+
"""Q_t and R_t for t = 0..T (index T is the one-step-ahead forecast)."""
|
|
120
|
+
T, n = z.shape
|
|
121
|
+
Qs = np.empty((T + 1, n, n))
|
|
122
|
+
Rs = np.empty((T + 1, n, n))
|
|
123
|
+
C = (1.0 - a - b) * Qbar - g * Nbar
|
|
124
|
+
Q = Q0.copy()
|
|
125
|
+
for t in range(T + 1):
|
|
126
|
+
if t > 0:
|
|
127
|
+
zp = z[t - 1]
|
|
128
|
+
for i in range(n):
|
|
129
|
+
ni = zp[i] if zp[i] < 0.0 else 0.0
|
|
130
|
+
for j in range(i, n):
|
|
131
|
+
nj = zp[j] if zp[j] < 0.0 else 0.0
|
|
132
|
+
v = C[i, j] + a * zp[i] * zp[j] + g * ni * nj + b * Q[i, j]
|
|
133
|
+
Q[i, j] = v
|
|
134
|
+
Q[j, i] = v
|
|
135
|
+
Qs[t] = Q
|
|
136
|
+
_q_to_r(Q, Rs[t])
|
|
137
|
+
return Qs, Rs
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@njit(cache=True)
|
|
141
|
+
def dcc_composite_loglik(z, Qbar, Nbar, pairs, a, b, g, nu, Q0):
|
|
142
|
+
"""Composite likelihood (Engle, Shephard & Sheppard 2008): the sum of
|
|
143
|
+
bivariate DCC log-likelihoods over the given pairs. O(T * #pairs) with
|
|
144
|
+
scalar 2x2 algebra, so it scales to hundreds of assets."""
|
|
145
|
+
T = z.shape[0]
|
|
146
|
+
total = 0.0
|
|
147
|
+
if nu > 0.0:
|
|
148
|
+
c = (
|
|
149
|
+
math.lgamma(0.5 * (nu + 2.0))
|
|
150
|
+
- math.lgamma(0.5 * nu)
|
|
151
|
+
- math.log(math.pi * (nu - 2.0))
|
|
152
|
+
)
|
|
153
|
+
else:
|
|
154
|
+
c = -LOG_2PI
|
|
155
|
+
for p in range(pairs.shape[0]):
|
|
156
|
+
i = pairs[p, 0]
|
|
157
|
+
j = pairs[p, 1]
|
|
158
|
+
cii = (1.0 - a - b) * Qbar[i, i] - g * Nbar[i, i]
|
|
159
|
+
cjj = (1.0 - a - b) * Qbar[j, j] - g * Nbar[j, j]
|
|
160
|
+
cij = (1.0 - a - b) * Qbar[i, j] - g * Nbar[i, j]
|
|
161
|
+
qii = Q0[i, i]
|
|
162
|
+
qjj = Q0[j, j]
|
|
163
|
+
qij = Q0[i, j]
|
|
164
|
+
for t in range(T):
|
|
165
|
+
if t > 0:
|
|
166
|
+
zi = z[t - 1, i]
|
|
167
|
+
zj = z[t - 1, j]
|
|
168
|
+
ni = zi if zi < 0.0 else 0.0
|
|
169
|
+
nj = zj if zj < 0.0 else 0.0
|
|
170
|
+
qii = cii + a * zi * zi + g * ni * ni + b * qii
|
|
171
|
+
qjj = cjj + a * zj * zj + g * nj * nj + b * qjj
|
|
172
|
+
qij = cij + a * zi * zj + g * ni * nj + b * qij
|
|
173
|
+
rho = qij / math.sqrt(qii * qjj)
|
|
174
|
+
det = 1.0 - rho * rho
|
|
175
|
+
if det <= 0.0:
|
|
176
|
+
return -np.inf
|
|
177
|
+
x = z[t, i]
|
|
178
|
+
y = z[t, j]
|
|
179
|
+
quad = (x * x - 2.0 * rho * x * y + y * y) / det
|
|
180
|
+
if nu > 0.0:
|
|
181
|
+
total += c - 0.5 * math.log(det) - 0.5 * (nu + 2.0) * math.log(
|
|
182
|
+
1.0 + quad / (nu - 2.0)
|
|
183
|
+
)
|
|
184
|
+
else:
|
|
185
|
+
total += c - 0.5 * math.log(det) - 0.5 * quad
|
|
186
|
+
return total
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# ---------------------------------------------------------------- simulation
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@njit(cache=True)
|
|
193
|
+
def simulate_path(mu, omega, alpha, gamma, beta, Qbar, Nbar, a, b, g, shocks, burn,
|
|
194
|
+
Q0, s2_0):
|
|
195
|
+
"""Simulate returns from GJR-GARCH margins + (A)DCC correlation.
|
|
196
|
+
|
|
197
|
+
`shocks` are iid unit-variance draws (T+burn, n) with identity
|
|
198
|
+
correlation; they are rotated by chol(R_t) each step. Q0 and s2_0 are
|
|
199
|
+
the state for the first simulated period (Qbar and the unconditional
|
|
200
|
+
variances for an unconditional start, Q_{T+1} and sigma2_{T+1} to
|
|
201
|
+
simulate forward from the end of a sample).
|
|
202
|
+
"""
|
|
203
|
+
Ttot, n = shocks.shape
|
|
204
|
+
eps = np.empty((Ttot, n))
|
|
205
|
+
sig2 = np.empty((Ttot, n))
|
|
206
|
+
zs = np.empty((Ttot, n))
|
|
207
|
+
C = (1.0 - a - b) * Qbar - g * Nbar
|
|
208
|
+
Q = Q0.copy()
|
|
209
|
+
R = np.empty((n, n))
|
|
210
|
+
for t in range(Ttot):
|
|
211
|
+
if t > 0:
|
|
212
|
+
zp = zs[t - 1]
|
|
213
|
+
for i in range(n):
|
|
214
|
+
ni = zp[i] if zp[i] < 0.0 else 0.0
|
|
215
|
+
for j in range(i, n):
|
|
216
|
+
nj = zp[j] if zp[j] < 0.0 else 0.0
|
|
217
|
+
v = C[i, j] + a * zp[i] * zp[j] + g * ni * nj + b * Q[i, j]
|
|
218
|
+
Q[i, j] = v
|
|
219
|
+
Q[j, i] = v
|
|
220
|
+
ep = eps[t - 1]
|
|
221
|
+
for i in range(n):
|
|
222
|
+
e2 = ep[i] * ep[i]
|
|
223
|
+
s = omega[i] + alpha[i] * e2 + beta[i] * sig2[t - 1, i]
|
|
224
|
+
if ep[i] < 0.0:
|
|
225
|
+
s += gamma[i] * e2
|
|
226
|
+
sig2[t, i] = s
|
|
227
|
+
else:
|
|
228
|
+
sig2[0] = s2_0
|
|
229
|
+
_q_to_r(Q, R)
|
|
230
|
+
L = np.linalg.cholesky(R)
|
|
231
|
+
zt = L @ shocks[t]
|
|
232
|
+
zs[t] = zt
|
|
233
|
+
for i in range(n):
|
|
234
|
+
eps[t, i] = math.sqrt(sig2[t, i]) * zt[i]
|
|
235
|
+
return eps[burn:] + mu, sig2[burn:], zs[burn:]
|
multiarch/_numdiff.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Central finite differences with arch-style relative step sizes."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _steps(p):
|
|
7
|
+
return np.maximum(np.abs(p), 1e-4) * 1e-5
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def jacobian(f, p):
|
|
11
|
+
"""d f(p) / d p for vector-valued f -> (len(f), len(p))."""
|
|
12
|
+
p = np.asarray(p, dtype=float)
|
|
13
|
+
h = _steps(p)
|
|
14
|
+
cols = []
|
|
15
|
+
for k in range(p.size):
|
|
16
|
+
e = np.zeros_like(p)
|
|
17
|
+
e[k] = h[k]
|
|
18
|
+
cols.append((f(p + e) - f(p - e)) / (2 * h[k]))
|
|
19
|
+
return np.column_stack(cols)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def gradient(f, p):
|
|
23
|
+
return jacobian(lambda q: np.atleast_1d(f(q)), p)[0]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def hessian(f, p):
|
|
27
|
+
"""Hessian of scalar f by second-order central differences."""
|
|
28
|
+
p = np.asarray(p, dtype=float)
|
|
29
|
+
k = p.size
|
|
30
|
+
h = np.maximum(np.abs(p), 1e-4) * 1e-4
|
|
31
|
+
H = np.empty((k, k))
|
|
32
|
+
f0 = f(p)
|
|
33
|
+
for i in range(k):
|
|
34
|
+
ei = np.zeros(k)
|
|
35
|
+
ei[i] = h[i]
|
|
36
|
+
H[i, i] = (f(p + ei) - 2 * f0 + f(p - ei)) / h[i] ** 2
|
|
37
|
+
for j in range(i):
|
|
38
|
+
ej = np.zeros(k)
|
|
39
|
+
ej[j] = h[j]
|
|
40
|
+
H[i, j] = H[j, i] = (
|
|
41
|
+
f(p + ei + ej) - f(p + ei - ej) - f(p - ei + ej) + f(p - ei - ej)
|
|
42
|
+
) / (4 * h[i] * h[j])
|
|
43
|
+
return H
|