pyconnectedness 0.1.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.
- pyconnectedness/__init__.py +35 -0
- pyconnectedness/causality/__init__.py +0 -0
- pyconnectedness/connectedness/__init__.py +23 -0
- pyconnectedness/connectedness/decomposition.py +135 -0
- pyconnectedness/connectedness/dynamic.py +153 -0
- pyconnectedness/connectedness/frequency.py +185 -0
- pyconnectedness/connectedness/static.py +163 -0
- pyconnectedness/connectedness/var.py +156 -0
- pyconnectedness/viz/__init__.py +11 -0
- pyconnectedness/viz/heatmap.py +118 -0
- pyconnectedness/viz/network.py +160 -0
- pyconnectedness-0.1.0.dist-info/METADATA +307 -0
- pyconnectedness-0.1.0.dist-info/RECORD +15 -0
- pyconnectedness-0.1.0.dist-info/WHEEL +4 -0
- pyconnectedness-0.1.0.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Connectedness measures
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from importlib.metadata import version
|
|
8
|
+
|
|
9
|
+
__version__ = version("pyconnectedness")
|
|
10
|
+
|
|
11
|
+
from .connectedness import (
|
|
12
|
+
ConnectednessResult,
|
|
13
|
+
DynamicConnectednessResult,
|
|
14
|
+
FrequencyConnectednessResult,
|
|
15
|
+
VARFit,
|
|
16
|
+
dynamic_connectedness,
|
|
17
|
+
fit_var,
|
|
18
|
+
frequency_connectedness,
|
|
19
|
+
generalized_fevd,
|
|
20
|
+
normalize_fevd,
|
|
21
|
+
orthogonalized_fevd,
|
|
22
|
+
static_connectedness,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = ["fit_var", "VARFit",
|
|
26
|
+
"generalized_fevd", "normalize_fevd", "orthogonalized_fevd",
|
|
27
|
+
"ConnectednessResult", "static_connectedness",
|
|
28
|
+
"DynamicConnectednessResult", "dynamic_connectedness",
|
|
29
|
+
"FrequencyConnectednessResult", "frequency_connectedness",
|
|
30
|
+
"__version__",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
File without changes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .decomposition import generalized_fevd, normalize_fevd, orthogonalized_fevd
|
|
4
|
+
from .dynamic import DynamicConnectednessResult, dynamic_connectedness
|
|
5
|
+
from .frequency import FrequencyConnectednessResult, frequency_connectedness
|
|
6
|
+
from .static import ConnectednessResult, static_connectedness
|
|
7
|
+
from .var import VARFit, fit_var
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"fit_var",
|
|
11
|
+
"VARFit",
|
|
12
|
+
"static_connectedness",
|
|
13
|
+
"ConnectednessResult",
|
|
14
|
+
"generalized_fevd",
|
|
15
|
+
"orthogonalized_fevd",
|
|
16
|
+
"normalize_fevd",
|
|
17
|
+
"dynamic_connectedness",
|
|
18
|
+
"DynamicConnectednessResult",
|
|
19
|
+
"frequency_connectedness",
|
|
20
|
+
"FrequencyConnectednessResult",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
r"""
|
|
2
|
+
Forecast error variance decompositions for connectedness analysis.
|
|
3
|
+
|
|
4
|
+
Two schemes are provided:
|
|
5
|
+
|
|
6
|
+
* :func:`generalized_fevd` — the generalized decomposition of Pesaran and
|
|
7
|
+
Shin (1998), invariant to variable ordering (Diebold-Yilmaz 2012/2014).
|
|
8
|
+
* :func:`orthogonalized_fevd` — the Cholesky decomposition of Diebold and
|
|
9
|
+
Yilmaz (2009), which depends on variable ordering.
|
|
10
|
+
|
|
11
|
+
References
|
|
12
|
+
----------
|
|
13
|
+
Pesaran and Shin (1998) Generalized impulse response analysis in linear
|
|
14
|
+
multivariate models. Economics Letters, 58, 17-29.
|
|
15
|
+
|
|
16
|
+
Diebold and Yilmaz (2009) Measuring financial asset return and volatility
|
|
17
|
+
spillovers, with application to global equity markets. The Economic Journal,
|
|
18
|
+
119, 158-171.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def generalized_fevd(ma_coefficients: np.ndarray, sigma: np.ndarray) -> np.ndarray:
|
|
27
|
+
r"""
|
|
28
|
+
Generalized forecast error variance decomposition (Pesaran-Shin 1998).
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
ma_coefficients : ndarray (H x k x k)
|
|
33
|
+
Moving-average matrices :math:`\Phi_0, \ldots, \Phi_{H-1}`.
|
|
34
|
+
sigma : ndarray (k x k)
|
|
35
|
+
Residual covariance matrix :math:`\Sigma_u`.
|
|
36
|
+
|
|
37
|
+
Notes
|
|
38
|
+
-----
|
|
39
|
+
The share of the H-step forecast error variance of variable i due to
|
|
40
|
+
shocks in variable j is
|
|
41
|
+
|
|
42
|
+
.. math::
|
|
43
|
+
|
|
44
|
+
\theta_{ij}(H) = \frac{\sigma_{jj}^{-1}
|
|
45
|
+
\sum_{h=0}^{H-1} (e_i' \Phi_h \Sigma e_j)^2}
|
|
46
|
+
{\sum_{h=0}^{H-1} e_i' \Phi_h \Sigma \Phi_h' e_i}
|
|
47
|
+
|
|
48
|
+
where :math:`e_i` is a selection vector and :math:`\sigma_{jj}` the j-th
|
|
49
|
+
diagonal element of :math:`\Sigma`. Rows do not sum to one under this
|
|
50
|
+
scheme; apply :func:`normalize_fevd`.
|
|
51
|
+
|
|
52
|
+
Returns
|
|
53
|
+
-------
|
|
54
|
+
ndarray (k x k)
|
|
55
|
+
Matrix theta with theta[i, j] the contribution of j to i.
|
|
56
|
+
"""
|
|
57
|
+
ma = np.asarray(ma_coefficients)
|
|
58
|
+
sigma = np.asarray(sigma)
|
|
59
|
+
H, k, _ = ma.shape
|
|
60
|
+
|
|
61
|
+
sigma_jj = np.diag(sigma)
|
|
62
|
+
numerator = np.zeros((k, k))
|
|
63
|
+
denominator = np.zeros(k)
|
|
64
|
+
for h in range(H):
|
|
65
|
+
phi = ma[h]
|
|
66
|
+
phi_sigma = phi @ sigma # entry [i, j] = e_i' Phi_h Sigma e_j
|
|
67
|
+
numerator += phi_sigma ** 2
|
|
68
|
+
denominator += np.diag(phi_sigma @ phi.T)
|
|
69
|
+
|
|
70
|
+
return (numerator / sigma_jj[np.newaxis, :]) / denominator[:, np.newaxis]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def orthogonalized_fevd(ma_coefficients: np.ndarray, sigma: np.ndarray) -> np.ndarray:
|
|
74
|
+
r"""
|
|
75
|
+
Orthogonalized (Cholesky) forecast error variance decomposition (DY 2009).
|
|
76
|
+
|
|
77
|
+
Parameters
|
|
78
|
+
----------
|
|
79
|
+
ma_coefficients : ndarray (H x k x k)
|
|
80
|
+
Moving-average matrices :math:`\Phi_0, \ldots, \Phi_{H-1}`.
|
|
81
|
+
sigma : ndarray (k x k)
|
|
82
|
+
Residual covariance matrix :math:`\Sigma_u`.
|
|
83
|
+
|
|
84
|
+
Notes
|
|
85
|
+
-----
|
|
86
|
+
Uses the Cholesky factor :math:`P` with :math:`\Sigma = P P'`:
|
|
87
|
+
|
|
88
|
+
.. math::
|
|
89
|
+
|
|
90
|
+
\theta_{ij}(H) = \frac{\sum_{h=0}^{H-1} (e_i' \Phi_h P e_j)^2}
|
|
91
|
+
{\sum_{h=0}^{H-1} e_i' \Phi_h \Sigma \Phi_h' e_i}
|
|
92
|
+
|
|
93
|
+
Rows sum to one by construction. The result depends on the column order
|
|
94
|
+
of the data — the known ordering sensitivity of the DY-2009 method.
|
|
95
|
+
|
|
96
|
+
Returns
|
|
97
|
+
-------
|
|
98
|
+
ndarray (k x k)
|
|
99
|
+
Matrix theta with theta[i, j] the contribution of j to i.
|
|
100
|
+
"""
|
|
101
|
+
ma = np.asarray(ma_coefficients)
|
|
102
|
+
sigma = np.asarray(sigma)
|
|
103
|
+
H, k, _ = ma.shape
|
|
104
|
+
|
|
105
|
+
P = np.linalg.cholesky(sigma) # Sigma = P P', lower triangular
|
|
106
|
+
numerator = np.zeros((k, k))
|
|
107
|
+
denominator = np.zeros(k)
|
|
108
|
+
for h in range(H):
|
|
109
|
+
phi = ma[h]
|
|
110
|
+
phi_P = phi @ P
|
|
111
|
+
numerator += phi_P ** 2
|
|
112
|
+
denominator += np.diag(phi @ sigma @ phi.T)
|
|
113
|
+
|
|
114
|
+
return numerator / denominator[:, np.newaxis]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def normalize_fevd(theta: np.ndarray) -> np.ndarray:
|
|
118
|
+
"""
|
|
119
|
+
Normalize a decomposition row-wise so each row sums to one.
|
|
120
|
+
|
|
121
|
+
Parameters
|
|
122
|
+
----------
|
|
123
|
+
theta : ndarray (k x k)
|
|
124
|
+
|
|
125
|
+
Notes
|
|
126
|
+
-----
|
|
127
|
+
Required after :func:`generalized_fevd`. A no-op after
|
|
128
|
+
:func:`orthogonalized_fevd`, whose rows already sum to one.
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
ndarray (k x k)
|
|
133
|
+
"""
|
|
134
|
+
theta = np.asarray(theta)
|
|
135
|
+
return theta / theta.sum(axis=1, keepdims=True)
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rolling-window (dynamic) connectedness measures
|
|
3
|
+
|
|
4
|
+
References
|
|
5
|
+
----------
|
|
6
|
+
Diebold and Yilmaz (2009) Measuring financial asset return and volatility
|
|
7
|
+
spillovers, with application to global equity markets. The Economic Journal,
|
|
8
|
+
119, 158-171.
|
|
9
|
+
Diebold and Yilmaz (2012) Better to give than to receive: predictive
|
|
10
|
+
directional measurement of volatility spillovers. International Journal of
|
|
11
|
+
Forecasting, 28, 57-66.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
import pandas as pd
|
|
18
|
+
|
|
19
|
+
from .static import static_connectedness
|
|
20
|
+
from .var import fit_var
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class DynamicConnectednessResult:
|
|
25
|
+
"""
|
|
26
|
+
Rolling-window connectedness measures.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
total : Series (T,)
|
|
31
|
+
Total connectedness (spillover) index per window, in percent.
|
|
32
|
+
directional_to : DataFrame (T x k)
|
|
33
|
+
Contribution of each variable to the forecast error variance of all
|
|
34
|
+
others, off-diagonal column sums.
|
|
35
|
+
directional_from : DataFrame (T x k)
|
|
36
|
+
Contribution received by each variable from all others, off-diagonal
|
|
37
|
+
row sums
|
|
38
|
+
net : DataFrame (T x k)
|
|
39
|
+
TO minus FROM. Sums to zero across variables in every window.
|
|
40
|
+
fevd : ndarray (T x k x k)
|
|
41
|
+
pairwise_net: ndarray (T x k x k)
|
|
42
|
+
names : list of str
|
|
43
|
+
window, horizon, lag_order : int
|
|
44
|
+
method : str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
total: pd.Series
|
|
50
|
+
directional_to: pd.DataFrame
|
|
51
|
+
directional_from: pd.DataFrame
|
|
52
|
+
net: pd.DataFrame
|
|
53
|
+
fevd: np.ndarray
|
|
54
|
+
pairwise_net: np.ndarray
|
|
55
|
+
names: list
|
|
56
|
+
window: int
|
|
57
|
+
horizon: int
|
|
58
|
+
lag_order: int
|
|
59
|
+
method: str = "generalized"
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def k(self):
|
|
63
|
+
return len(self.names)
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def n_windows(self):
|
|
67
|
+
return len(self.total)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def dynamic_connectedness(
|
|
71
|
+
data, window, horizon=10, *, method="generalized", lags=None, **fit_kwargs
|
|
72
|
+
):
|
|
73
|
+
r"""
|
|
74
|
+
Connectedness measures over a rolling estimation window
|
|
75
|
+
|
|
76
|
+
Parameters
|
|
77
|
+
----------
|
|
78
|
+
data : DataFrame (n x k)
|
|
79
|
+
Observations in rows, variables in columns
|
|
80
|
+
window : int
|
|
81
|
+
Number of observations per estimation window
|
|
82
|
+
horizon : int
|
|
83
|
+
Forecast horizon H of the variance decomposition.
|
|
84
|
+
method : {"generalized", "orthogonalized"}
|
|
85
|
+
Decomposition applied in every window, passed on to
|
|
86
|
+
static_connectedness. DY-2009 is Cholesky, DY-2012 onward generalized.
|
|
87
|
+
lags : int or None
|
|
88
|
+
VAR order, held fixed across windows. If None it is selected once on
|
|
89
|
+
the full sample and then frozen; see Notes.
|
|
90
|
+
**fit_kwargs
|
|
91
|
+
Further arguments for fit_var, e.g. ``ic="aic"``, ``trend="c"``.
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
Returns
|
|
96
|
+
-------
|
|
97
|
+
DynamicConnectednessResult
|
|
98
|
+
|
|
99
|
+
Raises
|
|
100
|
+
------
|
|
101
|
+
TypeError
|
|
102
|
+
If data is not a DataFrame.
|
|
103
|
+
ValueError
|
|
104
|
+
If the window is longer than the sample.
|
|
105
|
+
"""
|
|
106
|
+
if not isinstance(data, pd.DataFrame):
|
|
107
|
+
raise TypeError("data must be a DataFrame")
|
|
108
|
+
|
|
109
|
+
nobs, k = data.shape
|
|
110
|
+
if window > nobs:
|
|
111
|
+
raise ValueError(f"window ({window}) exceeds the sample length ({nobs})")
|
|
112
|
+
|
|
113
|
+
if lags is None:
|
|
114
|
+
lags = fit_var(data, **fit_kwargs).lag_order
|
|
115
|
+
|
|
116
|
+
index = data.index[window - 1:] # right-aligned, as in the papers
|
|
117
|
+
n_windows = len(index)
|
|
118
|
+
|
|
119
|
+
theta = np.empty((n_windows, k, k))
|
|
120
|
+
to = np.empty((n_windows, k))
|
|
121
|
+
frm = np.empty((n_windows, k))
|
|
122
|
+
total = np.empty(n_windows)
|
|
123
|
+
|
|
124
|
+
for t in range(n_windows):
|
|
125
|
+
res = static_connectedness(
|
|
126
|
+
data.iloc[t:t + window],
|
|
127
|
+
horizon,
|
|
128
|
+
method=method,
|
|
129
|
+
lags=lags,
|
|
130
|
+
**fit_kwargs,
|
|
131
|
+
)
|
|
132
|
+
theta[t] = res.fevd
|
|
133
|
+
to[t] = np.asarray(res.directional_to)
|
|
134
|
+
frm[t] = np.asarray(res.directional_from)
|
|
135
|
+
total[t] = res.total
|
|
136
|
+
|
|
137
|
+
names = list(data.columns)
|
|
138
|
+
|
|
139
|
+
pairwise = theta.transpose(0,2,1) - theta
|
|
140
|
+
|
|
141
|
+
return DynamicConnectednessResult(
|
|
142
|
+
total=pd.Series(total, index=index, name="total"),
|
|
143
|
+
directional_to=pd.DataFrame(to, index=index, columns=names),
|
|
144
|
+
directional_from=pd.DataFrame(frm, index=index, columns=names),
|
|
145
|
+
net=pd.DataFrame(to - frm, index=index, columns=names), # = res.net
|
|
146
|
+
fevd=theta,
|
|
147
|
+
pairwise_net= pairwise,
|
|
148
|
+
names=names,
|
|
149
|
+
window=window,
|
|
150
|
+
horizon=horizon,
|
|
151
|
+
lag_order=lags,
|
|
152
|
+
method=method,
|
|
153
|
+
)
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
r"""
|
|
2
|
+
Frequency connectedness (Baruník-Křehlík 2018).
|
|
3
|
+
|
|
4
|
+
References
|
|
5
|
+
----------
|
|
6
|
+
Baruník and Křehlík (2018) Measuring the frequency dynamics of financial
|
|
7
|
+
connectedness and systemic risk. Journal of Financial Econometrics, 16, 271-296.
|
|
8
|
+
|
|
9
|
+
Diebold and Yilmaz (2012) Better to give than to receive: predictive directional
|
|
10
|
+
measurement of volatility spillovers. International Journal of
|
|
11
|
+
Forecasting, 28, 57-66.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
import pandas as pd
|
|
20
|
+
|
|
21
|
+
from .static import _build_result
|
|
22
|
+
from .var import VARFit, fit_var
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class FrequencyConnectednessResult:
|
|
27
|
+
"""
|
|
28
|
+
Connectedness by frequency band.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
bands : dict
|
|
33
|
+
Band label -> ConnectednessResult, the frequency connectedness
|
|
34
|
+
within : dict
|
|
35
|
+
Band label -> ConnectednessResult, the band rescaled as if it were the
|
|
36
|
+
whole system.
|
|
37
|
+
share : Series
|
|
38
|
+
Share of each band in the forecast error variance, in percent. Sums
|
|
39
|
+
to 100.
|
|
40
|
+
total : Series
|
|
41
|
+
Total connectedness per band, in percent
|
|
42
|
+
horizon : int
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
bands: dict
|
|
46
|
+
within: dict
|
|
47
|
+
share: pd.Series
|
|
48
|
+
total: pd.Series
|
|
49
|
+
horizon: int
|
|
50
|
+
|
|
51
|
+
def __repr__(self) -> str:
|
|
52
|
+
return (f"FrequencyConnectednessResult(TCI={self.total.sum():.2f}%, "
|
|
53
|
+
f"bands={self.total.round(2).to_dict()})")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def frequency_connectedness(
|
|
57
|
+
data: pd.DataFrame | None = None,
|
|
58
|
+
horizon: int = 100,
|
|
59
|
+
*,
|
|
60
|
+
periods: tuple = (5, 20),
|
|
61
|
+
method: str = "generalized",
|
|
62
|
+
var_fit: VARFit | None = None,
|
|
63
|
+
**fit_kwargs,
|
|
64
|
+
) -> FrequencyConnectednessResult:
|
|
65
|
+
r"""
|
|
66
|
+
Compute Baruník-Křehlík frequency connectedness.
|
|
67
|
+
|
|
68
|
+
Provide either `data`` (a VAR is estimated internally) or a pre-fitted
|
|
69
|
+
`var_fit`
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
data : DataFrame, optional
|
|
74
|
+
Multivariate time series, one column per variable.
|
|
75
|
+
horizon : int
|
|
76
|
+
Forecast horizon H, also the number of points of the frequency grid.
|
|
77
|
+
periods : tuple of int
|
|
78
|
+
Band cutoffs as cycle lengths in observations, increasing and larger
|
|
79
|
+
than 2
|
|
80
|
+
method : {"generalized", "orthogonalized"}
|
|
81
|
+
Decomposition scheme, as in static_connectedness.
|
|
82
|
+
var_fit : VARFit, optional
|
|
83
|
+
Pre-fitted VAR model; skips the internal estimation.
|
|
84
|
+
**fit_kwargs
|
|
85
|
+
Passed through to :func:`pyconnectedness.connectedness.var.fit_var`
|
|
86
|
+
(e.g. ``lags``, ``ic``, ``max_lags``).
|
|
87
|
+
|
|
88
|
+
Notes
|
|
89
|
+
-----
|
|
90
|
+
The share of the forecast error variance of variable i due to shocks in
|
|
91
|
+
j on the frequency band d is
|
|
92
|
+
|
|
93
|
+
.. math::
|
|
94
|
+
|
|
95
|
+
\tilde\theta_{ij}(d) = \frac{\sigma_{jj}^{-1} \int_d
|
|
96
|
+
|(\Psi(e^{-i\omega}) \Sigma)_{ij}|^2 d\omega}
|
|
97
|
+
{\sum_l \sigma_{ll}^{-1} \int_{-\pi}^{\pi}
|
|
98
|
+
|(\Psi(e^{-i\omega}) \Sigma)_{il}|^2 d\omega}
|
|
99
|
+
|
|
100
|
+
with :math:`\Psi(e^{-i\omega}) = \sum_h \Phi_h e^{-i\omega h}`.
|
|
101
|
+
|
|
102
|
+
A cutoff of p observations is the frequency :math:`2\pi / p`. As the R
|
|
103
|
+
package frequencyConnectedness takes the cutoffs in radians, so
|
|
104
|
+
`periods=(5, 20)` corresponds to
|
|
105
|
+
`c(pi + 0.00001, 2*pi/5, 2*pi/20, 0)`.
|
|
106
|
+
|
|
107
|
+
Returns
|
|
108
|
+
-------
|
|
109
|
+
FrequencyConnectednessResult
|
|
110
|
+
|
|
111
|
+
Raises
|
|
112
|
+
------
|
|
113
|
+
ValueError
|
|
114
|
+
If neither `data` nor `var_fit` is given, `method` is unknown,
|
|
115
|
+
`periods` is not increasing or not larger than 2, or a band holds no
|
|
116
|
+
frequency at the given horizon.
|
|
117
|
+
"""
|
|
118
|
+
if var_fit is None:
|
|
119
|
+
if data is None:
|
|
120
|
+
raise ValueError("provide either 'data' or 'var_fit'")
|
|
121
|
+
var_fit = fit_var(data, **fit_kwargs)
|
|
122
|
+
|
|
123
|
+
if min(periods) <= 2:
|
|
124
|
+
raise ValueError("periods must be larger than 2")
|
|
125
|
+
if (np.diff(periods) <= 0).any():
|
|
126
|
+
raise ValueError("periods must be increasing")
|
|
127
|
+
|
|
128
|
+
names = var_fit.names
|
|
129
|
+
sigma = var_fit.sigma
|
|
130
|
+
k = var_fit.k
|
|
131
|
+
|
|
132
|
+
# 1) MA matrices Phi_0, ..., Phi_{H-1}, H x k x k
|
|
133
|
+
ma = var_fit.ma_coefficients(horizon)
|
|
134
|
+
|
|
135
|
+
# 2) psi[s] = Psi(exp(-i omega_s)) = sum_h Phi_h exp(-i omega_s h)
|
|
136
|
+
psi = np.fft.fft(ma, axis=0)
|
|
137
|
+
|
|
138
|
+
# 3) omega_s = 2 pi s / H; rows above H/2 are the negative frequencies and
|
|
139
|
+
# fold onto their positive twins
|
|
140
|
+
omega = 2 * np.pi * np.abs(np.fft.fftfreq(horizon))
|
|
141
|
+
|
|
142
|
+
# 4) contribution of shocks in j to variable i at each frequency
|
|
143
|
+
if method == "generalized":
|
|
144
|
+
numerator = np.abs(psi @ sigma) ** 2 / np.diag(sigma)
|
|
145
|
+
elif method == "orthogonalized":
|
|
146
|
+
numerator = np.abs(psi @ np.linalg.cholesky(sigma)) ** 2
|
|
147
|
+
else:
|
|
148
|
+
raise ValueError(
|
|
149
|
+
f"unknown method '{method}'; use 'generalized' or 'orthogonalized'"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# 5) normalize row i over all frequencies and all shocks
|
|
153
|
+
theta = numerator / numerator.sum(axis=(0, 2))[np.newaxis, :, np.newaxis]
|
|
154
|
+
|
|
155
|
+
# 6) band edges as frequencies, high to low
|
|
156
|
+
tol = 1e-10
|
|
157
|
+
edges = [np.inf] + [2 * np.pi / p for p in periods] + [0.0]
|
|
158
|
+
|
|
159
|
+
labels = [f"<={periods[0]}"]
|
|
160
|
+
for i in range(1, len(periods)):
|
|
161
|
+
labels.append(f"{periods[i - 1]}-{periods[i]}")
|
|
162
|
+
labels.append(f">{periods[-1]}")
|
|
163
|
+
|
|
164
|
+
# 7)sum theta over the frequencies of each band
|
|
165
|
+
bands = {}
|
|
166
|
+
within = {}
|
|
167
|
+
share = {}
|
|
168
|
+
total = {}
|
|
169
|
+
for b, label in enumerate(labels):
|
|
170
|
+
in_band = (omega >= edges[b + 1] - tol) & (omega < edges[b] - tol)
|
|
171
|
+
if not in_band.any():
|
|
172
|
+
raise ValueError(f"band '{label}' is empty at horizon {horizon}")
|
|
173
|
+
theta_band = theta[in_band].sum(axis=0)
|
|
174
|
+
bands[label] = _build_result(theta_band, names)
|
|
175
|
+
within[label] = _build_result(theta_band * k / theta_band.sum(), names)
|
|
176
|
+
share[label] = theta_band.sum() / k * 100.0
|
|
177
|
+
total[label] = bands[label].total
|
|
178
|
+
|
|
179
|
+
return FrequencyConnectednessResult(
|
|
180
|
+
bands=bands,
|
|
181
|
+
within=within,
|
|
182
|
+
share=pd.Series(share, name="share"),
|
|
183
|
+
total=pd.Series(total, name="total"),
|
|
184
|
+
horizon=horizon,
|
|
185
|
+
)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
r"""
|
|
2
|
+
Static connectedness measures (Diebold-Yilmaz 2009, 2012).
|
|
3
|
+
|
|
4
|
+
From a normalized variance decomposition :math:`D = [\tilde\theta_{ij}]` the
|
|
5
|
+
spillover measures are derived: the total connectedness index (TCI),
|
|
6
|
+
directional "from" (shocks received), directional "to" (shocks transmitted), and
|
|
7
|
+
net (to minus from).
|
|
8
|
+
|
|
9
|
+
References
|
|
10
|
+
----------
|
|
11
|
+
Diebold and Yilmaz (2009) Measuring financial asset return and volatility
|
|
12
|
+
spillovers, with application to global equity markets. The Economic Journal,
|
|
13
|
+
119, 158-171.
|
|
14
|
+
|
|
15
|
+
Diebold and Yilmaz (2012) Better to give than to receive: predictive
|
|
16
|
+
directional measurement of volatility spillovers. International Journal of
|
|
17
|
+
Forecasting, 28, 57-66.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
|
|
24
|
+
import numpy as np
|
|
25
|
+
import pandas as pd
|
|
26
|
+
|
|
27
|
+
from .decomposition import generalized_fevd, normalize_fevd, orthogonalized_fevd
|
|
28
|
+
from .var import VARFit, fit_var
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class ConnectednessResult:
|
|
33
|
+
"""
|
|
34
|
+
Result of a static connectedness computation.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
table : DataFrame
|
|
39
|
+
Spillover table in Diebold-Yilmaz layout: the normalized decomposition
|
|
40
|
+
(in percent) with "FROM" column and "TO"/"NET" rows.
|
|
41
|
+
fevd : DataFrame (k x k)
|
|
42
|
+
Normalized variance decomposition, in percent.
|
|
43
|
+
total : float
|
|
44
|
+
Total connectedness index, in percent.
|
|
45
|
+
directional_to : Series
|
|
46
|
+
"To others" spillover per variable.
|
|
47
|
+
directional_from : Series
|
|
48
|
+
"From others" spillover per variable.
|
|
49
|
+
net : Series
|
|
50
|
+
Net spillover per variable (to minus from).
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
table: pd.DataFrame
|
|
54
|
+
fevd: pd.DataFrame
|
|
55
|
+
total: float
|
|
56
|
+
directional_to: pd.Series
|
|
57
|
+
directional_from: pd.Series
|
|
58
|
+
net: pd.Series
|
|
59
|
+
pairwise_net: pd.DataFrame
|
|
60
|
+
|
|
61
|
+
def __repr__(self) -> str:
|
|
62
|
+
return f"ConnectednessResult(TCI={self.total:.2f}%, n={self.fevd.shape[0]})"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _measures(theta_norm: np.ndarray) -> dict:
|
|
66
|
+
"""Compute directional and total measures from a normalized decomposition."""
|
|
67
|
+
k = theta_norm.shape[0]
|
|
68
|
+
own = np.diag(theta_norm)
|
|
69
|
+
to_others = (theta_norm.sum(axis=0) - own) * 100.0 # sums, off-diagonal
|
|
70
|
+
from_others = (theta_norm.sum(axis=1) - own) * 100.0 # sums, off-diagonal
|
|
71
|
+
net = to_others - from_others
|
|
72
|
+
incl_own = theta_norm.sum(axis=0) * 100.0
|
|
73
|
+
total = (theta_norm.sum() - np.trace(theta_norm)) / k * 100.0
|
|
74
|
+
pairwise = (theta_norm.T - theta_norm) * 100.0 # C_ij = theta_ji - theta_ij
|
|
75
|
+
return {"to": to_others, "from": from_others, "net": net, "incl_own":incl_own,
|
|
76
|
+
"total": total, "pairwise": pairwise}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _build_result(theta_norm: np.ndarray, names: list) -> ConnectednessResult:
|
|
81
|
+
"""Assemble a result object from a normalized decomposition."""
|
|
82
|
+
m = _measures(theta_norm)
|
|
83
|
+
fevd_df = pd.DataFrame(theta_norm * 100.0, index=names, columns=names)
|
|
84
|
+
|
|
85
|
+
# Spillover table in DY layout: FEVD + FROM column, then TO and NET rows
|
|
86
|
+
table = fevd_df.copy()
|
|
87
|
+
table["FROM"] = m["from"]
|
|
88
|
+
to_row = pd.Series(dict(zip(names, m["to"], strict=True)), name="TO")
|
|
89
|
+
to_row["FROM"] = m["to"].sum() # corner - sum of TO
|
|
90
|
+
incl_row = pd.Series(dict(zip(names,m["incl_own"], strict=True)), name="TO_incl_own")
|
|
91
|
+
incl_row ["FROM"] = m["total"]
|
|
92
|
+
net_row = pd.Series(dict(zip(names, m["net"], strict=True)), name="NET")
|
|
93
|
+
net_row["FROM"] = 0.0 # m["total"] # corner entry = total connectedness index
|
|
94
|
+
table = pd.concat([table, to_row.to_frame().T, incl_row.to_frame().T, net_row.to_frame().T])
|
|
95
|
+
|
|
96
|
+
return ConnectednessResult(
|
|
97
|
+
table=table,
|
|
98
|
+
fevd=fevd_df,
|
|
99
|
+
total=float(m["total"]),
|
|
100
|
+
directional_to=pd.Series(m["to"], index=names, name="TO"),
|
|
101
|
+
directional_from=pd.Series(m["from"], index=names, name="FROM"),
|
|
102
|
+
net=pd.Series(m["net"], index=names, name="NET"),
|
|
103
|
+
pairwise_net=pd.DataFrame(m["pairwise"], index=names, columns=names)#.T,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def static_connectedness(
|
|
107
|
+
data: pd.DataFrame | None = None,
|
|
108
|
+
horizon: int = 10,
|
|
109
|
+
*,
|
|
110
|
+
method: str = "generalized",
|
|
111
|
+
var_fit: VARFit | None = None,
|
|
112
|
+
**fit_kwargs,
|
|
113
|
+
) -> ConnectednessResult:
|
|
114
|
+
"""
|
|
115
|
+
Compute static Diebold-Yilmaz connectedness.
|
|
116
|
+
|
|
117
|
+
Provide either ``data`` (a VAR is estimated internally) or a pre-fitted
|
|
118
|
+
``var_fit``.
|
|
119
|
+
|
|
120
|
+
Parameters
|
|
121
|
+
----------
|
|
122
|
+
data : DataFrame, optional
|
|
123
|
+
Multivariate time series, one column per variable.
|
|
124
|
+
horizon : int
|
|
125
|
+
Forecast horizon H for the variance decomposition.
|
|
126
|
+
method : {"generalized", "orthogonalized"}
|
|
127
|
+
Decomposition scheme - "generalized" (Pesaran-Shin, order-invariant,
|
|
128
|
+
reproduces DY-2012/2014) or "orthogonalized" (Cholesky, order-
|
|
129
|
+
dependent, reproduces DY-2009). Under "orthogonalized" the column
|
|
130
|
+
order of the data determines the result.
|
|
131
|
+
var_fit : VARFit, optional
|
|
132
|
+
Pre-fitted VAR model; skips the internal estimation.
|
|
133
|
+
**fit_kwargs
|
|
134
|
+
Passed through to :func:`pyconnectedness.connectedness.var.fit_var`
|
|
135
|
+
(e.g. ``lags``, ``ic``, ``max_lags``).
|
|
136
|
+
|
|
137
|
+
Returns
|
|
138
|
+
-------
|
|
139
|
+
ConnectednessResult
|
|
140
|
+
|
|
141
|
+
Raises
|
|
142
|
+
------
|
|
143
|
+
ValueError
|
|
144
|
+
If neither ``data`` nor ``var_fit`` is given, or ``method`` is unknown.
|
|
145
|
+
"""
|
|
146
|
+
if var_fit is None:
|
|
147
|
+
if data is None:
|
|
148
|
+
raise ValueError("provide either 'data' or 'var_fit'")
|
|
149
|
+
var_fit = fit_var(data, **fit_kwargs)
|
|
150
|
+
|
|
151
|
+
ma = var_fit.ma_coefficients(horizon)
|
|
152
|
+
if method == "generalized":
|
|
153
|
+
theta = generalized_fevd(ma, var_fit.sigma)
|
|
154
|
+
elif method == "orthogonalized":
|
|
155
|
+
theta = orthogonalized_fevd(ma, var_fit.sigma)
|
|
156
|
+
else:
|
|
157
|
+
raise ValueError(
|
|
158
|
+
f"unknown method '{method}'; use 'generalized' or 'orthogonalized'"
|
|
159
|
+
)
|
|
160
|
+
theta_norm = normalize_fevd(theta)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
return _build_result(theta_norm, var_fit.names)
|