multiarch 0.2.0__tar.gz

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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Legard525
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,204 @@
1
+ Metadata-Version: 2.4
2
+ Name: multiarch
3
+ Version: 0.2.0
4
+ Summary: Fast multivariate GARCH for Python: CCC, DCC, aDCC, copula-DCC and GO-GARCH, validated against rmgarch
5
+ Author: Alex Nikolaenko
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Legard525/multiarch
8
+ Project-URL: Issues, https://github.com/Legard525/multiarch/issues
9
+ Keywords: garch,dcc,multivariate garch,volatility,correlation,copula,go-garch,value-at-risk,econometrics,finance
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Intended Audience :: Financial and Insurance Industry
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
15
+ Classifier: Topic :: Office/Business :: Financial :: Investment
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: numpy>=1.24
20
+ Requires-Dist: scipy>=1.10
21
+ Requires-Dist: pandas>=2.0
22
+ Requires-Dist: numba>=0.58
23
+ Provides-Extra: test
24
+ Requires-Dist: pytest; extra == "test"
25
+ Requires-Dist: arch; extra == "test"
26
+ Dynamic: license-file
27
+
28
+ # multiarch
29
+
30
+ [![tests](https://github.com/Legard525/multiarch/actions/workflows/tests.yml/badge.svg)](https://github.com/Legard525/multiarch/actions/workflows/tests.yml)
31
+
32
+ **Multivariate GARCH for Python.** The [`arch`](https://github.com/bashtage/arch) package
33
+ only fits univariate models, so Python has had no maintained multivariate GARCH library.
34
+ Courses and research therefore send Python users to R (`rmgarch`, `tsmarch`).
35
+ multiarch fills that gap:
36
+
37
+ - **Models:** CCC, DCC and asymmetric DCC, optionally as Gaussian or Student-t copulas, plus GO-GARCH.
38
+ - **Margins:** GARCH(1,1) or GJR-GARCH(1,1) with normal or Student-t errors.
39
+ - **Inference:** two-step standard errors that correct for the first estimation stage.
40
+ - **Large cross-sections:** a composite-likelihood estimator for many assets.
41
+ - **Outputs:** forecasts, simulation, portfolio VaR and ES, rolling backtests and specification tests.
42
+ - **Validation:** it reproduces `rmgarch`'s likelihood exactly. It runs 2–9× faster than rmgarch and 7–30× faster than tsmarch.
43
+
44
+ ```bash
45
+ pip install multiarch
46
+ ```
47
+
48
+ Requires Python ≥ 3.10, NumPy, SciPy, pandas and numba.
49
+
50
+ ## Quick start
51
+
52
+ ```python
53
+ import multiarch as ma
54
+
55
+ res = ma.DCC(returns).fit() # returns: (T, N) DataFrame, any units
56
+ print(res.summary())
57
+
58
+ res.conditional_correlation # (T, N, N) R_t
59
+ res.conditional_covariance # (T, N, N) H_t = D_t R_t D_t
60
+ res.correlation("JPM", "INTC") # one rho_t path as a Series
61
+ res.forecast(10) # mean, variance, correlation, covariance
62
+ res.value_at_risk([0.5, 0.5], level=0.01, value=1e6) # {'VaR': ..., 'ES': ...}
63
+ ```
64
+
65
+ [`examples/course_workflow.py`](https://github.com/Legard525/multiarch/blob/main/examples/course_workflow.py) walks through the standard
66
+ course sequence: EWMA → CCC → DCC → constant-correlation test → portfolio VaR → rolling VaR backtest.
67
+
68
+ ## What's in it
69
+
70
+ | | |
71
+ |---|---|
72
+ | `DCC(returns, model=...)` | `"dcc"` (Engle 2002), `"adcc"` (Cappiello, Engle & Sheppard 2006), `"ccc"` (Bollerslev 1990) |
73
+ | `vol=` | `"garch"` or `"gjr"` margins, each (1,1) |
74
+ | `mean=` | `"constant"` or `"zero"` |
75
+ | `dist=` | margin distribution, `"normal"` or `"t"` |
76
+ | `corr_dist=` | correlation-stage distribution, `"normal"` or multivariate `"t"` (defaults to `dist`) |
77
+ | `copula=` | `None`, `"normal"` or `"t"`: copula-DCC with parametric margins (as `rmgarch::cgarchfit`) |
78
+ | `estimator=` | `"full"` likelihood or `"composite"` pairwise likelihood (Engle, Shephard & Sheppard 2008) for large N |
79
+ | `convention=` | `"arch"` (default), `"rmgarch"` or `"tsmarch"`: start-up conventions that reproduce each package |
80
+ | `.fit(se=...)` | `"two-step"` (Engle & Sheppard 2001 corrected), `"naive"` or `None` |
81
+ | `.fix(params)` / `.apply(new_returns)` | evaluate fixed parameters, or filter new data with fitted ones |
82
+ | `.forecast(h)` | closed-form variances; Engle–Sheppard correlation forecast as in rmgarch |
83
+ | `.simulate(n, n_paths, start="end")` | Monte Carlo paths, including copula models |
84
+ | `.value_at_risk(w, level, horizon)` | analytic (normal or multivariate t) or simulated VaR and ES |
85
+ | `.value_at_risk_path(w, level)` | in-sample one-step VaR/ES path |
86
+ | `GOGARCH(returns)` | GO-GARCH (van der Weide 2002) with a built-in FastICA rotation |
87
+ | `rolling_forecast(returns, window, refit_every)` | rolling or expanding re-estimation with no look-ahead |
88
+ | `var_backtest(r, VaR, level)` | violation ratio, Kupiec and Christoffersen tests |
89
+ | `constant_correlation_test(returns)` | Engle & Sheppard (2001) test of CCC against DCC |
90
+ | `hosking_test(x, lags)` | multivariate portmanteau test |
91
+ | `ewma_covariance(returns, lam)` | RiskMetrics EWMA |
92
+ | `simulate_dcc(...)` | simulate from given parameters |
93
+
94
+ Estimation is scale-free: percent and fractional returns give the same parameters,
95
+ with ω and μ rescaled.
96
+
97
+ ## Validation
98
+
99
+ All numbers come from [`validation/`](https://github.com/Legard525/multiarch/tree/main/validation) and are reproducible: `Rscript rmgarch_fit.R .`
100
+ then `python compare.py`, and `Rscript tsmarch_fit.R .` then `python compare_tsmarch.py`.
101
+ The data are daily returns of DJI stocks from `rmgarch`'s `dji30ret` (T = 5,521).
102
+
103
+ ### Against rmgarch 1.3 (R 4.6)
104
+
105
+ `ll(ours @ rm)` is multiarch's log-likelihood evaluated at rmgarch's own estimates.
106
+ If both packages implement the same model, it equals rmgarch's reported value.
107
+
108
+ | case | ll (rmgarch) | ll (ours @ rm) | max \|ρ_t diff\| @ rm | ll (ours, own fit) | time rmgarch → ours |
109
+ |---|---|---|---|---|---|
110
+ | DCC, normal, N=5 | −53514.64292 | −53514.64292 | 0 | −53513.99601 | 2.8 s → 0.32 s |
111
+ | aDCC, normal | −53513.86843 | −53513.86843 | 0 | −53513.13231 | 3.4 s → 0.45 s |
112
+ | DCC, Student-t | −52235.91344 | −52235.91344 | 0 | −52237.53475 | 3.7 s → 0.60 s |
113
+ | DCC, GJR margins | −53481.97322 | −53481.97322 | 0 | −53481.59132 | 3.3 s → 0.39 s |
114
+ | DCC, zero mean | −53534.39786 | −53534.39786 | 0 | −53533.64643 | 2.1 s → 0.26 s |
115
+ | DCC, N=10 | −101267.48845 | −101267.48845 | 0 | −101266.80559 | 7.6 s → 1.2 s |
116
+ | DCC, raw returns, N=2 | 28163.98498 | 28163.98498 | 0 | 28163.97845 | 0.84 s → 0.09 s |
117
+ | t-copula DCC, t margins | −52079.16469 | −52079.16469 | 0 | −52080.47842 | 6.3 s → 3.1 s |
118
+ | Gaussian-copula DCC, t margins | −52196.46089 | −52196.46089 | 0 | −52197.96371 | 3.6 s → 1.1 s |
119
+
120
+ Timings for multiarch include the two-step standard errors.
121
+
122
+ - The likelihood functions are identical, and so are the correlation paths at the same parameters.
123
+ - The 10-step DCC covariance forecasts agree to 2e-14.
124
+ - Estimates differ only through the optimiser, and multiarch reaches higher margin likelihoods.
125
+ Because estimation is two-step, a better first stage can still leave the joint
126
+ log-likelihood slightly lower (the t and copula rows). That is a property of the two-step
127
+ estimator, not a different model.
128
+ - Standard errors for the DCC parameters *a* and *b* are within 5% of rmgarch's. For the shape ν and the aDCC asymmetry *g* they are 17–31% larger.
129
+ A 300-sample Monte Carlo (`mc_se.py`) shows that multiarch's two-step SEs are calibrated:
130
+ the median SE is 0.0059 against a sampling SD of 0.0059 for *a*, and 0.0124 against 0.0139 for *b*.
131
+ - **GO-GARCH:** given rmgarch's ICA rotation and factor parameters, the likelihood agrees to 0.06,
132
+ a difference that comes from how the constant mean is estimated. multiarch's own FastICA finds a rotation
133
+ with a likelihood 277 points higher on this data, in 0.04 s against 0.47 s.
134
+
135
+ ### Against tsmarch (the package used in *Financial Risk Forecasting* notes)
136
+
137
+ | case | ll (tsmarch) | ll (ours @ ts) | max \|ρ_t diff\| @ ts | time tsmarch → ours |
138
+ |---|---|---|---|---|
139
+ | DCC, N=2 | 28163.94124 | 28163.94306 | 4.5e-4 | 0.73 s → 0.04 s |
140
+ | DCC, N=5 | 73611.21223 | 73611.23406 | 4.2e-4 | 2.7 s → 0.10 s |
141
+ | aDCC, N=5 | 73612.01170 | 73612.03276 | 4.2e-4 | 3.8 s → 0.11 s |
142
+ | DCC, multivariate t | 74873.58661 | 74873.58785 | 3.8e-4 | 3.9 s → 0.13 s |
143
+
144
+ The margins match tsgarch exactly (σ_t to 1e-14). A residual gap of under 0.03 log-likelihood
145
+ points remains in tsmarch's compiled DCC recursion; it fades to 1e-7 by the end of the sample.
146
+
147
+ ### Notes on rmgarch's `DCCtest`
148
+
149
+ `rmgarch::DCCtest` departs from Engle & Sheppard (2001) in two ways. It calls
150
+ `qr.solve(regressand, regressors)`, which regresses the lags on the target instead of the
151
+ reverse, and it divides by √σ² instead of σ². `validation/compare.py` reproduces its statistic
152
+ exactly (65.34) from its own residuals. On the same residuals, the textbook statistic
153
+ returned by `constant_correlation_test` is 41.08.
154
+
155
+ ### Scaling
156
+
157
+ From `validation/bench.py` (T = 2,500, no SEs):
158
+
159
+ | N | full likelihood | composite likelihood |
160
+ |---|---|---|
161
+ | 5 | 0.05 s | 0.02 s |
162
+ | 25 | 0.34 s | 0.09 s |
163
+ | 50 | 0.35 s | 0.19 s |
164
+ | 100 | n/a | 0.41 s |
165
+
166
+ The full-likelihood DCC estimate of *b* is biased downward as N grows: 0.930 against a true 0.95
167
+ at N = 50. The composite estimator does not have this bias.
168
+
169
+ ## Conventions
170
+
171
+ The default (`"arch"`) follows the `arch` package for the margins (exponential backcast)
172
+ and uses Q̄ = Z′Z/T with Q₁ = Q̄. `convention="rmgarch"` reproduces rmgarch's start-up:
173
+ - σ²₁ is the sample mean of ε².
174
+ - Q̄ = cov(Z).
175
+ - The first Q step uses a pre-sample shock of ones in DCC and of zeros in copula-DCC.
176
+ - Copula uniforms are clipped at 0.99999.
177
+
178
+ `convention="tsmarch"` reproduces tsgarch's σ²₁ = ω + persistence · mean(ε²) and uses Q₁ = Q̄ = cov(Z).
179
+ These choices only affect the first few dozen observations.
180
+
181
+ ## Limitations
182
+
183
+ - Margins are GARCH(1,1) or GJR(1,1) with a constant or zero mean. There are no ARMA means or higher orders.
184
+ - DCC is of order (1,1).
185
+ - Estimation is two-step. The two-step SEs treat Q̄ and N̄ as known, as rmgarch does.
186
+ - Composite-likelihood SEs are Hessian-only.
187
+ - GO-GARCH SEs ignore estimation error in the rotation.
188
+ - Copula models use parametric margins only. Empirical and semi-parametric margins are not implemented.
189
+ - BEKK and DCC-MIDAS are not implemented.
190
+
191
+ ## References
192
+
193
+ - Bollerslev (1990), *Modelling the coherence in short-run nominal exchange rates*, REStat.
194
+ - Engle (2002), *Dynamic conditional correlation*, JBES.
195
+ - Engle & Sheppard (2001), *Theoretical and empirical properties of dynamic conditional correlation multivariate GARCH*, NBER WP 8554.
196
+ - Cappiello, Engle & Sheppard (2006), *Asymmetric dynamics in the correlations of global equity and bond returns*, J. Fin. Econometrics.
197
+ - Engle, Shephard & Sheppard (2008), *Fitting vast dimensional time-varying covariance models*, working paper.
198
+ - van der Weide (2002), *GO-GARCH: a multivariate generalized orthogonal GARCH model*, J. Applied Econometrics.
199
+ - Patton (2006), *Modelling asymmetric exchange rate dependence*, IER.
200
+ - Kupiec (1995); Christoffersen (1998); Hosking (1980).
201
+
202
+ ## License
203
+
204
+ MIT
@@ -0,0 +1,177 @@
1
+ # multiarch
2
+
3
+ [![tests](https://github.com/Legard525/multiarch/actions/workflows/tests.yml/badge.svg)](https://github.com/Legard525/multiarch/actions/workflows/tests.yml)
4
+
5
+ **Multivariate GARCH for Python.** The [`arch`](https://github.com/bashtage/arch) package
6
+ only fits univariate models, so Python has had no maintained multivariate GARCH library.
7
+ Courses and research therefore send Python users to R (`rmgarch`, `tsmarch`).
8
+ multiarch fills that gap:
9
+
10
+ - **Models:** CCC, DCC and asymmetric DCC, optionally as Gaussian or Student-t copulas, plus GO-GARCH.
11
+ - **Margins:** GARCH(1,1) or GJR-GARCH(1,1) with normal or Student-t errors.
12
+ - **Inference:** two-step standard errors that correct for the first estimation stage.
13
+ - **Large cross-sections:** a composite-likelihood estimator for many assets.
14
+ - **Outputs:** forecasts, simulation, portfolio VaR and ES, rolling backtests and specification tests.
15
+ - **Validation:** it reproduces `rmgarch`'s likelihood exactly. It runs 2–9× faster than rmgarch and 7–30× faster than tsmarch.
16
+
17
+ ```bash
18
+ pip install multiarch
19
+ ```
20
+
21
+ Requires Python ≥ 3.10, NumPy, SciPy, pandas and numba.
22
+
23
+ ## Quick start
24
+
25
+ ```python
26
+ import multiarch as ma
27
+
28
+ res = ma.DCC(returns).fit() # returns: (T, N) DataFrame, any units
29
+ print(res.summary())
30
+
31
+ res.conditional_correlation # (T, N, N) R_t
32
+ res.conditional_covariance # (T, N, N) H_t = D_t R_t D_t
33
+ res.correlation("JPM", "INTC") # one rho_t path as a Series
34
+ res.forecast(10) # mean, variance, correlation, covariance
35
+ res.value_at_risk([0.5, 0.5], level=0.01, value=1e6) # {'VaR': ..., 'ES': ...}
36
+ ```
37
+
38
+ [`examples/course_workflow.py`](https://github.com/Legard525/multiarch/blob/main/examples/course_workflow.py) walks through the standard
39
+ course sequence: EWMA → CCC → DCC → constant-correlation test → portfolio VaR → rolling VaR backtest.
40
+
41
+ ## What's in it
42
+
43
+ | | |
44
+ |---|---|
45
+ | `DCC(returns, model=...)` | `"dcc"` (Engle 2002), `"adcc"` (Cappiello, Engle & Sheppard 2006), `"ccc"` (Bollerslev 1990) |
46
+ | `vol=` | `"garch"` or `"gjr"` margins, each (1,1) |
47
+ | `mean=` | `"constant"` or `"zero"` |
48
+ | `dist=` | margin distribution, `"normal"` or `"t"` |
49
+ | `corr_dist=` | correlation-stage distribution, `"normal"` or multivariate `"t"` (defaults to `dist`) |
50
+ | `copula=` | `None`, `"normal"` or `"t"`: copula-DCC with parametric margins (as `rmgarch::cgarchfit`) |
51
+ | `estimator=` | `"full"` likelihood or `"composite"` pairwise likelihood (Engle, Shephard & Sheppard 2008) for large N |
52
+ | `convention=` | `"arch"` (default), `"rmgarch"` or `"tsmarch"`: start-up conventions that reproduce each package |
53
+ | `.fit(se=...)` | `"two-step"` (Engle & Sheppard 2001 corrected), `"naive"` or `None` |
54
+ | `.fix(params)` / `.apply(new_returns)` | evaluate fixed parameters, or filter new data with fitted ones |
55
+ | `.forecast(h)` | closed-form variances; Engle–Sheppard correlation forecast as in rmgarch |
56
+ | `.simulate(n, n_paths, start="end")` | Monte Carlo paths, including copula models |
57
+ | `.value_at_risk(w, level, horizon)` | analytic (normal or multivariate t) or simulated VaR and ES |
58
+ | `.value_at_risk_path(w, level)` | in-sample one-step VaR/ES path |
59
+ | `GOGARCH(returns)` | GO-GARCH (van der Weide 2002) with a built-in FastICA rotation |
60
+ | `rolling_forecast(returns, window, refit_every)` | rolling or expanding re-estimation with no look-ahead |
61
+ | `var_backtest(r, VaR, level)` | violation ratio, Kupiec and Christoffersen tests |
62
+ | `constant_correlation_test(returns)` | Engle & Sheppard (2001) test of CCC against DCC |
63
+ | `hosking_test(x, lags)` | multivariate portmanteau test |
64
+ | `ewma_covariance(returns, lam)` | RiskMetrics EWMA |
65
+ | `simulate_dcc(...)` | simulate from given parameters |
66
+
67
+ Estimation is scale-free: percent and fractional returns give the same parameters,
68
+ with ω and μ rescaled.
69
+
70
+ ## Validation
71
+
72
+ All numbers come from [`validation/`](https://github.com/Legard525/multiarch/tree/main/validation) and are reproducible: `Rscript rmgarch_fit.R .`
73
+ then `python compare.py`, and `Rscript tsmarch_fit.R .` then `python compare_tsmarch.py`.
74
+ The data are daily returns of DJI stocks from `rmgarch`'s `dji30ret` (T = 5,521).
75
+
76
+ ### Against rmgarch 1.3 (R 4.6)
77
+
78
+ `ll(ours @ rm)` is multiarch's log-likelihood evaluated at rmgarch's own estimates.
79
+ If both packages implement the same model, it equals rmgarch's reported value.
80
+
81
+ | case | ll (rmgarch) | ll (ours @ rm) | max \|ρ_t diff\| @ rm | ll (ours, own fit) | time rmgarch → ours |
82
+ |---|---|---|---|---|---|
83
+ | DCC, normal, N=5 | −53514.64292 | −53514.64292 | 0 | −53513.99601 | 2.8 s → 0.32 s |
84
+ | aDCC, normal | −53513.86843 | −53513.86843 | 0 | −53513.13231 | 3.4 s → 0.45 s |
85
+ | DCC, Student-t | −52235.91344 | −52235.91344 | 0 | −52237.53475 | 3.7 s → 0.60 s |
86
+ | DCC, GJR margins | −53481.97322 | −53481.97322 | 0 | −53481.59132 | 3.3 s → 0.39 s |
87
+ | DCC, zero mean | −53534.39786 | −53534.39786 | 0 | −53533.64643 | 2.1 s → 0.26 s |
88
+ | DCC, N=10 | −101267.48845 | −101267.48845 | 0 | −101266.80559 | 7.6 s → 1.2 s |
89
+ | DCC, raw returns, N=2 | 28163.98498 | 28163.98498 | 0 | 28163.97845 | 0.84 s → 0.09 s |
90
+ | t-copula DCC, t margins | −52079.16469 | −52079.16469 | 0 | −52080.47842 | 6.3 s → 3.1 s |
91
+ | Gaussian-copula DCC, t margins | −52196.46089 | −52196.46089 | 0 | −52197.96371 | 3.6 s → 1.1 s |
92
+
93
+ Timings for multiarch include the two-step standard errors.
94
+
95
+ - The likelihood functions are identical, and so are the correlation paths at the same parameters.
96
+ - The 10-step DCC covariance forecasts agree to 2e-14.
97
+ - Estimates differ only through the optimiser, and multiarch reaches higher margin likelihoods.
98
+ Because estimation is two-step, a better first stage can still leave the joint
99
+ log-likelihood slightly lower (the t and copula rows). That is a property of the two-step
100
+ estimator, not a different model.
101
+ - Standard errors for the DCC parameters *a* and *b* are within 5% of rmgarch's. For the shape ν and the aDCC asymmetry *g* they are 17–31% larger.
102
+ A 300-sample Monte Carlo (`mc_se.py`) shows that multiarch's two-step SEs are calibrated:
103
+ the median SE is 0.0059 against a sampling SD of 0.0059 for *a*, and 0.0124 against 0.0139 for *b*.
104
+ - **GO-GARCH:** given rmgarch's ICA rotation and factor parameters, the likelihood agrees to 0.06,
105
+ a difference that comes from how the constant mean is estimated. multiarch's own FastICA finds a rotation
106
+ with a likelihood 277 points higher on this data, in 0.04 s against 0.47 s.
107
+
108
+ ### Against tsmarch (the package used in *Financial Risk Forecasting* notes)
109
+
110
+ | case | ll (tsmarch) | ll (ours @ ts) | max \|ρ_t diff\| @ ts | time tsmarch → ours |
111
+ |---|---|---|---|---|
112
+ | DCC, N=2 | 28163.94124 | 28163.94306 | 4.5e-4 | 0.73 s → 0.04 s |
113
+ | DCC, N=5 | 73611.21223 | 73611.23406 | 4.2e-4 | 2.7 s → 0.10 s |
114
+ | aDCC, N=5 | 73612.01170 | 73612.03276 | 4.2e-4 | 3.8 s → 0.11 s |
115
+ | DCC, multivariate t | 74873.58661 | 74873.58785 | 3.8e-4 | 3.9 s → 0.13 s |
116
+
117
+ The margins match tsgarch exactly (σ_t to 1e-14). A residual gap of under 0.03 log-likelihood
118
+ points remains in tsmarch's compiled DCC recursion; it fades to 1e-7 by the end of the sample.
119
+
120
+ ### Notes on rmgarch's `DCCtest`
121
+
122
+ `rmgarch::DCCtest` departs from Engle & Sheppard (2001) in two ways. It calls
123
+ `qr.solve(regressand, regressors)`, which regresses the lags on the target instead of the
124
+ reverse, and it divides by √σ² instead of σ². `validation/compare.py` reproduces its statistic
125
+ exactly (65.34) from its own residuals. On the same residuals, the textbook statistic
126
+ returned by `constant_correlation_test` is 41.08.
127
+
128
+ ### Scaling
129
+
130
+ From `validation/bench.py` (T = 2,500, no SEs):
131
+
132
+ | N | full likelihood | composite likelihood |
133
+ |---|---|---|
134
+ | 5 | 0.05 s | 0.02 s |
135
+ | 25 | 0.34 s | 0.09 s |
136
+ | 50 | 0.35 s | 0.19 s |
137
+ | 100 | n/a | 0.41 s |
138
+
139
+ The full-likelihood DCC estimate of *b* is biased downward as N grows: 0.930 against a true 0.95
140
+ at N = 50. The composite estimator does not have this bias.
141
+
142
+ ## Conventions
143
+
144
+ The default (`"arch"`) follows the `arch` package for the margins (exponential backcast)
145
+ and uses Q̄ = Z′Z/T with Q₁ = Q̄. `convention="rmgarch"` reproduces rmgarch's start-up:
146
+ - σ²₁ is the sample mean of ε².
147
+ - Q̄ = cov(Z).
148
+ - The first Q step uses a pre-sample shock of ones in DCC and of zeros in copula-DCC.
149
+ - Copula uniforms are clipped at 0.99999.
150
+
151
+ `convention="tsmarch"` reproduces tsgarch's σ²₁ = ω + persistence · mean(ε²) and uses Q₁ = Q̄ = cov(Z).
152
+ These choices only affect the first few dozen observations.
153
+
154
+ ## Limitations
155
+
156
+ - Margins are GARCH(1,1) or GJR(1,1) with a constant or zero mean. There are no ARMA means or higher orders.
157
+ - DCC is of order (1,1).
158
+ - Estimation is two-step. The two-step SEs treat Q̄ and N̄ as known, as rmgarch does.
159
+ - Composite-likelihood SEs are Hessian-only.
160
+ - GO-GARCH SEs ignore estimation error in the rotation.
161
+ - Copula models use parametric margins only. Empirical and semi-parametric margins are not implemented.
162
+ - BEKK and DCC-MIDAS are not implemented.
163
+
164
+ ## References
165
+
166
+ - Bollerslev (1990), *Modelling the coherence in short-run nominal exchange rates*, REStat.
167
+ - Engle (2002), *Dynamic conditional correlation*, JBES.
168
+ - Engle & Sheppard (2001), *Theoretical and empirical properties of dynamic conditional correlation multivariate GARCH*, NBER WP 8554.
169
+ - Cappiello, Engle & Sheppard (2006), *Asymmetric dynamics in the correlations of global equity and bond returns*, J. Fin. Econometrics.
170
+ - Engle, Shephard & Sheppard (2008), *Fitting vast dimensional time-varying covariance models*, working paper.
171
+ - van der Weide (2002), *GO-GARCH: a multivariate generalized orthogonal GARCH model*, J. Applied Econometrics.
172
+ - Patton (2006), *Modelling asymmetric exchange rate dependence*, IER.
173
+ - Kupiec (1995); Christoffersen (1998); Hosking (1980).
174
+
175
+ ## License
176
+
177
+ MIT
@@ -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"
@@ -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:]