Source code for mizarlabs.structural_breaks.cusum_chu_stinchcome_white

"""
Implementation of Chu-Stinchcombe-White test
"""
import numpy as np
import pandas as pd
from numba import jit
from numba import njit
from numba import prange

ONE_SIDED_POSITIVE = "one_sided_positive"
ONE_SIDED_NEGATIVE = "one_sided_negative"
TWO_SIDED = "two_sided"


[docs]@jit(parallel=True, nopython=True) def get_test_statistics_array( side_test: str, t_indices: np.ndarray, time_series_array: np.ndarray ) -> np.ndarray: """Returns an array with the test statistics of the Chu-Stinchcombe-White and the critical values :param side_test: 'one_sided_positive' or 'one_sided_negative' or 'two_sided' :type side_test: str :param t_indices: t indices to loop over :type t_indices: np.ndarray :param time_series_array: Time series to be tested :type time_series_array: np.ndarray :return: Array with two columns, one for the test statistic and one for the critical values :rtype: np.ndarray """ b_alpha_5_pct = 4.6 # 4.6 is b_a estimate derived via Monte-Carlo test_statistics_array = np.empty((t_indices.shape[0], 2), dtype=np.float64) # outer loops goes over all t for i in prange(len(t_indices)): # compute variance t = t_indices[i] array_t = time_series_array[:t] sigma_squared_t = np.sum(np.square(np.diff(array_t))) / (t - 1) # init supremum vals max_S_n_t_value = -np.inf max_S_n_t_critical_value = np.nan # Corresponds to c_alpha[n,t] y_t = array_t[t - 1] # inner loop goes over all n between 1 and t for j in prange(len(array_t) - 1): # compute test statistic n = j + 1 y_n = time_series_array[j] y_t_y_n_diff = get_y_t_y_n_diff(side_test, y_t, y_n) S_n_t = y_t_y_n_diff / np.sqrt(sigma_squared_t * (t - n)) # check if new val is better than supremum # if so compute new critical value if S_n_t > max_S_n_t_value: max_S_n_t_value = S_n_t max_S_n_t_critical_value = np.sqrt(b_alpha_5_pct + np.log(t - n)) # store result of iteration test_statistics_array[i, :] = np.array( [max_S_n_t_value, max_S_n_t_critical_value], dtype=np.float64 ) return test_statistics_array
[docs]@njit def get_y_t_y_n_diff(side_test: str, y_t: np.float64, y_n: np.float64) -> np.float64: """Returns the difference between y_t and y_n given a test specification. :param side_test: 'one_sided_positive' or 'one_sided_negative' or 'two_sided' :type side_test: str :param y_t: value of the series to be tested at time t :type y_t: np.float64 :param y_n: value of the series to be tested at time n, where n < t :type y_n: np.float64 :return: difference between y_t and y_n given a test specification :rtype: np.float64 """ if side_test == ONE_SIDED_POSITIVE: values_diff = y_t - y_n elif side_test == ONE_SIDED_NEGATIVE: values_diff = y_n - y_t else: values_diff = abs(y_t - y_n) return values_diff
[docs]class ChuStinchcombeWhiteStatTest: STATISTIC = "statistic" CRITICAL_VALUE = "critical_value" def __init__(self, side_test: str): """ Chu-Stinchcombe-White test CUSUM Tests on Levels as described on page 250-251 in Advances in Financial Machine Learning by Marcos Lopez de Prado. :param side_test: 'one_sided_positive' or 'one_sided_negative' or 'two_sided' :type side_test: str """ expected_vals = [ONE_SIDED_NEGATIVE, ONE_SIDED_POSITIVE, TWO_SIDED] assert ( side_test in expected_vals ), f"Expected one of {expected_vals} got {side_test}..." self.side_test = side_test
[docs] def run(self, series_to_test: pd.Series) -> pd.DataFrame: """Runs the Chu-Stinchcombe-White stat test and returns a dataframe with the test statistics and corresponding critical values. :param series_to_test: Time series to be tested. :type series_to_test: pd.Series :return: DataFrame with results. :rtype: pd.DataFrame """ t_indices = np.arange(2, len(series_to_test) + 1) S_n_t_array = get_test_statistics_array( self.side_test, t_indices, series_to_test.values ) return pd.DataFrame( S_n_t_array, index=series_to_test.index[1:], columns=[self.STATISTIC, self.CRITICAL_VALUE], )