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 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