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.
- pycointbreak/__init__.py +237 -0
- pycointbreak/breakpoint.py +238 -0
- pycointbreak/critical_values.py +281 -0
- pycointbreak/fracdiff.py +244 -0
- pycointbreak/gph.py +127 -0
- pycointbreak/hassler_breitung.py +456 -0
- pycointbreak/plots.py +363 -0
- pycointbreak/reporting.py +240 -0
- pycointbreak/rsv_tests.py +531 -0
- pycointbreak/simulate.py +194 -0
- pycointbreak-0.1.0.dist-info/METADATA +316 -0
- pycointbreak-0.1.0.dist-info/RECORD +15 -0
- pycointbreak-0.1.0.dist-info/WHEEL +5 -0
- pycointbreak-0.1.0.dist-info/licenses/LICENSE +21 -0
- pycointbreak-0.1.0.dist-info/top_level.txt +1 -0
pycointbreak/__init__.py
ADDED
|
@@ -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
|
+
)
|