pycointbreak 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.
@@ -0,0 +1,237 @@
1
+ """
2
+ pycointbreak — A Python library for testing breaks in fractional
3
+ cointegration relationships
4
+ ================================================================
5
+
6
+ Implements:
7
+
8
+ * The **Hassler & Breitung (2006)** residual-based LM-type test
9
+ against fractional cointegration (eqs. 12–14 and 17–18).
10
+ * The **Rodrigues, Sibbertsen & Voges (2019)** supremum tests for
11
+ *breaks* in the cointegrating relationship — split sample,
12
+ forward / backward incremental, and rolling (eqs. 5–15).
13
+ * The RSV (2019) **break-point estimator** (eq. 19), with both
14
+ forward and reverse residual scans (Theorem 3 / Remark 3.3).
15
+
16
+ Author
17
+ ------
18
+ Dr. Merwan Roudane
19
+ Email: merwanroudane920@gmail.com
20
+ GitHub: https://github.com/merwanroudane/pycointbreak
21
+
22
+ References
23
+ ----------
24
+ Hassler, U. and Breitung, J. (2006). A Residual-Based LM Type Test
25
+ Against Fractional Cointegration. *Econometric Theory*, 22(6),
26
+ 1091-1111.
27
+
28
+ Rodrigues, P. M. M., Sibbertsen, P. and Voges, M. (2019).
29
+ Testing for breaks in the cointegrating relationship: On the
30
+ stability of government bond markets' equilibrium. Discussion
31
+ Paper 656.
32
+ """
33
+
34
+ __version__ = "0.1.0"
35
+ __author__ = "Dr. Merwan Roudane"
36
+ __email__ = "merwanroudane920@gmail.com"
37
+ __url__ = "https://github.com/merwanroudane/pycointbreak"
38
+
39
+ from .breakpoint import BreakPointResult, estimate_break_point
40
+ from .critical_values import (
41
+ HB_CRITICAL_VALUES,
42
+ RSV_TABLE1,
43
+ bootstrap_rsv_cv,
44
+ hb_critical_value,
45
+ hb_pvalue,
46
+ rsv_critical_value,
47
+ simulate_sup_chi2_cv,
48
+ )
49
+ from .fracdiff import (
50
+ drift_regressor,
51
+ fdiff,
52
+ frac_coefs,
53
+ harmonic_running_sum,
54
+ )
55
+ from .gph import GPHResult, gph
56
+ from .hassler_breitung import HBResult, hassler_breitung_test
57
+ from .reporting import (
58
+ battery_to_dataframe,
59
+ combine_results,
60
+ render_table,
61
+ )
62
+ from .rsv_tests import RSVResult, rsv_battery, rsv_sup_test
63
+ from .simulate import (
64
+ frac_integrate,
65
+ simulate_hb_dgp,
66
+ simulate_rsv_dgp,
67
+ simulate_segmented_cointegration,
68
+ )
69
+
70
+ # Plotting is a soft dependency — only re-exported if matplotlib is
71
+ # importable.
72
+ try: # pragma: no cover
73
+ from .plots import (
74
+ plot_breakpoint_objective,
75
+ plot_residual_diagnostics,
76
+ plot_rsv_battery,
77
+ plot_rsv_profile,
78
+ plot_series_with_breaks,
79
+ plot_split_heatmap,
80
+ set_style,
81
+ )
82
+ except ImportError: # pragma: no cover
83
+ pass
84
+
85
+
86
+ __all__ = [
87
+ # version / metadata
88
+ "__version__",
89
+ "__author__",
90
+ "__email__",
91
+ "__url__",
92
+ # main tests
93
+ "hassler_breitung_test",
94
+ "rsv_sup_test",
95
+ "rsv_battery",
96
+ "estimate_break_point",
97
+ # result classes
98
+ "HBResult",
99
+ "RSVResult",
100
+ "BreakPointResult",
101
+ "GPHResult",
102
+ # building blocks
103
+ "fdiff",
104
+ "frac_coefs",
105
+ "harmonic_running_sum",
106
+ "drift_regressor",
107
+ "frac_integrate",
108
+ "gph",
109
+ # critical values
110
+ "hb_critical_value",
111
+ "hb_pvalue",
112
+ "rsv_critical_value",
113
+ "simulate_sup_chi2_cv",
114
+ "bootstrap_rsv_cv",
115
+ "HB_CRITICAL_VALUES",
116
+ "RSV_TABLE1",
117
+ # simulation
118
+ "simulate_rsv_dgp",
119
+ "simulate_segmented_cointegration",
120
+ "simulate_hb_dgp",
121
+ # reporting
122
+ "battery_to_dataframe",
123
+ "combine_results",
124
+ "render_table",
125
+ # plotting (re-exported only if matplotlib is installed)
126
+ "plot_series_with_breaks",
127
+ "plot_rsv_profile",
128
+ "plot_rsv_battery",
129
+ "plot_breakpoint_objective",
130
+ "plot_residual_diagnostics",
131
+ "plot_split_heatmap",
132
+ "set_style",
133
+ ]
134
+
135
+
136
+ def cite() -> str:
137
+ """Return a BibTeX-formatted citation for the library and the two
138
+ underlying papers."""
139
+ return r"""
140
+ @software{Roudane_pycointbreak_2024,
141
+ author = {Roudane, Merwan},
142
+ title = {pycointbreak: Python tools for fractional cointegration
143
+ tests with breaks},
144
+ year = {2024},
145
+ url = {https://github.com/merwanroudane/pycointbreak},
146
+ version = {""" + __version__ + r"""}
147
+ }
148
+
149
+ @article{HasslerBreitung2006,
150
+ author = {Hassler, Uwe and Breitung, J{\"o}rg},
151
+ title = {A Residual-Based {LM} Type Test Against Fractional
152
+ Cointegration},
153
+ journal = {Econometric Theory},
154
+ volume = {22},
155
+ number = {6},
156
+ pages = {1091--1111},
157
+ year = {2006}
158
+ }
159
+
160
+ @techreport{RodriguesSibbertsenVoges2019,
161
+ author = {Rodrigues, Paulo M. M. and Sibbertsen, Philipp and
162
+ Voges, Michelle},
163
+ title = {Testing for breaks in the cointegrating relationship:
164
+ On the stability of government bond markets'
165
+ equilibrium},
166
+ institution = {Hannover Economic Papers (HEP)},
167
+ number = {656},
168
+ year = {2019}
169
+ }
170
+ """.strip() + "\n"
171
+
172
+
173
+ def help_text() -> str:
174
+ """Return a one-screen orientation guide."""
175
+ return r"""
176
+ ================================================================
177
+ pycointbreak — Quickstart
178
+ ================================================================
179
+ Author : Dr. Merwan Roudane <merwanroudane920@gmail.com>
180
+ GitHub : https://github.com/merwanroudane/pycointbreak
181
+ Version: """ + __version__ + r"""
182
+
183
+ Implements:
184
+ - Hassler & Breitung (2006) LM test for no fractional
185
+ cointegration.
186
+ - Rodrigues, Sibbertsen & Voges (2019) supremum tests for
187
+ breaks in the cointegrating relationship.
188
+ - Break-point estimator (RSV2019 eq. 19).
189
+
190
+ Typical workflow
191
+ ----------------
192
+ >>> import numpy as np
193
+ >>> from pycointbreak import (
194
+ ... simulate_segmented_cointegration,
195
+ ... hassler_breitung_test,
196
+ ... rsv_battery,
197
+ ... estimate_break_point,
198
+ ... plot_rsv_battery,
199
+ ... plot_series_with_breaks,
200
+ ... plot_breakpoint_objective,
201
+ ... render_table, battery_to_dataframe,
202
+ ... )
203
+
204
+ >>> # 1. Get / simulate two I(d) series.
205
+ >>> y, x = simulate_segmented_cointegration(
206
+ ... T=500, d=1.0, b=0.4, break_frac=0.5,
207
+ ... regime="coint_then_spurious", seed=1)
208
+
209
+ >>> # 2. Full-sample HB test.
210
+ >>> hb = hassler_breitung_test(y, x, d=1.0, p=1)
211
+ >>> print(hb.summary())
212
+
213
+ >>> # 3. RSV battery of sup-tests for breaks.
214
+ >>> bat = rsv_battery(y, x, d=1.0)
215
+ >>> for k, r in bat.items():
216
+ ... print(r.summary())
217
+
218
+ >>> # 4. Pretty Table 7-style summary.
219
+ >>> df = battery_to_dataframe(bat, hb=hb, label="y on x")
220
+ >>> print(render_table(df, fmt="text",
221
+ ... title="Tests for segmented cointegration"))
222
+
223
+ >>> # 5. Estimate the break date.
224
+ >>> bp = estimate_break_point(y, x, d=1.0, direction="auto")
225
+ >>> print(bp.summary())
226
+
227
+ >>> # 6. Plots.
228
+ >>> plot_series_with_breaks({"y": y, "x": x}, {"break": bp.obs})
229
+ >>> plot_rsv_battery(bat)
230
+ >>> plot_breakpoint_objective(bp)
231
+ ================================================================
232
+ """.strip()
233
+
234
+
235
+ def about() -> None: # pragma: no cover
236
+ """Print library metadata and author info."""
237
+ print(help_text())
@@ -0,0 +1,238 @@
1
+ """
2
+ Break-point estimator
3
+ =====================
4
+
5
+ Implements eq. (19) of Rodrigues, Sibbertsen & Voges (2019):
6
+
7
+ .. math::
8
+
9
+ \\hat\\tau \\;=\\; \\arg\\inf_{\\tau \\in \\Delta}
10
+ [\\tau T]^{-2\\hat d}\\sum_{t=1}^{[\\tau T]}\\hat e_t^{\\,2}(\\tau),
11
+ \\qquad \\Delta = (\\delta, 1 - \\delta),
12
+
13
+ with :math:`0 < \\delta < 0.5`. By Theorem 3, when the break is from
14
+ cointegration to no cointegration, :math:`\\hat\\tau \\to \\tau_0`.
15
+
16
+ By Remark 3.3 the same estimator applied to the reversed residual
17
+ series can be used when the break is from no cointegration to
18
+ cointegration.
19
+
20
+ Author
21
+ ------
22
+ Dr. Merwan Roudane <merwanroudane920@gmail.com>
23
+ """
24
+ from __future__ import annotations
25
+
26
+ from dataclasses import dataclass
27
+ from typing import Literal, Optional, Union
28
+
29
+ import numpy as np
30
+ import pandas as pd
31
+
32
+ ArrayLike = Union[np.ndarray, pd.Series, pd.DataFrame]
33
+
34
+
35
+ @dataclass
36
+ class BreakPointResult:
37
+ """Result of the RSV2019 break-point estimator.
38
+
39
+ Attributes
40
+ ----------
41
+ tau_hat : float
42
+ Break fraction in [0, 1].
43
+ obs : int
44
+ Observation index of the estimated break (``int(tau_hat * T)``).
45
+ date : pandas.Timestamp or None
46
+ Calendar date of the break if the input series carried a
47
+ DatetimeIndex.
48
+ delta : float
49
+ Trimming parameter used.
50
+ direction : str
51
+ Either 'forward' (cointegrated -> spurious) or 'backward'
52
+ (spurious -> cointegrated).
53
+ objective : numpy.ndarray
54
+ The objective ``[tauT]^{-2d} * SSR(tau)`` evaluated along the
55
+ full grid (useful for plotting).
56
+ tau_grid : numpy.ndarray
57
+ Grid of break fractions on which the objective was evaluated.
58
+ T : int
59
+ d : float
60
+ """
61
+
62
+ tau_hat: float
63
+ obs: int
64
+ date: Optional[pd.Timestamp]
65
+ delta: float
66
+ direction: str
67
+ objective: np.ndarray
68
+ tau_grid: np.ndarray
69
+ T: int
70
+ d: float
71
+
72
+ def summary(self) -> str:
73
+ lines = []
74
+ lines.append("=" * 70)
75
+ lines.append(" RSV (2019) break-point estimator")
76
+ lines.append("=" * 70)
77
+ lines.append(f" Direction : {self.direction}")
78
+ lines.append(f" Trimming delta : {self.delta:.3f}")
79
+ lines.append(f" Fractional order d : {self.d:.3f}")
80
+ lines.append(f" Sample size T : {self.T}")
81
+ lines.append("-" * 70)
82
+ lines.append(f" tau_hat : {self.tau_hat:.4f}")
83
+ lines.append(f" Observation : {self.obs}")
84
+ if self.date is not None:
85
+ lines.append(f" Estimated break date : {self.date.date()}")
86
+ lines.append("=" * 70)
87
+ return "\n".join(lines)
88
+
89
+ def __repr__(self) -> str: # pragma: no cover
90
+ return (
91
+ f"BreakPointResult(tau_hat={self.tau_hat:.3f}, obs={self.obs}, "
92
+ f"direction={self.direction})"
93
+ )
94
+
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # Internal: OLS residuals
98
+ # ---------------------------------------------------------------------------
99
+ def _ols_residuals_for_breakpoint(
100
+ y1: np.ndarray,
101
+ y2: np.ndarray,
102
+ include_const: bool,
103
+ ) -> np.ndarray:
104
+ T = y1.size
105
+ if include_const:
106
+ X = np.column_stack([np.ones(T), y2])
107
+ else:
108
+ X = y2
109
+ beta, *_ = np.linalg.lstsq(X, y1, rcond=None)
110
+ return y1 - X @ beta
111
+
112
+
113
+ # ---------------------------------------------------------------------------
114
+ # Public API
115
+ # ---------------------------------------------------------------------------
116
+ def estimate_break_point(
117
+ y1: ArrayLike,
118
+ y2: ArrayLike,
119
+ d: float = 1.0,
120
+ delta: float = 0.05,
121
+ direction: Literal["forward", "backward", "auto"] = "auto",
122
+ include_const: bool = True,
123
+ residuals: Optional[np.ndarray] = None,
124
+ index: Optional[pd.DatetimeIndex] = None,
125
+ ) -> BreakPointResult:
126
+ """Estimate the break fraction :math:`\\tau_0` per eq. (19) of
127
+ RSV2019.
128
+
129
+ Parameters
130
+ ----------
131
+ y1 : array_like, shape (T,)
132
+ Scalar :math:`I(d)` series.
133
+ y2 : array_like, shape (T,) or (T, m)
134
+ :math:`I(d)` regressor(s).
135
+ d : float, default 1.0
136
+ Fractional integration order. By Theorem 3, ``d`` need only be
137
+ consistently estimated.
138
+ delta : float, default 0.05
139
+ Trimming parameter :math:`0 < \\delta < 0.5`. RSV2019 (last
140
+ paragraph of Sec. 4) recommend a small :math:`\\delta`.
141
+ direction : {'forward', 'backward', 'auto'}, default 'auto'
142
+ ``'forward'`` corresponds to a break from cointegration to no
143
+ cointegration (Theorem 3). ``'backward'`` (Remark 3.3) applies
144
+ the estimator to the reversed residual series, suitable for a
145
+ break from no cointegration to cointegration. ``'auto'``
146
+ evaluates both and returns the lower objective value.
147
+ include_const : bool, default True
148
+ Whether to include a constant in the cointegrating regression.
149
+ residuals : array_like, optional
150
+ Pre-computed OLS residuals (overrides ``y1``, ``y2``).
151
+ index : pandas.DatetimeIndex, optional
152
+ If given, used to map the estimated observation index to a
153
+ calendar date.
154
+
155
+ Returns
156
+ -------
157
+ BreakPointResult
158
+ """
159
+ if residuals is None:
160
+ y1_arr = np.asarray(y1, dtype=float).ravel()
161
+ y2_arr = np.atleast_2d(np.asarray(y2, dtype=float))
162
+ if y2_arr.shape[0] != y1_arr.size:
163
+ y2_arr = y2_arr.T
164
+ if isinstance(y1, pd.Series) and index is None:
165
+ if isinstance(y1.index, pd.DatetimeIndex):
166
+ index = y1.index
167
+ e = _ols_residuals_for_breakpoint(y1_arr, y2_arr, include_const)
168
+ else:
169
+ e = np.asarray(residuals, dtype=float).ravel()
170
+
171
+ T = e.size
172
+ if not 0.0 < delta < 0.5:
173
+ raise ValueError("delta must be in (0, 0.5).")
174
+
175
+ t_lo = int(np.ceil(delta * T))
176
+ t_hi = int(np.floor((1.0 - delta) * T))
177
+ if t_hi <= t_lo + 1:
178
+ raise ValueError(
179
+ "Sample too small for the requested delta — reduce delta."
180
+ )
181
+
182
+ def _forward_obj(e_local: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
183
+ tau_grid = np.arange(t_lo, t_hi + 1) / T
184
+ obj = np.empty(tau_grid.size, dtype=float)
185
+ # Cumulative sum of squared residuals — O(T) total.
186
+ csum_sq = np.cumsum(e_local**2)
187
+ for k, tt in enumerate(np.arange(t_lo, t_hi + 1)):
188
+ ssr = csum_sq[tt - 1]
189
+ scale = float(tt) ** (-2.0 * d)
190
+ obj[k] = scale * ssr
191
+ return tau_grid, obj
192
+
193
+ def _do(direction_label: str, e_local: np.ndarray):
194
+ tau_grid, obj = _forward_obj(e_local)
195
+ k_star = int(np.argmin(obj))
196
+ tau_hat = float(tau_grid[k_star])
197
+ obs = int(tau_hat * T)
198
+ date = None
199
+ if index is not None:
200
+ # For backward, the optimum was found on the reversed
201
+ # series, so the calendar position is T - obs (clipped).
202
+ obs_for_date = obs if direction_label == "forward" else max(
203
+ 0, T - obs - 1
204
+ )
205
+ try:
206
+ date = index[obs_for_date]
207
+ except IndexError:
208
+ date = None
209
+ return tau_hat, obs, date, tau_grid, obj
210
+
211
+ if direction == "forward":
212
+ tau_hat, obs, date, grid, obj = _do("forward", e)
213
+ elif direction == "backward":
214
+ tau_hat, obs, date, grid, obj = _do("backward", e[::-1])
215
+ elif direction == "auto":
216
+ fwd = _do("forward", e)
217
+ bwd = _do("backward", e[::-1])
218
+ # Pick the one with lower objective at its argmin.
219
+ if fwd[4].min() <= bwd[4].min():
220
+ tau_hat, obs, date, grid, obj = fwd
221
+ direction = "forward"
222
+ else:
223
+ tau_hat, obs, date, grid, obj = bwd
224
+ direction = "backward"
225
+ else:
226
+ raise ValueError(f"Unknown direction '{direction}'.")
227
+
228
+ return BreakPointResult(
229
+ tau_hat=tau_hat,
230
+ obs=obs,
231
+ date=date,
232
+ delta=float(delta),
233
+ direction=direction,
234
+ objective=obj,
235
+ tau_grid=grid,
236
+ T=int(T),
237
+ d=float(d),
238
+ )