Source code for eval_utils


import numpy as np

[docs] class PinballLoss(): ''' Pinball (a.k.a. Quantile) loss function Parameters: ---------------- - theta: float the target confidence level - ret_mean: bool, optional if True, the function returns the mean of the loss, otherwise the loss point-by-point. Default is True Example of usage ---------------- .. code-block:: python import numpy as np from eval_utils import PinballLoss y = np.random.randn(250)*1e-2 #Replace with price returns qf = np.random.uniform(-1, 0, 250) #Replace with quantile forecasts theta = 0.05 #Set the desired confidence level PinballLoss(theta)(qf, y) #Compute the pinball loss Methods: ---------------- ''' def __init__(self, theta, ret_mean=True): self.theta = theta self.ret_mean = ret_mean def __call__(self, y_pred, y_true): ''' Compute the pinball loss INPUTS: - y_pred: ndarray the predicted values - y_true: ndarray the true values OUTPUTS: - loss: float the loss function mean value, if ret_mean is True. Otherwise, the loss for each observation ''' #Check consistency in the dimensions if len(y_pred.shape) == 1: y_pred = y_pred.reshape(-1,1) if len(y_true.shape) == 1: y_true = y_true.reshape(-1,1) if y_pred.shape != y_true.shape: raise ValueError(f'Dimensions of y_pred ({y_pred.shape}) and y_true ({y_true.shape}) do not match!!!') # Compute the pinball loss error = y_true - y_pred loss = np.where(error >= 0, self.theta * error, (self.theta - 1) * error) if self.ret_mean: #If true, return the mean of the loss loss = np.nanmean(loss) return loss
[docs] class barrera_loss(): ''' Barrera loss function ''' def __init__(self, theta, ret_mean=True): ''' INPUT: - theta: float, the threshold for the loss function - ret_mean: bool, if True, the function returns the mean of the loss. Default is True ''' self.theta = theta self.ret_mean = ret_mean def __call__(self, v_, e_, y_): ''' INPUT: - v_: numpy array, the quantile estimate - e_: numpy array, the expected shortfall estimate - y_: numpy array, the actual time series OUTPUT: - loss: float, the loss function mean value, if ret_mean is True. Otherwise, the loss for each observation ''' v, e, y = v_.flatten(), e_.flatten(), y_.flatten() r = e - v #Barrera loss is computed on the difference ES - VaR if self.ret_mean: #If true, return the mean of the loss loss = np.nanmean( (r - np.where(y<v, (y-v)/self.theta, 0))**2 ) else: #Otherwise, return the loss for each observation loss = (r - np.where(y<v, (y-v)/self.theta, 0))**2 return loss
[docs] class patton_loss(): ''' Patton loss function ''' def __init__(self, theta, ret_mean=True): ''' INPUT: - theta: float, the threshold for the loss function - ret_mean: bool, if True, the function returns the mean of the loss. Default is True ''' self.theta = theta self.ret_mean = ret_mean def __call__(self, v_, e_, y_): ''' INPUT: - v_: numpy array, the quantile estimate - e_: numpy array, the expected shortfall estimate - y_: numpy array, the actual time series OUTPUT: - loss: float, the loss function mean value, if ret_mean is True. Otherwise, the loss for each observation ''' v, e, y = v_.flatten()*100, e_.flatten()*100, y_.flatten()*100 if self.ret_mean: #If true, return the mean of the loss loss = np.nanmean( np.where(y<=v, (y-v)/(self.theta*e), 0) + v/e + np.log(-e) - 1 ) else: #Otherwise, return the loss for each observation loss = np.where(y<=v, (y-v)/(self.theta*e), 0) + v/e + np.log(-e) - 1 return loss
[docs] class bootstrap_mean_test(): ''' Bootstrap test for assessing whenever mean of a sample is == or >= a target value Parameters: ---------------- - mu_target: float the mean to test against - one_side: bool, optional if True, the test is one sided (i.e. H0: mu >= mu_target), otherwise it is two-sided (i.e. H0: mu == mu_target). Default is False - n_boot: int, optional the number of bootstrap replications. Default is 10_000 ''' def __init__(self, mu_target, one_side=False, n_boot=10_000): self.mu_target = mu_target self.one_side = one_side self.n_boot = n_boot def null_statistic(self, B_data): ''' Compute the null statistic for the bootstrap sample INPUTS: - B_data: ndarray the bootstrap sample OUTPUTS: - stat: float the null statistic :meta private: ''' return (np.mean(B_data) - self.obs_mean) * np.sqrt(self.n) / np.std(B_data) def statistic(self, data): ''' Compute the test statistic for the original sample INPUTS: :data: ndarray the original sample OUTPUTS: - :float the test statistic :meta private: ''' return (self.obs_mean - self.mu_target) * np.sqrt(self.n) / np.std(data) def __call__(self, data, seed=None): ''' Compute the test INPUTS: - data: ndarray the original sample - seed: int, optional the seed for the random number generator. Default is None OUTPUTS: - statistic: float the test statistic - p_value: float the p-value of the test ''' np.random.seed(seed) self.obs_mean = np.mean(data) self.n = len(data) B_stats = list() for _ in range(self.n_boot): B_stats.append( self.null_statistic( np.random.choice(data, size=self.n, replace=True) )) B_stats = np.array(B_stats) self.B_stats = B_stats if self.one_side: obs = self.statistic(data) return {'statistic':obs, 'p_value':np.mean(B_stats < obs)} else: obs = np.abs(self.statistic(data)) return {'statistic':self.statistic(data), 'p_value':np.mean((B_stats > obs) | (B_stats < -obs))}
[docs] class AS14_test(bootstrap_mean_test): ''' Acerbi-Szekely test for assessing the goodness of the Expected Shortfall estimate, with both Z1 and Z2 statistics, as described in: Acerbi, C., & Szekely, B. (2014). Back-testing expected shortfall. Risk, 27(11), 76-81. Parameters: ---------------- - one_side: bool, optional if True, the test is one sided (i.e. H0: mu >= mu_target). Default is False - n_boot: int, optional the number of bootstrap replications. Default is 10_000 Example of usage ---------------- .. code-block:: python import numpy as np from eval_utils import AS14_test y = np.random.randn(250)*1e-2 #Replace with price returns qf = np.random.uniform(-1, 0, 250)*1e-1 #Replace with quantile forecasts ef = np.random.uniform(-1, 0, 250)*1e-1 #Replace with expected shortfall forecasts theta = 0.05 #Set the desired confidence level # Compute the Acerbi-Szekely test with Z1 statistic AS14_test()(qf, ef, y, test_type='Z1', theta=theta, seed=2) Methods: ---------------- ''' def __init__(self, one_side=False, n_boot=10_000): super().__init__(-1, one_side, n_boot) def as14_transform(self, test_type, Q, E, Y, theta): ''' Transform the data to compute the Acerbi-Szekely test INPUTS: :test_type: str the type of test to perform. It must be either 'Z1' or 'Z2' :Q: ndarray the quantile estimates :E: ndarray the expected shortfall estimates :Y: ndarray the actual time series :theta: float the threshold for the test OUTPUTS: - :ndarray the transformed data :meta private: ''' import warnings Q, E, Y = Q.flatten(), E.flatten(), Y.flatten() #Flatten the data if test_type == 'Z1': output = (- Y/E)[Y <= Q] elif test_type == 'Z2': output = - Y * (Y <= Q) / (theta * E) else: raise ValueError(f'test_type {test_type} not recognized. It must be either Z1 or Z2') n = len(output) output = output[~np.isnan(output)] if len(output) < n: warnings.warn('There are NaN in the population! They have been removed.', UserWarning) return output def __call__(self, Q, E, Y, theta, test_type='Z1', seed=None): ''' Compute the test INPUTS: - Q: ndarray the quantile estimates - E: ndarray the expected shortfall estimates - Y: ndarray the actual time series - test_type: str, optional the type of test to perform. It must be either 'Z1' or 'Z2'. Default is 'Z1' - seed: int, optional the seed for the random number generator. Default is None OUTPUTS: - statistic: float the test statistic - p_value: float the p-value of the test ''' return super().__call__( self.as14_transform(test_type, Q, E, Y, theta).flatten(), seed)