├── .idea ├── .gitignore ├── DeepLearningCreditRiskModeling.iml ├── encodings.xml ├── inspectionProfiles │ └── profiles_settings.xml ├── misc.xml ├── modules.xml └── vcs.xml ├── DNN_website.JPG ├── README.md ├── data ├── calibratedmodels │ └── %s_calib_output.pkl ├── market_data │ └── market_data.xlsx ├── simulations │ ├── 0SV1SJ_sims.plk │ ├── 1SV1SJ_sims.plk │ ├── 2SV0SJ_sims.plk │ ├── 2SV1SJ_sims.plk │ ├── HestonJump_sims.plk │ ├── Heston_sims.plk │ ├── KouJump_sims.plk │ ├── Merton74basic_sims.plk │ ├── Merton76jump_sims.plk │ └── PanSingleton2008_sims.plk └── trainedmodels │ ├── 0SV1SJ_DNNWeights.h5 │ ├── 1SV1SJ_DNNWeights.h5 │ ├── 2SV0SJ_DNNWeights.h5 │ ├── 2SV1SJ_DNNWeights.h5 │ ├── HestonJump_DNNWeights.h5 │ ├── Heston_DNNWeights.h5 │ ├── KouJump_DNNWeights.h5 │ ├── Merton74basic_DNNWeights.h5 │ ├── Merton76jump_DNNWeights.h5 │ └── PanSingleton2008_DNNWeights.h5 ├── dnn_training ├── __init__.py ├── dnn_trainer.py ├── dnn_utils.py └── master_trainer.py ├── efficiencyTable.png ├── environment.py ├── model_calibration ├── __init__.py ├── calib_utils.py ├── master_calibration.py └── uk_dnn_filter.py └── model_simulator ├── __init__.py ├── charafun_pricing.py ├── master_simulator.py └── pricing_models.py /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/DeepLearningCreditRiskModeling.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /DNN_website.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmanzog/DeepLearningCreditRiskModeling/78487f0da054ea39e9fb61708063afe017f3e8d9/DNN_website.JPG -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deep Learning Credit Risk Modeling 2 | Replication codes for Deep Learning Credit Risk Modeling by Manzo, Qiao 3 | 4 | This repository contains the Python APIs to replicate the paper "Deep Learning Credit Risk Modeling," Gerardo Manzo and Xiao Qiao, The Journal of Fixed Income Fall 2021, jfi.2021.1.121; DOI: https://doi.org/10.3905/jfi.2021.1.121 5 | 6 | ![alt text](https://github.com/gmanzog/DeepLearningCreditRiskModeling/blob/main/DNN_website.JPG) 7 | 8 | # Abstract 9 | This article demonstrates how deep learning can be used to price and calibrate models of credit risk. Deep neural networks can learn structural and reduced-form models with high degrees of accuracy. For complex credit risk models with no closed-form solutions available, deep learning offers a conceptually simple and more efficient alternative solution. This article proposes an approach that combines deep learning with the unscented Kalman filter to calibrate credit risk models based on historical data; this strategy attains an in-sample R-squared of 98.5% for the reduced-form model and 95% for the structural model. 10 | 11 | # How to run the codes 12 | In each folder, the main file's name starts with 'master_' and folders are organized as follows (in order): 13 | Markup : 14 | * model_simulator: simulate data from pricing models 15 | * dnn_training: after the data is simulated, train each model 16 | * model_calibration: calibrate models to real data using trained DNN 17 | 18 | Finally, the folder 'data' contains all the generated data and trained models. 19 | 20 | # Calibration Efficiency 21 | Our DNN approach will make it very efficient to online calibrate a pricing model to data. In practice, our trained DNNs will replace any numerical approximation of any complex pricing function that is not available in closed-form solution. 22 | 23 | ![alt text](https://github.com/gmanzog/DeepLearningCreditRiskModeling/blob/main/efficiencyTable.png) 24 | -------------------------------------------------------------------------------- /data/calibratedmodels/%s_calib_output.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmanzog/DeepLearningCreditRiskModeling/78487f0da054ea39e9fb61708063afe017f3e8d9/data/calibratedmodels/%s_calib_output.pkl -------------------------------------------------------------------------------- /data/market_data/market_data.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmanzog/DeepLearningCreditRiskModeling/78487f0da054ea39e9fb61708063afe017f3e8d9/data/market_data/market_data.xlsx -------------------------------------------------------------------------------- /data/simulations/0SV1SJ_sims.plk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmanzog/DeepLearningCreditRiskModeling/78487f0da054ea39e9fb61708063afe017f3e8d9/data/simulations/0SV1SJ_sims.plk -------------------------------------------------------------------------------- /data/simulations/1SV1SJ_sims.plk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmanzog/DeepLearningCreditRiskModeling/78487f0da054ea39e9fb61708063afe017f3e8d9/data/simulations/1SV1SJ_sims.plk -------------------------------------------------------------------------------- /data/simulations/2SV0SJ_sims.plk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmanzog/DeepLearningCreditRiskModeling/78487f0da054ea39e9fb61708063afe017f3e8d9/data/simulations/2SV0SJ_sims.plk -------------------------------------------------------------------------------- /data/simulations/2SV1SJ_sims.plk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmanzog/DeepLearningCreditRiskModeling/78487f0da054ea39e9fb61708063afe017f3e8d9/data/simulations/2SV1SJ_sims.plk -------------------------------------------------------------------------------- /data/simulations/HestonJump_sims.plk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmanzog/DeepLearningCreditRiskModeling/78487f0da054ea39e9fb61708063afe017f3e8d9/data/simulations/HestonJump_sims.plk -------------------------------------------------------------------------------- /data/simulations/Heston_sims.plk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmanzog/DeepLearningCreditRiskModeling/78487f0da054ea39e9fb61708063afe017f3e8d9/data/simulations/Heston_sims.plk -------------------------------------------------------------------------------- /data/simulations/KouJump_sims.plk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmanzog/DeepLearningCreditRiskModeling/78487f0da054ea39e9fb61708063afe017f3e8d9/data/simulations/KouJump_sims.plk -------------------------------------------------------------------------------- /data/simulations/Merton74basic_sims.plk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmanzog/DeepLearningCreditRiskModeling/78487f0da054ea39e9fb61708063afe017f3e8d9/data/simulations/Merton74basic_sims.plk -------------------------------------------------------------------------------- /data/simulations/Merton76jump_sims.plk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmanzog/DeepLearningCreditRiskModeling/78487f0da054ea39e9fb61708063afe017f3e8d9/data/simulations/Merton76jump_sims.plk -------------------------------------------------------------------------------- /data/simulations/PanSingleton2008_sims.plk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmanzog/DeepLearningCreditRiskModeling/78487f0da054ea39e9fb61708063afe017f3e8d9/data/simulations/PanSingleton2008_sims.plk -------------------------------------------------------------------------------- /data/trainedmodels/0SV1SJ_DNNWeights.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmanzog/DeepLearningCreditRiskModeling/78487f0da054ea39e9fb61708063afe017f3e8d9/data/trainedmodels/0SV1SJ_DNNWeights.h5 -------------------------------------------------------------------------------- /data/trainedmodels/1SV1SJ_DNNWeights.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmanzog/DeepLearningCreditRiskModeling/78487f0da054ea39e9fb61708063afe017f3e8d9/data/trainedmodels/1SV1SJ_DNNWeights.h5 -------------------------------------------------------------------------------- /data/trainedmodels/2SV0SJ_DNNWeights.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmanzog/DeepLearningCreditRiskModeling/78487f0da054ea39e9fb61708063afe017f3e8d9/data/trainedmodels/2SV0SJ_DNNWeights.h5 -------------------------------------------------------------------------------- /data/trainedmodels/2SV1SJ_DNNWeights.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmanzog/DeepLearningCreditRiskModeling/78487f0da054ea39e9fb61708063afe017f3e8d9/data/trainedmodels/2SV1SJ_DNNWeights.h5 -------------------------------------------------------------------------------- /data/trainedmodels/HestonJump_DNNWeights.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmanzog/DeepLearningCreditRiskModeling/78487f0da054ea39e9fb61708063afe017f3e8d9/data/trainedmodels/HestonJump_DNNWeights.h5 -------------------------------------------------------------------------------- /data/trainedmodels/Heston_DNNWeights.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmanzog/DeepLearningCreditRiskModeling/78487f0da054ea39e9fb61708063afe017f3e8d9/data/trainedmodels/Heston_DNNWeights.h5 -------------------------------------------------------------------------------- /data/trainedmodels/KouJump_DNNWeights.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmanzog/DeepLearningCreditRiskModeling/78487f0da054ea39e9fb61708063afe017f3e8d9/data/trainedmodels/KouJump_DNNWeights.h5 -------------------------------------------------------------------------------- /data/trainedmodels/Merton74basic_DNNWeights.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmanzog/DeepLearningCreditRiskModeling/78487f0da054ea39e9fb61708063afe017f3e8d9/data/trainedmodels/Merton74basic_DNNWeights.h5 -------------------------------------------------------------------------------- /data/trainedmodels/Merton76jump_DNNWeights.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmanzog/DeepLearningCreditRiskModeling/78487f0da054ea39e9fb61708063afe017f3e8d9/data/trainedmodels/Merton76jump_DNNWeights.h5 -------------------------------------------------------------------------------- /data/trainedmodels/PanSingleton2008_DNNWeights.h5: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmanzog/DeepLearningCreditRiskModeling/78487f0da054ea39e9fb61708063afe017f3e8d9/data/trainedmodels/PanSingleton2008_DNNWeights.h5 -------------------------------------------------------------------------------- /dnn_training/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmanzog/DeepLearningCreditRiskModeling/78487f0da054ea39e9fb61708063afe017f3e8d9/dnn_training/__init__.py -------------------------------------------------------------------------------- /dnn_training/dnn_trainer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright: 3 | Deep Learning Credit Risk Modeling 4 | Gerardo Manzo and Xiao Qiao 5 | The Journal of Fixed Income Fall 2021, jfi.2021.1.121; DOI: https://doi.org/10.3905/jfi.2021.1.121 6 | Disclaimer: 7 | THIS SOFTWARE IS PROVIDED "AS IS". YOU ARE USING THIS SOFTWARE AT YOUR OWN 8 | RISK. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 9 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 10 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY DIRECT, 11 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 12 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 13 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 14 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 15 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 16 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 17 | Description: 18 | DNN trainer 19 | """ 20 | import pandas as pd 21 | import numpy as np 22 | from dnn_training.dnn_utils import ScaleNormalize, LoadNeuralNetsModels 23 | from environment import FOLDERS 24 | import tensorflow as tf 25 | tf.config.experimental.set_visible_devices([], 'GPU') 26 | 27 | folderSims = FOLDERS['sims_models'] 28 | folderTrainedDNN = FOLDERS['trained_models'] 29 | 30 | class DNNTrainer(object): 31 | def __init__(self, model, maturities): 32 | """ 33 | Train DNN model 34 | :param model: str, one of the simulated models 35 | :param maturities: list of int, meturities 36 | """ 37 | self.model = model 38 | self.mats = maturities 39 | 40 | # load simulated data 41 | self.data = self.load_simulated_data() 42 | # prepare data to feed into DNN 43 | self.proc_data = self.data_processing(data=self.data) 44 | # load DNN model 45 | param_labels = self.data['param_labels'] 46 | self.dnn_generator = self.load_dnn_model(model=model, param_labels=param_labels) 47 | 48 | def load_simulated_data(self): 49 | """ 50 | Load simulated data 51 | :return: dict of simulated data 52 | """ 53 | sim_dat = pd.read_pickle(folderSims + self.model + "_sims.plk") 54 | if 'PanSingleton' in self.model: 55 | sim_dat.drop('leverage', inplace=True, axis=1) 56 | spread_cols = [i for i in sim_dat.columns if 'mat:' in i and int(i.split(':')[1]) in self.mats] 57 | spreads = sim_dat[spread_cols] 58 | 59 | pars_cols = [ i for i in sim_dat.columns if i not in spread_cols + ['maturity']] 60 | pars = sim_dat[pars_cols].values 61 | lb = sim_dat[pars_cols].min() 62 | ub = sim_dat[pars_cols].max() 63 | data = {'params': pars, 'lb': lb, 'ub': ub, 'spreads': spreads, 'param_labels': pars_cols} 64 | return data 65 | 66 | def data_processing(self, data): 67 | """ 68 | Prepare data for training DNN 69 | :param data: dict of simulated data 70 | :return: 71 | """ 72 | spreads = data['spreads'] 73 | pars = data['params'] 74 | lowerb = data['lb'] 75 | upperb = data['ub'] 76 | scaler = ScaleNormalize(y=spreads, x=pars, lb=lowerb, ub=upperb) 77 | # pars_train_transform = np.array([scaler.myscale(x) for x in scaler.X_train]) 78 | pars_train_transform = scaler.myscale(scaler.X_train) 79 | # pars_test_transform = np.array([scaler.myscale(x) for x in scaler.X_test]) 80 | pars_test_transform = scaler.myscale(scaler.X_test) 81 | proc_data = {'scaler': scaler, 82 | 'train_transf': pars_train_transform, 83 | 'test_transf': pars_test_transform} 84 | return proc_data 85 | 86 | def load_dnn_model(self, model, param_labels): 87 | """ 88 | load DNN model architecture 89 | :param model: str 90 | :return: 91 | """ 92 | Nmat = len(self.mats) 93 | loadNN = LoadNeuralNetsModels(params=param_labels, 94 | Nmat=Nmat, 95 | model=model) 96 | modelGEN = loadNN.modelGEN 97 | return modelGEN 98 | 99 | def fit(self, verbose=False): 100 | """ 101 | Train and save the DNN model 102 | :return: 103 | """ 104 | scaler = self.proc_data['scaler'] 105 | pars_train_transform = self.proc_data['train_transf'] 106 | pars_test_transform = self.proc_data['test_transf'] 107 | self.dnn_generator.fit(x=pars_train_transform, y=scaler.y_train_transform, 108 | batch_size=1024, 109 | validation_data=(pars_test_transform, scaler.y_test_transform), 110 | epochs=500, 111 | verbose=verbose, shuffle=1) 112 | self.dnn_generator.save_weights(folderTrainedDNN + '%s_DNNWeights.h5' % self.model) 113 | return print('Model %s has been trained' % self.model) 114 | 115 | def load_trained_weights(self): 116 | """ 117 | Load trained params 118 | :return: 119 | """ 120 | try: 121 | self.dnn_generator.load_weights(folderTrainedDNN + '%s_DNNWeights.h5' % self.model) 122 | except: 123 | raise ValueError('Model %s han not been trained yet' % self.model) 124 | 125 | NNParameters=[] 126 | for i in range(1, len(self.dnn_generator.layers)): 127 | NNParameters.append(self.dnn_generator.layers[i].get_weights()) 128 | return NNParameters 129 | 130 | def predict(self): 131 | """ 132 | Prediction using trained model 133 | :return: 134 | """ 135 | # load trained model 136 | scaler = self.proc_data['scaler'] 137 | pars_test_transform = self.proc_data['test_transf'] 138 | 139 | NNParameters = self.load_trained_weights() 140 | 141 | prediction = {} 142 | for k in range(scaler.X_test.shape[0]): 143 | pred = self.dnn_generator.predict(pars_test_transform[k][:, None].T)[0] 144 | prediction[k] = scaler.scale.inverse_transform(pred) 145 | 146 | cds_cols = self.data['spreads'].columns 147 | prediction = pd.DataFrame(prediction, index=cds_cols).T 148 | 149 | input_data = scaler.y_test.reset_index(drop=True) 150 | err2sum = np.sum((input_data - prediction) ** 2) 151 | y2sum = np.sum((input_data - input_data.mean()) ** 2) 152 | self.r2 = 1 - err2sum / y2sum 153 | print('Model r2: ') 154 | print(self.r2) 155 | return prediction 156 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /dnn_training/dnn_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright: 3 | Deep Learning Credit Risk Modeling 4 | Gerardo Manzo and Xiao Qiao 5 | The Journal of Fixed Income Fall 2021, jfi.2021.1.121; DOI: https://doi.org/10.3905/jfi.2021.1.121 6 | Disclaimer: 7 | THIS SOFTWARE IS PROVIDED "AS IS". YOU ARE USING THIS SOFTWARE AT YOUR OWN 8 | RISK. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 9 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 10 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY DIRECT, 11 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 12 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 13 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 14 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 15 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 16 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 17 | Description: 18 | Utils to train DNN 19 | """ 20 | import numpy as np 21 | from sklearn.model_selection import train_test_split 22 | from sklearn.preprocessing import StandardScaler 23 | from scipy.optimize import minimize, least_squares 24 | from keras import backend as K 25 | import keras 26 | 27 | class NumpyNN(object): 28 | def __init__(self, model_pars, NumLayers, NNParameters): 29 | """ 30 | Numpy DNN implemented for speed 31 | :param model_pars: parameters 32 | :param NumLayers: number of layers 33 | :param NNParameters: DNN trained parameters 34 | """ 35 | self.NumLayers = NumLayers 36 | self.NNParameters = NNParameters 37 | self.Npars = len(model_pars) 38 | 39 | 40 | def myscale(self, x): 41 | """ 42 | scaler 43 | :param x: 44 | :return: 45 | """ 46 | res = np.zeros(len(x)) 47 | for i in range(len(x)): 48 | res[i] = (x[i] - (self.ub[i] + self.lb[i]) * 0.5) * 2 / (self.ub[i] - self.lb[i]) 49 | return res 50 | 51 | def elu(self, x): 52 | # ELU activation func 53 | ind = (x < 0) 54 | x[ind] = np.exp(x[ind]) - 1 55 | return x 56 | 57 | def relu(self, x): 58 | # RELU activation func 59 | x = np.maximum(x, 0) 60 | return x 61 | 62 | def eluPrime(self, y): 63 | # we make a deep copy of input x 64 | x = np.copy(y) 65 | ind = (x < 0) 66 | x[ind] = np.exp(x[ind]) 67 | x[~ind] = 1 68 | return x 69 | 70 | def reluPrime(self, y): 71 | # we make a deep copy of input x 72 | x = np.copy(y) 73 | x = np.maximum(x, 0) 74 | return x 75 | 76 | def NeuralNetwork(self, x): 77 | input1 = x 78 | 79 | for i in range(self.NumLayers): 80 | input1 = np.dot(input1, self.NNParameters[i][0]) + self.NNParameters[i][1] 81 | # Elu activation 82 | input1 = self.relu(input1) 83 | # The output layer is linnear 84 | i += 1 85 | return np.dot(input1, self.NNParameters[i][0]) + self.NNParameters[i][1] 86 | 87 | def NeuralNetworkGradient(self, x): 88 | input1 = x 89 | 90 | # Identity Matrix represents Jacobian with respect to initial parameters 91 | grad = np.eye(len(input1)) 92 | # Propagate the gradient via chain rule 93 | for i in range(self.NumLayers): 94 | input1 = (np.dot(input1, self.NNParameters[i][0]) + self.NNParameters[i][1]) 95 | grad = (np.einsum('ij,jk->ik', grad, self.NNParameters[i][0])) 96 | # Elu activation 97 | grad *= self.reluPrime(input1) 98 | input1 = self.relu(input1) 99 | 100 | grad = np.einsum('ij,jk->ik', grad, self.NNParameters[i + 1][0]) 101 | # grad stores all intermediate Jacobians, however only the last one is used here as output 102 | return grad[:5, :] 103 | 104 | class ScaleNormalize(object): 105 | def __init__(self, y, x, lb, ub, test_size=0.05, random_state=42): 106 | """ 107 | Scaler for pricing model's parameters to be fed into DNN 108 | :param y: target 109 | :param x: features 110 | :param lb: params lower bounds 111 | :param ub: params upper bounds 112 | :param test_size: 113 | :param random_state: 114 | """ 115 | self.y = y 116 | self.x = x 117 | self.ub = ub 118 | self.lb = lb 119 | self.Npars = len(ub) 120 | self.test_size = test_size 121 | self.random_state = random_state 122 | 123 | self.train_split() 124 | 125 | self.scaler() 126 | 127 | def train_split(self): 128 | """ 129 | train split data 130 | :return: 131 | """ 132 | self.X_train, self.X_test, self.y_train, self.y_test = \ 133 | train_test_split(self.x, self.y, 134 | test_size=self.test_size, 135 | random_state=self.random_state) 136 | 137 | def scaler(self): 138 | """ 139 | scaler 140 | :return: 141 | """ 142 | self.scale = StandardScaler() 143 | self.scale2 = StandardScaler() 144 | self.y_train_transform = self.scale.fit_transform(self.y_train) 145 | self.y_test_transform = self.scale.transform(self.y_test) 146 | self.x_train_transform = self.scale2.fit_transform(self.X_train) 147 | self.x_test_transform = self.scale2.transform(self.X_test) 148 | 149 | def xinversetransform(self, x): 150 | return self.scale2.inverse_transform(x) 151 | 152 | def myscale_old(self, x): 153 | res = np.zeros(self.Npars) 154 | for i in range(self.Npars): 155 | res[i] = (x[i] - (self.ub[i] + self.lb[i]) * 0.5) * 2 / (self.ub[i] - self.lb[i]) 156 | return res 157 | 158 | def myscale(self, x): 159 | rb = (self.ub + self.lb) * 0.5 160 | rm = self.ub - self.lb 161 | res = (x - rb.values) * 2 / rm.values 162 | return res 163 | 164 | class Optimization(object): 165 | def __init__(self, paramlb, paramub, model_pars, NumLayers, NNParameters, sample_ind=500, method='Levenberg-Marquardt', 166 | x_test_transform=None): 167 | self.x_test_transform = x_test_transform 168 | self.sample_ind = sample_ind 169 | self.method = method 170 | self.init = np.zeros(len(paramlb)) 171 | self.lb = paramlb 172 | self.ub = paramub 173 | self.model_pars = model_pars 174 | self.NumLayers = NumLayers 175 | self.NNParameters = NNParameters 176 | 177 | self.NN = NumpyNN(model_pars, NumLayers, NNParameters) 178 | 179 | def optimize(self): 180 | solution = [] 181 | for i in range(5000): 182 | out = self.opt_method(i) 183 | print([i, out.x]) 184 | solution.append(self.myinverse(out.x)) 185 | return solution 186 | 187 | def opt_method(self, i): 188 | if self.method == "L-BFGS-B": 189 | opt = minimize(self.CostFunc, x0=self.init, args=i, method='L-BFGS-B', jac=self.Jacobian, tol=1E-10, 190 | options={"maxiter": 5000}) 191 | elif self.method == 'SLSQP': 192 | opt = minimize(self.CostFunc, x0=self.init, args=i, method='SLSQP', jac=self.Jacobian, tol=1E-10, 193 | options={"maxiter": 5000}) 194 | elif self.method == 'BFGS': 195 | opt = minimize(self.CostFunc, x0=self.init, args=i, method='BFGS', jac=self.Jacobian, tol=1E-10, 196 | options={"maxiter": 5000}) 197 | elif self.method == 'Levenberg-Marquardt': 198 | opt = least_squares(self.CostFuncLS, self.init, self.JacobianLS, args=(i,), gtol=1E-10) 199 | else: 200 | raise Warning('Methods in "L-BFGS-B ", "SLSQP", "BFGS", "Levenberg-Marquardt"') 201 | return opt 202 | 203 | def CostFunc(self, x, sample_ind): 204 | return np.sum(np.power((self.NN.NeuralNetwork(x) - self.x_test_transform[sample_ind]), 2)) 205 | 206 | def Jacobian(self, x, sample_ind): 207 | return 2 * np.sum((self.NN.NeuralNetwork(x) - self.x_test_transform[sample_ind]) * self.NeuralNetworkGradient(x), axis=1) 208 | 209 | def CostFuncLS(self, x, sample_ind): 210 | return (self.NN.NeuralNetwork(x) - self.x_test_transform[sample_ind]) 211 | 212 | def JacobianLS(self, x, sample_ind): 213 | return self.NN.NeuralNetworkGradient(x).T 214 | 215 | def myinverse(self, x): 216 | res = np.zeros(len(x)) 217 | for i in range(len(x)): 218 | res[i] = x[i] * (self.ub[i] - self.lb[i]) * 0.5 + (self.ub[i] + self.lb[i]) * 0.5 219 | return res 220 | 221 | class LoadNeuralNetsModels(object): 222 | 223 | def __init__(self, params, Nmat, model, nNodes=100, dropout=None, summary=True): 224 | """ 225 | Loads the architecture of DNN 226 | :param params: trained paramters 227 | :param Nmat: int, number of maturities 228 | :param model: str, models 229 | :param nNodes: int, number of nodes 230 | :param dropout: DNN dropout 231 | :param summary: True prints the DNN architecture 232 | """ 233 | self.model = model 234 | self.params = params 235 | self.Npars = len(params) 236 | self.Nmat = Nmat 237 | self.nNodes = nNodes 238 | self.dropout = dropout 239 | 240 | keras.backend.set_floatx('float64') 241 | 242 | self.modelGEN = self.load_model() 243 | 244 | if summary: 245 | self.modelGEN.summary() 246 | 247 | self.modelGEN.compile(loss=self.RMSE, optimizer="adam") 248 | 249 | def load_model(self): 250 | input1 = keras.layers.Input(shape=(self.Npars,)) 251 | 252 | if self.model in ['Merton74basic', 'Merton76jump', 'KouJump']: 253 | x1 = keras.layers.Dense(self.nNodes, activation='relu')(input1) 254 | x2 = keras.layers.Dense(self.nNodes, activation='relu')(x1) 255 | out_layer = keras.layers.Dense(self.Nmat, activation='linear')(x2) 256 | 257 | elif self.model in ['Heston', 'HestonJump', '1SV1SJ', '2SV1SJ', 258 | '0SV1SJ', '2SV0SJ', 'PanSingleton2008']: 259 | 260 | x1 = keras.layers.Dense(self.nNodes, activation='relu')(input1) 261 | x2 = keras.layers.Dense(self.nNodes, activation='relu')(x1) 262 | x3 = keras.layers.Dense(self.nNodes, activation='relu')(x2) 263 | out_layer = keras.layers.Dense(self.Nmat, activation='linear')(x3) 264 | else: 265 | raise Warning('Model is not specified correctly') 266 | 267 | modelGEN = keras.models.Model(inputs=input1, outputs=out_layer) 268 | return modelGEN 269 | 270 | def RMSE(self, y_true, y_pred): 271 | return K.sqrt(K.mean(K.square(y_pred - y_true))) 272 | 273 | 274 | 275 | -------------------------------------------------------------------------------- /dnn_training/master_trainer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright: 3 | Deep Learning Credit Risk Modeling 4 | Gerardo Manzo and Xiao Qiao 5 | The Journal of Fixed Income Fall 2021, jfi.2021.1.121; DOI: https://doi.org/10.3905/jfi.2021.1.121 6 | Disclaimer: 7 | THIS SOFTWARE IS PROVIDED "AS IS". YOU ARE USING THIS SOFTWARE AT YOUR OWN 8 | RISK. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 9 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 10 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY DIRECT, 11 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 12 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 13 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 14 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 15 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 16 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 17 | Description: 18 | DNN model training 19 | """ 20 | from dnn_trainer import DNNTrainer 21 | 22 | maturities = [1, 3, 5, 7, 10] 23 | 24 | Models = ['Merton74basic', 'Merton76jump', 'Heston', 'HestonJump', 25 | 'KouJump', '1SV1SJ', '2SV1SJ', '0SV1SJ', '2SV0SJ', 26 | 'PanSingleton2008'] 27 | Models = ['Merton74basic'] 28 | NNfits = {} 29 | for model in Models: 30 | print(model) 31 | dnnObj = DNNTrainer(model=model, maturities=maturities) 32 | # train model 33 | dnnObj.fit(verbose=True) 34 | # predictions 35 | preds = dnnObj.predict() 36 | # store accuracy 37 | NNfits[model] = dnnObj.r2 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /efficiencyTable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmanzog/DeepLearningCreditRiskModeling/78487f0da054ea39e9fb61708063afe017f3e8d9/efficiencyTable.png -------------------------------------------------------------------------------- /environment.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright: 3 | Deep Learning Credit Risk Modeling 4 | Gerardo Manzo and Xiao Qiao 5 | The Journal of Fixed Income Fall 2021, jfi.2021.1.121; DOI: https://doi.org/10.3905/jfi.2021.1.121 6 | Disclaimer: 7 | THIS SOFTWARE IS PROVIDED "AS IS". YOU ARE USING THIS SOFTWARE AT YOUR OWN 8 | RISK. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 9 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 10 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY DIRECT, 11 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 12 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 13 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 14 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 15 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 16 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 17 | Description: 18 | Environment folder 19 | """ 20 | 21 | import os 22 | 23 | metafolder = os.path.dirname(__file__) 24 | 25 | FOLDERS = {'market_data': metafolder + '/data/market_data/', 26 | 'sims_models': metafolder + '/data/simulations/', 27 | 'trained_models': metafolder + '/data/trainedmodels/', 28 | 'calibrated_models': metafolder + '/data/calibratedmodels/'} 29 | 30 | # create folders if not exist 31 | for fname, fdir in FOLDERS.items(): 32 | isExist = os.path.exists(fdir) 33 | if not isExist: 34 | 35 | # Create a new directory because it does not exist 36 | os.makedirs(fdir) 37 | print("The directory %s is created!" % fname) 38 | 39 | -------------------------------------------------------------------------------- /model_calibration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmanzog/DeepLearningCreditRiskModeling/78487f0da054ea39e9fb61708063afe017f3e8d9/model_calibration/__init__.py -------------------------------------------------------------------------------- /model_calibration/calib_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright: 3 | Deep Learning Credit Risk Modeling 4 | Gerardo Manzo and Xiao Qiao 5 | The Journal of Fixed Income Fall 2021, jfi.2021.1.121; DOI: https://doi.org/10.3905/jfi.2021.1.121 6 | Disclaimer: 7 | THIS SOFTWARE IS PROVIDED "AS IS". YOU ARE USING THIS SOFTWARE AT YOUR OWN 8 | RISK. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 9 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 10 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY DIRECT, 11 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 12 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 13 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 14 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 15 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 16 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 17 | Description: 18 | Model calibration to real data utils 19 | """ 20 | import numpy as np 21 | from pandas import DataFrame, Series 22 | from sklearn.preprocessing import StandardScaler 23 | from scipy.optimize import minimize, least_squares 24 | from dnn_training.dnn_utils import NumpyNN 25 | from joblib import Parallel, delayed 26 | import multiprocessing as mp 27 | from uk_dnn_filter import KalmanUKF 28 | 29 | class UnscentedKFLogLlk(object): 30 | def __init__(self, spreads, lev, mat, rf, dt, model, params, ub, lb, NNpars): 31 | """ 32 | PArallel calibration of a DNN trained model to real data using Unscented Kalman Filter 33 | :param spreads: spread data 34 | :param lev: leverage data 35 | :param mat: maturities 36 | :param rf: risk free 37 | :param dt: time fraction 38 | :param model: str, model 39 | :param param: model parameters 40 | :param ub: params upper bounds 41 | :param lb: params lower bounds 42 | :param NNpars: DNN trained parameters 43 | """ 44 | self.spreads = spreads 45 | self.model = model 46 | self.mat = mat 47 | self.lev = lev 48 | self.ub = ub 49 | self.lb = lb 50 | self.dt = dt 51 | self.rf = rf 52 | self.NNpars = NNpars 53 | self.x0, self.bounds, self.par_labels = self.set_param_bounds() 54 | self.params = params 55 | 56 | self.lb_se = [i[0] for i in self.bounds] 57 | self.ub_se = [i[1] for i in self.bounds] 58 | 59 | self.num_cores = mp.cpu_count() 60 | # num_cores = 12 61 | print(['num cores: ', self.num_cores]) 62 | 63 | 64 | 65 | def calib_output(self, kf_t): 66 | """ 67 | Extract output from parallelization and copmute performance metrics 68 | :param kf_t: 69 | :return: 70 | """ 71 | T = len(kf_t) 72 | N = self.spreads.shape[1] 73 | llk = 0 74 | states = [] 75 | fit_data = {} 76 | ss_res = 0 77 | for i in range(T): 78 | llk += kf_t[i].llk 79 | factors = kf_t[i].x_state[0] 80 | states.append(factors) 81 | ss_res += kf_t[i].err2 82 | fit_data[i] = kf_t[i].fit_data 83 | 84 | fit_data = DataFrame(fit_data, index=self.spreads.columns).T 85 | fit_data.index = self.spreads.index 86 | 87 | states = np.asarray(states) 88 | if states.ndim == 1: 89 | states = Series(states, index=self.spreads.index) 90 | else: 91 | states = DataFrame(states, index=self.spreads.index) 92 | 93 | r2, r2mat = self.r2(y=self.spreads, y_hat=fit_data, ss_res=ss_res) 94 | results = {'states': states, 'fittedvals': fit_data, 'sq_errs': ss_res, 95 | 'neg_llk': -llk / (T * N), 'r2': r2, 'r2byMat': r2mat} 96 | return results 97 | 98 | def r2(self, y, y_hat, ss_res): 99 | """ 100 | R-squared 101 | :param y: 102 | :param y_hat: 103 | :param ss_res: 104 | :return: 105 | """ 106 | err2sum = (y - y_hat).pow(2).sum() 107 | ss_tot = y.sub(y.mean()).pow(2).sum() 108 | r2mat = 1 - err2sum / ss_tot 109 | ss_tot = np.sum(ss_tot) 110 | r2 = 1 - ss_res / ss_tot 111 | return r2, r2mat 112 | 113 | def set_param_bounds(self): 114 | """ 115 | Set models bounds for optimization. Add here new models to calibrate 116 | :return: 117 | """ 118 | if self.model == 'Heston': 119 | x0 = np.array([1.34103376e-01, 1.03998288e-03, 1.49012263e-01, -9.36743934e-01, 120 | 1.60319430e-05]) 121 | bounds = ((0.001, 5), (0.001, 5), (0.001, 2), (-0.98, -0.05), (0.000001, 5)) 122 | par_labels = ['vol', 'kappa', 'theta', 'rho', 'sigma_eps'] 123 | 124 | elif self.model == 'PanSingleton2008': 125 | x0 = np.array([0.39267124, 0.03060086, -0.37510181, 0.00090748]) 126 | bounds = ((0.001, 5), (0.001, 2), (-7, 1), (0.000001, 5)) 127 | par_labels = ['vol', 'kappa', 'theta', 'sigma_eps'] 128 | else: 129 | raise ValueError('Model %s has not been implemented' % self.model) 130 | return x0, bounds, par_labels 131 | 132 | def negllk(self, params=None): 133 | params = params if params is not None else self.params 134 | params = np.maximum(np.minimum(params, self.ub_se), self.lb_se) 135 | scaler = StandardScaler() 136 | scaler.fit_transform(self.spreads) 137 | 138 | T, N = self.spreads.shape 139 | 140 | with Parallel(n_jobs=self.num_cores) as parallel: 141 | kf_t = parallel(delayed(KalmanUKF)(idx=_t, CDS=self.spreads.iloc[_t], lev=self.lev.iloc[_t], 142 | mat=self.mat, rf=self.rf, dt=self.dt, model=self.model, 143 | params=params, NNpars=self.NNpars, scaler=scaler, 144 | ub=self.ub, lb=self.lb) 145 | for _t in range(T)) 146 | 147 | results = self.calib_output(kf_t=kf_t) 148 | # self.fittedvals = results['fittedvals'] 149 | # self.states = results['states'] 150 | neg_llk = results['neg_llk'].flatten()[0] 151 | # self.ss_res = results['sq_errs'] 152 | # self.r2mat = results['r2byMat'] 153 | r2 = results['r2'] 154 | print({'r2': r2, 'negllk': neg_llk, 'params': params.tolist()}) 155 | return neg_llk, results 156 | 157 | 158 | 159 | class ModelCalibrator(object): 160 | def __init__(self, obsData, paramlb, paramub, model_pars, NumLayers, NNParameters, sample_ind=500, method='Levenberg-Marquardt', 161 | x_test_transform=None, obsParams=None): 162 | """ 163 | Calibration of a model to real data 164 | :param obsData: Series of observed data 165 | :param paramlb: parameters lower bounds 166 | :param paramub: parameters upper bounds 167 | :param model_pars: 168 | :param NumLayers: int, number of DNN layers 169 | :param NNParameters: trained DNN parameters 170 | :param sample_ind: 171 | :param method: 172 | :param x_test_transform: 173 | :param obsParams: 174 | """ 175 | 176 | self.scale = StandardScaler() 177 | self.scaled_data = DataFrame(self.scale.fit_transform(obsData)) 178 | 179 | self.x_test_transform = x_test_transform 180 | self.sample_ind = sample_ind 181 | self.method = method 182 | self.lb = paramlb.values 183 | self.ub = paramub.values 184 | self.model_pars = model_pars 185 | self.NumLayers = NumLayers 186 | self.NNParameters = NNParameters 187 | self.obsData = obsData 188 | self.dates = obsData.index 189 | self.obsParams = obsParams 190 | Npars = len(paramlb) 191 | if obsParams is not None: 192 | Npars -= obsParams.shape[1] 193 | 194 | self.NN = NumpyNN(model_pars, NumLayers, NNParameters) 195 | 196 | def optimize(self): 197 | x0 = np.array([0.02, 0.2, 0.06, 0.23, -.5]) 198 | solution = {} 199 | data_fit = {} 200 | window = 52 201 | for t in range(window, len(self.dates)): 202 | scale = StandardScaler() 203 | obs_data = self.obsData.loc[:t] 204 | scaled_data = scale.fit_transform(obs_data) 205 | # optimize Heston params 206 | out = self.opt_method(t, x0, obs_data=scaled_data[-1, :], obsparam=self.obsParams.loc[t]) 207 | 208 | # scale the observed inputs 209 | scaled_obs_inputs = self.myscale(self.obsParams.loc[t], ub=self.ub[-1], lb=self.lb[-1]) 210 | # store optimal params (that are scaled) and the (scaled) observed inputs 211 | opt_inputs = np.insert(out.x, len(out.x), scaled_obs_inputs) 212 | solution[t] = self.myinverse(opt_inputs) 213 | print([t, solution[t]]) 214 | _fit = self.NN.NeuralNetwork(opt_inputs) 215 | data_fit[t] = scale.inverse_transform(_fit) 216 | # data_fit[t] = _fit * self.scale.scale_ + self.scale.mean_ 217 | print(np.corrcoef(data_fit[t], self.obsData.loc[t])[0, 1]) 218 | # solution.append(out.x) 219 | return DataFrame(solution).T, DataFrame(data_fit).T 220 | 221 | 222 | def opt_method(self, i, x0, obs_data, obsparam): 223 | """ 224 | 'Methods in "L-BFGS-B ", "SLSQP", "BFGS", "Levenberg-Marquardt"' 225 | :param i: 226 | :param x0: 227 | :param obs_data: 228 | :param obsparam: 229 | :return: 230 | """ 231 | if self.method == 'Levenberg-Marquardt': 232 | lb = [-0.99] * 5 233 | ub = [0.99] * 5 234 | opt = least_squares(self.CostFuncLS, x0, self.JacobianLS, args=(i, obs_data, obsparam), 235 | gtol=1E-10, bounds=(lb, ub)) 236 | else: 237 | opt = minimize(self.CostFunc, x0=x0, args=(i, obs_data, obsparam), method=self.method, jac=self.Jacobian, 238 | tol=1E-10, 239 | options={"maxiter": 5000}, bounds=((-.99, .99), )*5) 240 | 241 | return opt 242 | 243 | def CostFunc(self, x, idx, obs_data, obsparam): 244 | """ 245 | Cost function: MSE 246 | :param x: 247 | :param idx: 248 | :param obs_data: 249 | :param obsparam: 250 | :return: 251 | """ 252 | x = np.insert(x, len(x), obsparam) 253 | err = self.NN.NeuralNetwork(x) - obs_data 254 | return np.sum(err ** 2) 255 | 256 | def Jacobian(self, x, idx, obs_data, obsparam): 257 | """ 258 | DNN Jacobian 259 | :param x: 260 | :param idx: 261 | :param obs_data: 262 | :param obsparam: 263 | :return: 264 | """ 265 | x = np.insert(x, len(x), obsparam) 266 | 267 | err = self.NN.NeuralNetwork(x) - obs_data 268 | grad = self.NN.NeuralNetworkGradient(x) 269 | return 2 * np.sum(err * grad, axis=1) 270 | 271 | def CostFuncLS(self, x, idx, obs_data, obsparam): 272 | x = np.insert(x, len(x), obsparam) 273 | return (self.NN.NeuralNetwork(x) - obs_data) 274 | 275 | def JacobianLS(self, x, idx, obs_data, obsparam): 276 | x = np.insert(x, len(x), obsparam) 277 | return self.NN.NeuralNetworkGradient(x).T 278 | 279 | def myinverse(self, x): 280 | res = np.zeros(len(x)) 281 | for i in range(len(x)): 282 | res[i] = x[i] * (self.ub[i] - self.lb[i]) * 0.5 + (self.ub[i] + self.lb[i]) * 0.5 283 | return res 284 | 285 | def myscale(self, x, ub, lb): 286 | res = (2 * x - (ub + lb)) / (ub - lb) 287 | return res 288 | -------------------------------------------------------------------------------- /model_calibration/master_calibration.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright: 3 | Deep Learning Credit Risk Modeling 4 | Gerardo Manzo and Xiao Qiao 5 | The Journal of Fixed Income Fall 2021, jfi.2021.1.121; DOI: https://doi.org/10.3905/jfi.2021.1.121 6 | Disclaimer: 7 | THIS SOFTWARE IS PROVIDED "AS IS". YOU ARE USING THIS SOFTWARE AT YOUR OWN 8 | RISK. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 9 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 10 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY DIRECT, 11 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 12 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 13 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 14 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 15 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 16 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 17 | Description: 18 | Model calibration to real data 19 | """ 20 | import pandas as pd 21 | from environment import FOLDERS 22 | from calib_utils import UnscentedKFLogLlk 23 | from dnn_training.dnn_trainer import DNNTrainer 24 | from scipy.optimize import minimize 25 | import pickle 26 | 27 | # Set folders 28 | folderSims = FOLDERS['sims_models'] 29 | folderTrainedPars = FOLDERS['trained_models'] 30 | folderMktData = FOLDERS['market_data'] 31 | folderCalib = FOLDERS['calibrated_models'] 32 | 33 | Models = ['PanSingleton2008', 'Heston', 'Heston_mktLev'] 34 | maturities = [1, 3, 5, 7, 10] 35 | 36 | # Load market data and resample weekly 37 | data = pd.read_excel(folderMktData + "market_data.xlsx").dropna() 38 | data = data.set_index('Unnamed: 0').resample('W').last() 39 | ycols = [i for i in data.columns if 'spread' in i] 40 | spreads = data[ycols] 41 | 42 | method = 'SLSQP' 43 | options = {'eps': 1e-12} 44 | 45 | for model in Models: 46 | # load leverage 47 | lev = data[['mkt_leverage']] if 'mktLev' in model else data[['leverage']] 48 | 49 | print(model) 50 | dnnObj = DNNTrainer(model=model, maturities=maturities) 51 | 52 | simdata = dnnObj.data 53 | # collect min/max for each param 54 | lb = simdata['lb'] 55 | ub = simdata['ub'] 56 | 57 | # load trained params 58 | NNParameters = dnnObj.load_trained_weights() 59 | 60 | NumLayers = 3 61 | dt = 1 / 52 62 | NNpars = {'NumLayers': NumLayers, 'NNParameters': NNParameters} 63 | llkObj = UnscentedKFLogLlk(spreads=spreads, lev=lev, mat=maturities, 64 | rf=0, dt=dt, model=model, 65 | params=None, ub=ub, lb=lb, NNpars=NNpars) 66 | 67 | obj = lambda x: llkObj.negllk(params=x)[0] 68 | x0 = llkObj.x0 69 | bounds = llkObj.bounds 70 | 71 | opt = minimize(obj, x0=x0, method=method, tol=1e-8, bounds=bounds, options=options) 72 | opt_pars = opt.x 73 | print(opt.x) 74 | 75 | print('------------Optimal Pars') 76 | print(opt_pars) 77 | optObj = UnscentedKFLogLlk(spreads=spreads, lev=lev, mat=maturities, 78 | rf=0, dt=dt, model=model, 79 | params=None, ub=ub, lb=lb, NNpars=NNpars) 80 | llk, results = optObj.negllk(params=opt_pars) 81 | f = open(folderCalib + "%s_calib_output.pkl" % model, "wb") 82 | pickle.dump(results, f) 83 | f.close() 84 | -------------------------------------------------------------------------------- /model_calibration/uk_dnn_filter.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright: 3 | Deep Learning Credit Risk Modeling 4 | Gerardo Manzo and Xiao Qiao 5 | The Journal of Fixed Income Fall 2021, jfi.2021.1.121; DOI: https://doi.org/10.3905/jfi.2021.1.121 6 | Disclaimer: 7 | THIS SOFTWARE IS PROVIDED "AS IS". YOU ARE USING THIS SOFTWARE AT YOUR OWN 8 | RISK. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 9 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 10 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY DIRECT, 11 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 12 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 13 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 14 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 15 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 16 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 17 | Description: 18 | Model calibration to real data utils 19 | """ 20 | from filterpy.kalman import unscented_transform, MerweScaledSigmaPoints 21 | import pandas as pd 22 | import numpy as np 23 | 24 | 25 | pd.set_option('display.width', 500) 26 | pd.set_option('display.max_columns', 500) 27 | 28 | def nearPSD(A, epsilon=0): 29 | n = A.shape[0] 30 | eigval, eigvec = np.linalg.eig(A) 31 | val = np.matrix(np.maximum(eigval, epsilon)) 32 | vec = np.matrix(eigvec) 33 | T = 1 / (np.multiply(vec, vec) * val.T) 34 | T = np.matrix(np.sqrt(np.diag(np.array(T).reshape((n))))) 35 | B = T * vec * np.diag(np.array(np.sqrt(val)).reshape((n))) 36 | out = B * B.T 37 | return out 38 | 39 | class SpreadNN(object): 40 | def __init__(self, Npars, NumLayers, NNParameters): 41 | self.NumLayers = NumLayers 42 | self.NNParameters = NNParameters 43 | self.Npars = Npars 44 | 45 | def elu(self, x): 46 | # Careful function ovewrites x 47 | ind = (x < 0) 48 | x[ind] = np.exp(x[ind]) - 1 49 | return x 50 | 51 | def relu(self, x): 52 | # Careful function ovewrites x 53 | x = np.maximum(x, 0) 54 | return x 55 | 56 | def eluPrime(self, y): 57 | # we make a deep copy of input x 58 | x = np.copy(y) 59 | ind = (x < 0) 60 | x[ind] = np.exp(x[ind]) 61 | x[~ind] = 1 62 | return x 63 | 64 | def reluPrime(self, y): 65 | # we make a deep copy of input x 66 | x = np.copy(y) 67 | x = np.maximum(x, 0) 68 | return x 69 | 70 | def NeuralNetwork(self, x): 71 | input1 = x 72 | for i in range(self.NumLayers): 73 | input1 = np.dot(input1, self.NNParameters[i][0]) + self.NNParameters[i][1] 74 | # Elu activation 75 | input1 = self.relu(input1) 76 | # The output layer is linnear 77 | i += 1 78 | return np.dot(input1, self.NNParameters[i][0]) + self.NNParameters[i][1] 79 | 80 | def NeuralNetworkGradient(self, x): 81 | input1 = x 82 | # Identity Matrix represents Jacobian with respect to initial parameters 83 | grad = np.eye(len(input1)) 84 | # Propagate the gradient via chain rule 85 | for i in range(self.NumLayers): 86 | input1 = (np.dot(input1, self.NNParameters[i][0]) + self.NNParameters[i][1]) 87 | grad = (np.einsum('ij,jk->ik', grad, self.NNParameters[i][0])) 88 | # Elu activation 89 | grad *= self.reluPrime(input1) 90 | input1 = self.relu(input1) 91 | grad = np.einsum('ij,jk->ik', grad, self.NNParameters[i + 1][0]) 92 | # grad stores all intermediate Jacobians, however only the last one is used here as output 93 | return grad[:5, :] 94 | 95 | class KalmanUKF(object): 96 | def __init__(self, idx, CDS, lev, mat, rf, dt, model, params, NNpars, scaler, ub, lb): 97 | # print(idx) 98 | self.idx = idx 99 | self.lev = lev 100 | self.mat = mat 101 | self.rf = rf 102 | self.model = model 103 | self.params = params 104 | self.scaler = scaler 105 | self.ub = ub 106 | self.lb = lb 107 | 108 | self.dt = dt 109 | 110 | # Initiliaze Trained Neural Network 111 | Npars = len(params) - 1 # -1 because model params + measurement error 112 | NumLayers = NNpars['NumLayers'] 113 | NNParameters = NNpars['NNParameters'] 114 | self.NNpricing = SpreadNN(Npars=Npars, NumLayers=NumLayers, NNParameters=NNParameters) 115 | 116 | # scaled_data = scaler.fit_transform(CDS) 117 | self.CDS = CDS#.iloc[-1] 118 | 119 | self.get_params() 120 | 121 | self.llk, self.x_state, self.err2, self.fit_data = self.UKFfit() 122 | 123 | def myinverse(self, x, ub, lb): 124 | res = x * (ub - lb) * .5 + (ub + lb) * .5 125 | return res 126 | 127 | def myscale(self, x, ub, lb): 128 | res = (2 * x - (ub + lb)) / (ub - lb) 129 | return res 130 | 131 | def measur_eq(self, state_sigmas, params): 132 | # scaled_lev = self.myscale(x=self.lev, ub=0.01, lb=0.9) 133 | if self.model == 'Heston': 134 | parsNN = np.concatenate([state_sigmas, params * np.ones((3, 1)), self.lev.values * np.ones((3, 1))], axis=1) 135 | elif self.model == 'PanSingleton2008': 136 | parsNN = np.concatenate([state_sigmas, params * np.ones((3, 1))], axis=1) 137 | 138 | scaled_parsNN = self.myscale(parsNN, self.ub.values, self.lb.values) 139 | NN_fit = self.NNpricing.NeuralNetwork(x=scaled_parsNN * np.ones((3, 1))) 140 | sigmas_measur = self.scaler.inverse_transform(NN_fit) 141 | return sigmas_measur 142 | 143 | 144 | def UKFfit(self): 145 | n_pars_pricing = len(self.params) - 1 146 | n_factors = len(self.factors) 147 | nCDS = len(self.CDS) 148 | # state equation: mean and variance 149 | if self.model == 'Heston': 150 | F = np.exp(-self.kappa * self.dt) 151 | F2 = np.exp(-2 * self.kappa * self.dt) 152 | state_eq = lambda ff0: self.theta * (1 - F) + np.maximum(ff0, 0.00001) * F 153 | var_state_eq = lambda ff0: ff0 * self.vol ** 2 / self.kappa * (F - F2) + \ 154 | self.theta * self.vol ** 2 / (2 * self.kappa) * (1 - F) ** 2 155 | elif self.model == 'PanSingleton2008': 156 | F2 = np.exp(-2 * self.kappa * self.dt) 157 | F = np.exp(-self.kappa * self.dt) 158 | state_eq = lambda ff0: np.exp(np.log(ff0) * np.exp(-self.kappa * self.dt) + self.theta / self.kappa * (1 - F)) 159 | var_state_eq = lambda ff0: self.vol ** 2 / (2 * self.kappa) * (1 - F2) 160 | 161 | # initial mean and cov of the state: assume homoskedasticity across states 162 | x_state = self.factors 163 | P_state = self.vol ** 2 164 | 165 | R = np.eye(nCDS) * self.sigma_err 166 | 167 | points = MerweScaledSigmaPoints(n=n_factors, alpha=1e-3, beta=2., kappa=3) 168 | Wm, Wc = points.Wm, points.Wc 169 | 170 | err_iter = [] 171 | state_iter = [] 172 | cds_fit_iter = [] 173 | ukf_cov_cds_iter = [] 174 | err_sum_iter = [] 175 | for _tt in range(5): 176 | # print(_tt) 177 | cds_tt = self.CDS 178 | r_t = self.rf 179 | x_state = state_eq(x_state) 180 | P_state = F ** 2 * P_state + var_state_eq(x_state) 181 | P_state = np.maximum(P_state, 0.0001) 182 | 183 | try: 184 | state_sigmas = points.sigma_points(x_state, P_state) 185 | except: 186 | print('PROBLEM here') 187 | P_state = nearPSD(P_state) 188 | state_sigmas = points.sigma_points(x_state, P_state) 189 | # number of sigma points is 2n+1 190 | self.n_sigma_point = 2 * n_factors + 1 191 | 192 | # use unscented transform to get new mean and covariance 193 | sigmas_measur = self.measur_eq(state_sigmas, params=self.params[:n_pars_pricing]) 194 | 195 | 196 | # use unscented transform to get new mean and covariance 197 | ukf_mean_cds, ukf_cov_cds = unscented_transform(sigmas_measur, Wm, Wc, noise_cov=R) 198 | cds_fit_iter.append(ukf_mean_cds) 199 | ukf_cov_cds_iter.append(ukf_cov_cds) 200 | 201 | # Innovations 202 | err = (cds_tt - ukf_mean_cds) 203 | # print([_tt, np.sum(err ** 2)]) 204 | err_sum_iter.append(np.sum(err ** 2)) 205 | # print(np.sum(err ** 2)) 206 | err_iter.append(err) 207 | 208 | # compute cross variance of the state and the measurements 209 | Pxz = np.zeros((n_factors, nCDS)) 210 | for i in range(self.n_sigma_point): 211 | Pxz += Wc[i] * np.outer(state_sigmas[i] - x_state, 212 | sigmas_measur[i] - ukf_mean_cds) 213 | # Kalman gain 214 | inv_ukf_cov_cds = np.linalg.inv(ukf_cov_cds) 215 | K = np.dot(Pxz, inv_ukf_cov_cds) 216 | 217 | x_state = np.maximum(x_state + np.dot(K, err), 0.00001) 218 | state_iter.append(x_state) 219 | 220 | P_state = P_state - np.dot(K, ukf_cov_cds).dot(K.T) 221 | 222 | min_vals = np.argmin(err_sum_iter) 223 | min_err = err_iter[min_vals][:, None] 224 | min_ukf_cov_y = ukf_cov_cds_iter[min_vals] 225 | fit_data = cds_fit_iter[min_vals] 226 | inv_ukf_cov_cds = np.linalg.inv(min_ukf_cov_y) 227 | det = np.linalg.det(min_ukf_cov_y) 228 | err2_ = np.dot(np.dot(min_err.T, inv_ukf_cov_cds), min_err) 229 | llk = -.5 * nCDS * np.log(2 * np.pi) -.5 * np.log(det) - .5 * err2_ 230 | x_state = state_iter[min_vals] 231 | ukf_mean_cds = cds_fit_iter[min_vals] 232 | minErr = err_sum_iter[min_vals] 233 | return llk, x_state, minErr, fit_data 234 | 235 | def get_params(self): 236 | if self.model == 'Heston': 237 | pars = ['vol', 'kappa', 'theta', 'rho'] 238 | 239 | elif self.model == 'PanSingleton2008': 240 | pars = ['vol', 'kappa', 'theta'] 241 | pars += ['sigma_err'] 242 | for name, val, in zip(pars, self.params): 243 | setattr(self, name, val) 244 | f0 = np.exp(self.theta) if self.model == 'PanSingleton2008' else self.theta 245 | self.factors = np.array([f0]) 246 | 247 | 248 | 249 | 250 | 251 | 252 | -------------------------------------------------------------------------------- /model_simulator/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gmanzog/DeepLearningCreditRiskModeling/78487f0da054ea39e9fb61708063afe017f3e8d9/model_simulator/__init__.py -------------------------------------------------------------------------------- /model_simulator/charafun_pricing.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright: 3 | Deep Learning Credit Risk Modeling 4 | Gerardo Manzo and Xiao Qiao 5 | The Journal of Fixed Income Fall 2021, jfi.2021.1.121; DOI: https://doi.org/10.3905/jfi.2021.1.121 6 | Disclaimer: 7 | THIS SOFTWARE IS PROVIDED "AS IS". YOU ARE USING THIS SOFTWARE AT YOUR OWN 8 | RISK. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 9 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 10 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY DIRECT, 11 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 12 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 13 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 14 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 15 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 16 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 17 | Description: 18 | Characteristic function 19 | """ 20 | import numpy as np 21 | from pandas import DataFrame 22 | from scipy import optimize 23 | from scipy.interpolate import interp1d 24 | import numpy.matlib 25 | import multiprocessing as mp 26 | 27 | 28 | class CharFun(object): 29 | def __init__(self, u, A0, param, rf, q, tau, model_type, state0=None): 30 | """ 31 | Characteristic functions for different model_type 32 | :param u: points of the integrand 33 | :param A0: asset value at 0, scalar 34 | :param param: dict of parameters. E.g. {'sigma': .2, 'lambd': .5}. See each charac for param names 35 | :param rf: annualized risk free rate 36 | :param q: annualized dividend yield 37 | :param tau: time to maturity 38 | :param model_type: one of Heston, HestonJump, KouJump, BlackScholes, BlackScholesJump 39 | """ 40 | self.param = param 41 | self.tau = tau 42 | self.rf = rf 43 | self.q = q 44 | self.u = u 45 | self.model_type = model_type 46 | self.x0 = A0 47 | self.state0 = state0 48 | 49 | self.cf = self.charfun() 50 | 51 | def heston(self): 52 | """ 53 | Heston basic and Heston with Jump 54 | 'Heston' 55 | v0 = spot variance level 56 | kappa = mean reversion speed 57 | theta = mean interest rate level 58 | sigma = volatility of variance 59 | rho = correlation between innovations in variance and spot process 60 | 61 | 'HestonJump': Heston with jumps. Requires 'Heston' and the following: 62 | muJ = expected jump size 63 | sigmaJ = jump volatility 64 | lambd = jump intensity 65 | """ 66 | try: 67 | kappa = self.param['kappa_var'] 68 | theta = self.param['theta_var'] 69 | sigma = self.param['sigma_var'] 70 | rho = self.param['rho'] 71 | v0 = self.param['v0'] 72 | except: 73 | kappa, theta, sigma, rho = self.param 74 | v0 = self.state0 75 | 76 | u = self.u 77 | 78 | a = -.5 * u * 1j * (1 - u * 1j) 79 | b = rho * sigma * u * 1j - kappa 80 | c = .5 * sigma ** 2 81 | m = -np.sqrt(b ** 2 - 4 * a * c) 82 | 83 | emt = np.exp(m * self.tau) 84 | beta2 = (m - b) / sigma ** 2 * (1 - emt) / (1 - (b - m) / (b + m) * emt) 85 | alpha = (self.rf - self.q) * u * 1j * self.tau + \ 86 | kappa * theta * (m - b) / sigma ** 2 * self.tau + \ 87 | kappa * theta / c * np.log((2 * m) / ((m - b) * emt + b + m)) 88 | phi = alpha + u * 1j * np.log(self.x0) + beta2 * v0 89 | return np.exp(phi) 90 | 91 | def hestonJump(self): 92 | """ 93 | Heston basic and Heston with Jump 94 | 'Heston' 95 | v0 = spot variance level 96 | kappa = mean reversion speed 97 | theta = mean interest rate level 98 | sigma = volatility of variance 99 | rho = correlation between innovations in variance and spot process 100 | 101 | 'HestonJump': Heston with jumps. Requires 'Heston' and the following: 102 | muJ = expected jump size 103 | sigmaJ = jump volatility 104 | lambd = jump intensity 105 | """ 106 | u = self.u 107 | 108 | lambd = self.param['lambd'] 109 | muJ = self.param['muJ'] 110 | sigmaJ = self.param['sigmaJ'] 111 | 112 | compensator = np.exp(muJ + .5 * sigmaJ ** 2) - 1 113 | compensator_u1j = np.exp(1j * u * muJ - u ** 2 * .5 * sigmaJ ** 2) - 1 114 | 115 | phi_jump = lambd * (compensator_u1j - u * 1j * compensator) 116 | 117 | phi_Heston = self.heston() 118 | phi = phi_Heston * np.exp(phi_jump * self.tau) 119 | return phi 120 | 121 | def heston_stochastic_jump(self): 122 | """ 123 | Heston basic and Heston with Jump 124 | 'Heston' 125 | v0 = spot variance level 126 | kappa = mean reversion speed 127 | theta = mean interest rate level 128 | sigma = volatility of variance 129 | rho = correlation between innovations in variance and spot process 130 | 131 | 'HestonJump': Heston with jumps. Requires 'Heston' and the following: 132 | muJ = expected jump size 133 | sigmaJ = jump volatility 134 | lambd = jump intensity 135 | """ 136 | # param for stochastic vol 137 | v0 = self.param['v0'] 138 | eta_var = self.param['sigma_var'] 139 | k_var = self.param['kappa_var'] 140 | theta_var = self.param['theta_var'] 141 | rho = self.param['rho'] 142 | 143 | # param for stochastic jump 144 | j0 = self.param['j0'] 145 | eta_jump = self.param['sigma_jump'] 146 | k_jump = self.param['kappa_jump'] 147 | theta_jump = self.param['theta_jump'] 148 | muJ = self.param['muJ'] 149 | sigmaJ = self.param['sigmaJ'] 150 | 151 | u = self.u 152 | xi_jump = np.exp(muJ + .5 * sigmaJ ** 2) - 1 153 | phi_jump = 1 - np.exp(1j * u * muJ - .5 * u ** 2 * sigmaJ ** 2) 154 | 155 | # vol_1 factor 156 | cV1 = k_var - 1j * u * rho * eta_var 157 | 158 | dV1 = np.sqrt(cV1 ** 2 + (1j * u + u ** 2) * eta_var ** 2) 159 | fV1 = np.divide(cV1 - dV1, cV1 + dV1) 160 | 161 | ge = np.exp(-dV1 * self.tau) 162 | gf = np.multiply(fV1, ge) 163 | 164 | CV1 = np.multiply((cV1 - dV1) / eta_var ** 2, np.divide(1 - ge, 1 - gf)) 165 | BV1 = (k_var * theta_var) / (eta_var ** 2) * ( 166 | (cV1 - dV1) * self.tau - 2 * np.log(np.divide(1 - gf, 1 - fV1))) 167 | 168 | # jump factor 169 | d_jump = np.sqrt(k_jump ** 2 + (1j * u * xi_jump + phi_jump) * 2 * eta_jump ** 2) 170 | f_jump = np.divide(k_jump - d_jump, k_jump + d_jump) 171 | 172 | ge_j = np.exp(-d_jump * self.tau) 173 | gf_j = np.multiply(f_jump, ge_j) 174 | 175 | C_jump = (k_jump - d_jump) / (eta_jump ** 2) * np.divide(1 - ge_j, 1 - gf_j) 176 | B_jump = (k_jump * theta_jump) / (eta_jump ** 2) * ((k_jump - d_jump) * self.tau - 2 * np.log( 177 | np.divide(1 - gf_j, 1 - f_jump))) 178 | 179 | drift_psi = 1j * u * (self.rf - self.q) 180 | 181 | phi = drift_psi * self.tau + BV1 + CV1 * v0 + B_jump + C_jump * j0 + u * 1j * np.log(self.x0) 182 | return np.exp(phi) 183 | 184 | def most_complex_model(self): 185 | """ 186 | Heston basic and Heston with Jump 187 | 'Heston' 188 | v0 = spot variance level 189 | kappa = mean reversion speed 190 | theta = mean interest rate level 191 | sigma = volatility of variance 192 | rho = correlation between innovations in variance and spot process 193 | 194 | 'HestonJump': Heston with jumps. Requires 'Heston' and the following: 195 | muJ = expected jump size 196 | sigmaJ = jump volatility 197 | lambd = jump intensity 198 | """ 199 | skipV1 = skipV2 = skipZ = False 200 | # param for stochastic vol 1 201 | try: 202 | v01 = self.param['v01'] 203 | eta_var1 = self.param['sigma_var1'] 204 | k_var1 = self.param['kappa_var1'] 205 | theta_var1 = self.param['theta_var1'] 206 | rho1 = self.param['rho1'] 207 | except: 208 | skipV1 = True 209 | v01 = eta_var1 = k_var1 = theta_var1 = rho1 = 0 210 | 211 | # param for stochastic vol 2 212 | try: 213 | v02 = self.param['v02'] 214 | eta_var2 = self.param['sigma_var2'] 215 | k_var2 = self.param['kappa_var2'] 216 | theta_var2 = self.param['theta_var2'] 217 | rho2 = self.param['rho2'] 218 | except: 219 | skipV2 = True 220 | v02 = eta_var2 = k_var2 = theta_var2 = rho2 = 0 221 | 222 | # param for stochastic jump 223 | try: 224 | z0 = self.param['z0'] 225 | eta_z = self.param['sigma_z'] 226 | k_z = self.param['kappa_z'] 227 | theta_z = self.param['theta_z'] 228 | muJ = self.param['muJ'] 229 | sigmaJ = self.param['sigmaJ'] 230 | except: 231 | skipZ = True 232 | z0 = eta_z = k_z = theta_z = muJ = sigmaJ = 0 233 | 234 | try: 235 | a_Var = self.param['a_Var'] 236 | except: 237 | a_Var = 0 238 | 239 | # j0 = a * (v01 + v02) + z0 240 | 241 | u = self.u 242 | xi_jump = np.exp(muJ + .5 * sigmaJ ** 2) - 1 243 | phi_jump = 1 - np.exp(1j * u * muJ - .5 * u ** 2 * sigmaJ ** 2) 244 | 245 | # vol_1 factor 246 | if not skipV1: 247 | cV1 = k_var1 - 1j * u * rho1 * eta_var1 248 | 249 | # dV1 = np.sqrt(cV1 ** 2 + (1j * u + u ** 2) * eta_var1 ** 2) 250 | dV1 = np.sqrt(cV1 ** 2 + (1j * u + u ** 2) * eta_var1 ** 2 + 251 | (1j * u * xi_jump + phi_jump) * a_Var * 2 * eta_var1 ** 2) 252 | fV1 = np.divide(cV1 - dV1, cV1 + dV1) 253 | 254 | ge1 = np.exp(-dV1 * self.tau) 255 | gf1 = np.multiply(fV1, ge1) 256 | 257 | CV1 = np.multiply((cV1 - dV1) / eta_var1 ** 2, np.divide(1 - ge1, 1 - gf1)) 258 | BV1 = (k_var1 * theta_var1) / (eta_var1 ** 2) * ( 259 | (cV1 - dV1) * self.tau - 2 * np.log(np.divide(1 - gf1, 1 - fV1))) 260 | else: 261 | CV1 = BV1 = 0 262 | 263 | # vol_2 factor 264 | if not skipV2: 265 | cV2 = k_var2 - 1j * u * rho2 * eta_var2 266 | 267 | # dV2 = np.sqrt(cV2 ** 2 + (1j * u + u ** 2) * eta_var2 ** 2) 268 | dV2 = np.sqrt(cV2 ** 2 + (1j * u + u ** 2) * eta_var2 ** 2 + 269 | (1j * u * xi_jump + phi_jump) * a_Var * 2 * eta_var2 ** 2) 270 | fV2 = np.divide(cV2 - dV2, cV2 + dV2) 271 | 272 | ge2 = np.exp(-dV2 * self.tau) 273 | gf2 = np.multiply(fV2, ge2) 274 | 275 | CV2 = np.multiply((cV2 - dV2) / eta_var2 ** 2, np.divide(1 - ge2, 1 - gf2)) 276 | BV2 = (k_var2 * theta_var2) / (eta_var2 ** 2) * ( 277 | (cV2 - dV2) * self.tau - 2 * np.log(np.divide(1 - gf2, 1 - fV2))) 278 | else: 279 | CV2 = BV2 = 0 280 | 281 | # z factor 282 | if not skipZ: 283 | d_z = np.sqrt(k_z ** 2 + (1j * u * xi_jump + phi_jump) * 2 * eta_z ** 2) 284 | f_z = np.divide(k_z - d_z, k_z + d_z) 285 | 286 | ge_z = np.exp(-d_z * self.tau) 287 | gf_z = np.multiply(f_z, ge_z) 288 | 289 | C_z = (k_z - d_z) / (eta_z ** 2) * np.divide(1 - ge_z, 1 - gf_z) 290 | B_z = (k_z * theta_z) / (eta_z ** 2) * ((k_z - d_z) * self.tau - 2 * np.log( 291 | np.divide(1 - gf_z, 1 - f_z))) 292 | else: 293 | C_z = B_z = 0 294 | 295 | drift_psi = 1j * u * (self.rf - self.q) * self.tau + u * 1j * np.log(self.x0) 296 | 297 | phi = drift_psi + \ 298 | BV1 + CV1 * v01 + \ 299 | BV2 + CV2 * v02 + \ 300 | B_z + C_z * z0 301 | return np.exp(phi) 302 | 303 | def kou_jump(self): 304 | """ 305 | Kou's model with asymmetric double exponential jump distribution 306 | sigma = diffusive volatility of spot process 307 | lambd = jump intensity 308 | pUp = probability of upward jump 309 | mUp = mean upward jump (set 0 < mUp < 1 for finite expectation) 310 | mDown = mean downward jump 311 | """ 312 | sigma = self.param['sigma'] 313 | lambd = self.param['lambd'] 314 | pUp = self.param['pUp'] 315 | mDown = 1 / self.param['mDown'] 316 | mUp = 1 / self.param['mUp'] 317 | u = self.u 318 | comp = lambda x: pUp * mUp / (mUp - x) + (1 - pUp) * mDown / (mDown + x) - 1 319 | alpha = -self.rf * self.tau + u * 1j * (self.rf - self.q - 0.5 * sigma ** 2 - lambd * comp(1)) * self.tau -\ 320 | 0.5 * u ** 2 * sigma ** 2 * self.tau + self.tau * lambd * comp(u * 1j) 321 | phi = alpha + u * 1j * np.log(self.x0) 322 | return np.exp(phi) 323 | 324 | def blackscholes_jump(self): 325 | """ 326 | Black and Scholes with Jump characteristic function 327 | sigma = spot volatility 328 | muJ = expected jump size 329 | sigmaJ = jump volatility 330 | lambda = jump intensity 331 | """ 332 | sigma = self.param['sigma'] 333 | muJ = self.param['muJ'] 334 | sigmaJ = self.param['sigmaJ'] 335 | lambd = self.param['lambd'] 336 | u = self.u 337 | m = np.exp(muJ+1 / 2 * sigmaJ ^ 2) - 1 338 | 339 | alpha = -self.rf * self.tau + u * 1j * self.tau * (self.rf - self.q - 0.5 * sigma ** 2 - lambd * m) - \ 340 | 0.5 * u ** 2 * sigma ** 2 * self.tau +\ 341 | lambd * self.tau * (np.exp(muJ * u * 1j - 0.5 * u ** 2 * sigmaJ ** 2) - 1) 342 | 343 | phi = alpha + u * 1j * np.log(self.x0) 344 | phi = np.exp(phi) 345 | return phi 346 | 347 | def blackscholes(self): 348 | """ 349 | Black and Scholes characteristic function 350 | sigma = spot volatility 351 | """ 352 | sigma = self.param['sigma'] 353 | u = self.u 354 | phi = -self.rf * self.tau + u * 1j * np.log(self.x0) + u * 1j * self.tau * (self.rf - self.q - 0.5 * sigma ** 2) - \ 355 | 0.5 * u ** 2 * sigma ** 2 * self.tau 356 | phi = np.exp(phi) 357 | return phi 358 | 359 | def charfun(self): 360 | if self.model_type == 'Heston': 361 | cf = self.heston() 362 | elif self.model_type == 'HestonJump': 363 | cf = self.hestonJump() 364 | elif self.model_type == 'HestonStochasticJump': 365 | cf = self.heston_stochastic_jump() 366 | elif self.model_type == 'KouJump': 367 | cf = self.kou_jump() 368 | elif self.model_type == 'BlackScholesJump': 369 | cf = self.blackscholes_jump() 370 | elif self.model_type == 'BlackScholes': 371 | cf = self.blackscholes() 372 | elif self.model_type in ['1SV1SJ', '2SV1SJ', '0SV1SJ', '2SV0SJ']: 373 | cf = self.most_complex_model() 374 | else: 375 | raise Warning('The model you are requesting does not exist') 376 | return cf 377 | 378 | class CarrMadanPricing(object): 379 | def __init__(self, char_fun, price, strike, maturity, risk_free, 380 | optimize_alpha=False): 381 | """ 382 | Carr-Madan pricing function for call options 383 | :param char_fun: characteristic function 384 | :param price: price level 385 | :param strike: strike price 386 | :param maturity: maturity 387 | :param risk_free: risk free rate 388 | :param optimize_alpha: Carr-Madan numerical alpha 389 | """ 390 | self.strike = strike 391 | self.price = price 392 | self.rf = risk_free 393 | self.T = maturity 394 | self.N = 1 # numer of contracts to price 395 | self.char_fun = char_fun 396 | self.optimize_alpha = optimize_alpha # if True, we get more 0 values 397 | 398 | self.callPrice = self.price_call_option() 399 | 400 | def LordKahlFindAlpha(self, alph, v=0): 401 | CF = self.char_fun(u=v - (alph + 1) * 1j) 402 | CF = np.divide(CF, alph ** 2 + alph - v ** 2 + 1j * (2 * alph + 1) * v).T 403 | y = - alph * np.log(self.strike) + .5 * np.log(np.real(CF) ** 2) 404 | return y 405 | 406 | def optimal_alpha(self): 407 | """ 408 | optimal Carr-Madan alpha 409 | :return: 410 | """ 411 | obj = lambda x: self.LordKahlFindAlpha(alph=x) 412 | opt_alphas = optimize.fminbound(obj, 0.1, 1, disp=False) 413 | return opt_alphas 414 | 415 | def price_call_option(self): 416 | """ 417 | numerical routine of the pricing function 418 | :return: 419 | """ 420 | n = 10 # Limit of integration 421 | k = np.log(self.strike) # log - Strike 422 | 423 | D = np.exp(-self.rf * self.T) # Discount Function 424 | 425 | N = 2 ** n # the integration is performed over N summations which are usually a power of 2 426 | eta = .05 # spacing of the integrand 427 | 428 | lmbd = (2 * np.pi) / (N * eta) # spacing size of the log-Strike 429 | b = (N * lmbd) / 2 # the log-Strike will be constrained between -b and +b 430 | 431 | u = np.arange(1, N + 1)[None, :] 432 | kk = -b + lmbd * (u - 1) # log - Strike levels 433 | jj = np.arange(1, N + 1) 434 | vj = (jj - 1) * eta 435 | 436 | if self.optimize_alpha: 437 | opt_alpha = self.optimal_alpha() 438 | else: 439 | opt_alpha = 0.2 440 | 441 | CF = self.char_fun(u=vj - (opt_alpha + 1) * 1j) 442 | CF = np.divide(CF, 443 | np.kron(opt_alpha ** 2 + opt_alpha - vj ** 2 + 1j * (2 * opt_alpha + 1) * vj, 444 | np.ones((self.N, 1)))).T 445 | calls = D * np.multiply(CF, np.exp(np.matlib.repmat(1j * vj * b, self.N, 1)).T) * (eta / 3) 446 | 447 | SimpsonW = 3 + (-1) ** jj - ((jj - 1) == 0) 448 | 449 | calls = np.multiply(calls, np.matlib.repmat(SimpsonW, self.N, 1).T) 450 | 451 | # price interpolation 452 | kkvec = kk[0] 453 | callsVector = (np.multiply(np.exp(-opt_alpha * kkvec) / np.pi, np.fft.fft(calls.T))).real 454 | callprice = np.diag(interp1d(kkvec, callsVector)(k)) 455 | return callprice 456 | 457 | class CharaBasedPrice(object): 458 | def __init__(self, param, stock, strike, maturity, risk_free, div_yield, model_type, state=None): 459 | """ 460 | Class to price call options using the closed-form characteristic function defined in CharFun 461 | :param param: set of paramters. DataFrame nSims x nParam with columns named as params. 462 | See each model in CharaFun for the proper names: 463 | :param stock: this is set to 1 (asset value), thus D is leverage 464 | :param strike: leverage, usually in (0, 1), DataFrame nSims x 1 465 | :param maturity: Maturity, DataFrame nSims x 1 466 | :param risk_free: risk-free, DataFrame nSims x 1 467 | """ 468 | self.Nsims = len(param) 469 | self.T = maturity 470 | self.rf = risk_free 471 | self.q = div_yield 472 | self.stock = stock 473 | self.strike = strike 474 | self.param = param 475 | self.model_type = model_type 476 | self.state0 = state 477 | 478 | callPrice = [] 479 | for i in range(len(param)): 480 | call_tmp = self.single_call_price(i, self.param.iloc[i], self.T.iloc[i], 0, 0, 481 | self.strike.iloc[i]) 482 | callPrice.append(call_tmp) 483 | self.callPrice = DataFrame(callPrice).set_index(0).sort_index()[1] 484 | 485 | def single_call_price(self, idx, param, T, rf, q, strike): 486 | # print(idx) 487 | char_fun = lambda u: CharFun(u, A0=self.stock, param=param, 488 | rf=rf, q=q, tau=T, model_type=self.model_type, state0=self.state0).cf 489 | try: 490 | call_price = CarrMadanPricing(char_fun=char_fun, price=self.stock, strike=strike, 491 | maturity=T, risk_free=rf).callPrice[0][0] 492 | except: 493 | # this try-except is very ugly but its a quick inexpensive trick for now 494 | call_price = CarrMadanPricing(char_fun=char_fun, price=self.stock, strike=strike, 495 | maturity=T, risk_free=rf).callPrice[0] 496 | return [idx, call_price] 497 | 498 | def apply_async_with_callback(self): 499 | result_list = [] 500 | 501 | def log_result(result): 502 | # This is called whenever foo_pool(i) returns a result. 503 | # result_list is modified only by the main process, not the pool workers. 504 | result_list.append(result) 505 | 506 | pool = mp.Pool() 507 | __spec__ = None 508 | for i in range(self.Nsims): 509 | pool.apply_async(self.single_call_price, 510 | args=(i, self.param.iloc[i], self.T.iloc[i], self.rf.iloc[i], self.q.iloc[i], 511 | self.strike.iloc[i]), 512 | callback=log_result) 513 | pool.close() 514 | pool.join() 515 | return result_list 516 | 517 | 518 | -------------------------------------------------------------------------------- /model_simulator/master_simulator.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright: 3 | Deep Learning Credit Risk Modeling 4 | Gerardo Manzo and Xiao Qiao 5 | The Journal of Fixed Income Fall 2021, jfi.2021.1.121; DOI: https://doi.org/10.3905/jfi.2021.1.121 6 | Disclaimer: 7 | THIS SOFTWARE IS PROVIDED "AS IS". YOU ARE USING THIS SOFTWARE AT YOUR OWN 8 | RISK. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 9 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 10 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY DIRECT, 11 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 12 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 13 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 14 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 15 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 16 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 17 | Description: 18 | Paper replication 19 | """ 20 | import pandas as pd 21 | from pandas import DataFrame 22 | import pricing_models as myprc 23 | import time 24 | from environment import FOLDERS 25 | 26 | # Set folder to save simulated data 27 | folderData = FOLDERS['sims_models'] 28 | 29 | # list of models to simulate 30 | Models = ['Merton74basic', 'Merton76jump', 'Heston', 'HestonJump', 31 | 'KouJump', '1SV1SJ', '2SV1SJ', '0SV1SJ', '2SV0SJ', 32 | 'PanSingleton2008'] 33 | 34 | # sample of data per model 35 | nSamples = 1000000 36 | mat = [1, 3, 5, 7, 10] 37 | 38 | if __name__ == '__main__': 39 | SpreadDist = {} 40 | for model in Models: 41 | print(model) 42 | # select model and simulate combination of parameters 43 | sim_pars = myprc.ModelSelection(modelType=model, nSamples=nSamples) 44 | model_pars = sim_pars.model_pars 45 | common_inputs = sim_pars.common_inputs 46 | # simulate model 47 | t = time.time() 48 | spreads = {} 49 | for m in mat: 50 | common_inputs['maturity'] = m 51 | out = myprc.CreditModelPricer(param=model_pars, 52 | leverage=common_inputs['leverage'], 53 | maturity=common_inputs['maturity'], 54 | risk_free=0, 55 | div_yield=0, 56 | model_type=model) 57 | spreads['mat:' + str(m)] = out.spread 58 | 59 | spreads = DataFrame(spreads) 60 | sims = pd.concat([model_pars, common_inputs, spreads], axis=1) 61 | sims.to_pickle(folderData + '%s_sims.plk' % model) 62 | time_Taken = time.time() - t 63 | print(time_Taken) 64 | print(time_Taken / 60 / 60) 65 | -------------------------------------------------------------------------------- /model_simulator/pricing_models.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright: 3 | Deep Learning Credit Risk Modeling 4 | Gerardo Manzo and Xiao Qiao 5 | The Journal of Fixed Income Fall 2021, jfi.2021.1.121; DOI: https://doi.org/10.3905/jfi.2021.1.121 6 | Disclaimer: 7 | THIS SOFTWARE IS PROVIDED "AS IS". YOU ARE USING THIS SOFTWARE AT YOUR OWN 8 | RISK. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 9 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 10 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY DIRECT, 11 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 12 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 13 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 14 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 15 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 16 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 17 | Description: 18 | Pricing function 19 | """ 20 | import numpy as np 21 | from pandas import DataFrame, Series 22 | import scipy.stats as si 23 | from smt.sampling_methods import LHS 24 | import math 25 | import charafun_pricing as mychara 26 | import scipy.linalg as la 27 | 28 | class ModelSelection(object): 29 | def __init__(self, modelType, nSamples): 30 | """ 31 | Select pricing model and its parameter sets 32 | :param modelType: str, see self.model_params for implemented models 33 | :param nSamples: int, samples to simulate, e.g. 1mm 34 | """ 35 | sim_params, common_inputs, par_labels_order = self.LHSsampling(modelType=modelType, nSamples=nSamples) 36 | 37 | mod_pars_labels = [i for i in sim_params.columns if i not in common_inputs.keys()] 38 | 39 | self.model_pars = sim_params[mod_pars_labels] 40 | self.common_inputs = sim_params[common_inputs.keys()] 41 | self.param_labels = par_labels_order 42 | 43 | def LHSsampling(self, modelType, nSamples): 44 | """ 45 | Latin hypercube sampling (LHS) 46 | :param modelType: 47 | :param nSamples: 48 | :return: 49 | """ 50 | par_labels_order, all_ranges = self.model_params(modelType) 51 | 52 | # Add common inputs: leverage, risk free, maturity 53 | common_inputs = self.common_model_inputs() 54 | [(all_ranges.append(v), par_labels_order.append(i)) for i, v in common_inputs.items()] 55 | 56 | np.random.seed(12345) 57 | all_ranges = np.asarray(all_ranges) 58 | sampling = LHS(xlimits=all_ranges) 59 | sim_params = sampling(nSamples) 60 | sim_params = DataFrame(sim_params, columns=par_labels_order) 61 | return sim_params, common_inputs, par_labels_order 62 | 63 | def common_model_inputs(self): 64 | """ 65 | set input for leverage for structural models 66 | :return: 67 | """ 68 | inputs = {'leverage': [0.01, 0.99]} 69 | return inputs 70 | 71 | def fix_feller_condition(self, sim_params): 72 | """ 73 | Enforce Feller condition for mean-reverting models (2 * k * Theta > eps^2) 74 | with k = reversion speed, Theta = reversion mean, eps: vol of vol 75 | :param sim_params: 76 | :return: 77 | """ 78 | for pname in sim_params.columns: 79 | if 'kappa' in pname: 80 | state = pname.split('_')[1] 81 | drift = 2 * sim_params['kappa' + '_' + state] * sim_params['theta' + '_' + state] 82 | var = sim_params['sigma' + '_' + state] ** 2 83 | feller_false = drift < var 84 | sim_params.loc[feller_false, 'sigma' + '_' + state] = np.sqrt(drift.loc[feller_false]) 85 | return sim_params 86 | 87 | def model_params(self, modelType): 88 | """ 89 | Define parameter range for each model. 90 | :return: range of values for each parameter for each model 91 | """ 92 | param_ranges = {'Merton74basic': {'sigma': [0.001, 2], 'mu': [0.01, 2]}, 93 | 'Merton76jump': {'lambd': [0.001, 1], 'sigma': [0.001, 2], 94 | 'muJ': [-2, 0], 'sigmaJ': [0.001, 1], 95 | 'mu': [-0.5, .5]}, 96 | 'Heston': {'v0': [0.001, 1], 'sigma_var': [0.001, 1], 97 | 'kappa_var': [0.001, 2], 'theta_var': [0.001, 1], 98 | 'rho': [-.99, -.05]}, 99 | 'HestonJump': {'v0': [0.001, 1], 'sigma_var': [0.001, 2], 100 | 'kappa_var': [0.001, 2], 'theta_var': [0.001, 1], 101 | 'rho': [-.99, -.05], 102 | 'lambd': [0.0001, 1], 103 | 'muJ': [-2, 0], 104 | 'sigmaJ': [0.001, 1]}, 105 | 'KouJump': {'lambd': [0.001, 1], 'sigma': [0.001, 2], 106 | 'pUp': [0.001, 1], 'mDown': [0.001, 1], 107 | 'mUp': [0.001, 1]}, 108 | '1SV1SJ': {'v01': [0.001, 1], 'sigma_var1': [0.001, 2], 'kappa_var1': [0.001, 2], 109 | 'theta_var1': [0.001, 1], 'rho1': [-.99, -.05], 110 | 'z0': [0.001, 1], 'sigma_z': [0.001, 2], 'kappa_z': [0.001, 2], 111 | 'theta_z': [0.001, 1], 'muJ': [-2, 0], 'sigmaJ': [0.001, 1], 112 | 'a_Var': [0.001, 1]}, 113 | '2SV1SJ': {'v01': [0.001, 1], 'sigma_var1': [0.001, 2], 'kappa_var1': [0.001, 2], 114 | 'theta_var1': [0.001, 1], 'rho1': [-.99, -.05], 115 | 'v02': [0.001, 1], 'sigma_var2': [0.001, 2], 'kappa_var2': [0.001, 2], 116 | 'theta_var2': [0.001, 1], 'rho2': [-.99, -.05], 117 | 'z0': [0.001, 1], 'sigma_z': [0.001, 2], 'kappa_z': [0.001, 2], 118 | 'theta_z': [0.001, 1], 'muJ': [-2, 0], 'sigmaJ': [0.001, 1], 119 | 'a_Var': [0.001, 1]}, 120 | '0SV1SJ': {'z0': [0.001, 0.1], 'sigma_z': [0.001, 2], 'kappa_z': [0.001, 2], 121 | 'theta_z': [0.001, 1], 'muJ': [-2, 0], 'sigmaJ': [0.001, 1]}, 122 | '2SV0SJ': {'v01': [0.001, 1], 'sigma_var1': [0.001, 2], 'kappa_var1': [0.001, 2], 123 | 'theta_var1': [0.001, 1], 'rho1': [-.99, -.05], 124 | 'v02': [0.001, 1], 'sigma_var2': [0.001, 2], 'kappa_var2': [0.001, 2], 125 | 'theta_var2': [0.001, 1], 'rho2': [-.99, -.05]}, 126 | 'PanSingleton2008': {'lmbd0': [0.001, 1], 'sigma_lmbd': [0.001, 2], 'kappa_lmbd': [0.001, 2], 127 | 'theta_lmbd': [-7, 1]}} 128 | 129 | param_ranges = param_ranges[modelType] 130 | par_labels_order = [] 131 | all_ranges = [] 132 | for par_label, par_range in param_ranges.items(): 133 | par_labels_order.append(par_label) 134 | all_ranges.append(par_range) 135 | return par_labels_order, all_ranges 136 | 137 | 138 | 139 | class CreditModelPricer(object): 140 | 141 | def __init__(self, param, leverage, maturity, risk_free, div_yield, model_type, LGD=0.55, 142 | states=None, nTrial=1e4, model=None): 143 | """ 144 | Model pricer 145 | :param param: dict of params for each model, refer to SelectModels for implemented models 146 | :param leverage: int 147 | :param maturity: 148 | :param risk_free: 149 | :param div_yield: 150 | :param model_type: str, model to price, refer to SelectModels for implemented models 151 | :param states: 152 | :param nTrial: 153 | :param model: 154 | """ 155 | self.param = param 156 | self.states = states 157 | self.debt = leverage 158 | self.rf = risk_free 159 | self.q = div_yield 160 | self.LGD = LGD 161 | self.nTrial = nTrial 162 | self.model = model 163 | self.assets = 1 164 | self.mat = maturity 165 | self.model_type = model_type 166 | 167 | if model_type == 'PanSingleton2008': 168 | self.spread = self.reducedform_model() 169 | else: 170 | # Compute Implied Put 171 | self.putPrice = self.implied_put() 172 | # Compute Spread 173 | self.spread = self.put2spread() 174 | 175 | def reducedform_model(self): 176 | """ 177 | price the reduced form model of pan and singleton 178 | :return: 179 | """ 180 | lmbd0 = self.param['lmbd0'] 181 | DiscFun = DataFrame(np.ones((1, 100))) 182 | spread = {} 183 | for i in range(len(self.mat)): 184 | param = self.param.loc[i] 185 | obj = PanSingletonSims(lmbd0=lmbd0[i], param=param, LGD=self.LGD, DiscFun=DiscFun, 186 | mat=[self.mat[i]]) 187 | spread[i] = obj.CDS[self.mat[i]] 188 | print([i, spread[i]]) 189 | spread = Series(spread) 190 | return spread 191 | 192 | def call2putprice(self, callPrice): 193 | """ 194 | Put-Call parity to retrieve put price 195 | :param callPrice: 196 | :return: 197 | """ 198 | discFun = np.exp(-self.rf * self.mat) 199 | divFun = np.exp(-self.q * self.mat) 200 | putPrice = np.maximum(callPrice - self.assets * divFun + discFun * self.debt, 0) 201 | return putPrice 202 | 203 | def put2spread(self): 204 | """ 205 | Convert put price into spreads 206 | Add dividend yield here if needed 207 | :return: 208 | """ 209 | L = self.debt * np.exp(-self.rf * self.mat) 210 | CS = - np.log(1 - (self.putPrice.astype('float') / L)) / self.mat 211 | spread = np.maximum(CS, 0) 212 | return spread 213 | 214 | def bs_call_price(self, mu=None, rf=None, sigma=None): 215 | """ 216 | Black-Scholes-Merton call option price 217 | :return: 218 | """ 219 | # S: spot price 220 | # K: strike price 221 | # T: time to maturity 222 | # r: interest rate 223 | # sigma: volatility of underlying asset 224 | 225 | rf = rf if rf is not None else self.rf 226 | sigma = sigma if sigma is not None else self.param['sigma'] 227 | mu = mu if mu is not None else self.param.get('mu', mu) 228 | 229 | # inverse of leverage 230 | Linv = self.assets * np.exp((mu - self.q) * self.mat) / self.debt 231 | sT = sigma * np.sqrt(self.mat) 232 | d1 = np.log(Linv) / sT + 0.5 * sT 233 | d2 = d1 - sT 234 | # Normal CDFs 235 | Nd1 = si.norm.cdf(d1, 0.0, 1.0) 236 | Nd2 = si.norm.cdf(d2, 0.0, 1.0) 237 | # call price 238 | callPrice = (self.assets * np.exp(-self.q * self.mat) * Nd1 - 239 | self.debt * np.exp(-rf * self.mat) * Nd2) 240 | return callPrice 241 | 242 | def bs_call_price_with_jump(self): 243 | """ 244 | Blac-Scholes-Merton call price with jumps 245 | Merton (1974) 246 | :return: 247 | """ 248 | try: 249 | lmbd = self.param['lambd'] 250 | sigma = self.param['sigma'] 251 | muJump = self.param['muJ'] 252 | sigmaJump = self.param['sigmaJ'] 253 | except: 254 | lmbd, sigma, muJump, sigmaJump = self.param 255 | 256 | k = np.exp(muJump + .5 * sigmaJump ** 2) - 1 257 | lmbd_hat = lmbd * (1 + k) 258 | 259 | callPrice = 0 260 | N = 100 261 | for i in range(N): 262 | sigma_ii = np.sqrt(sigma ** 2 + i * sigmaJump ** 2 / self.mat) 263 | r_ii = self.rf - k * lmbd + (i * np.log(1 + k) / self.mat) 264 | # jump-free call price 265 | bs_prices = self.bs_call_price(mu=r_ii, rf=r_ii, sigma=sigma_ii) 266 | # jump factor 267 | jump_factor = np.exp(-lmbd_hat * self.mat) * (lmbd_hat * self.mat) ** i 268 | jump_factor /= float(math.factorial(i)) 269 | # final call price 270 | callPrice += jump_factor * bs_prices 271 | 272 | return callPrice 273 | 274 | def implied_put(self): 275 | """ 276 | Model-implied put price 277 | :return: 278 | """ 279 | if self.model_type == 'Merton74basic': 280 | callPrice = self.bs_call_price() 281 | elif self.model_type == 'Merton76jump': 282 | callPrice = self.bs_call_price_with_jump() 283 | elif self.model_type not in ['Merton74basic', 'Merton76jump']: 284 | callPrice = mychara.CharaBasedPrice(param=self.param, stock=self.assets, strike=self.debt, 285 | maturity=self.mat, risk_free=self.rf, div_yield=self.q, 286 | model_type=self.model_type, state=self.states).callPrice 287 | else: 288 | raise Warning('ModelType not properly specified or not existent') 289 | self.callPrice = callPrice 290 | putPrice = self.call2putprice(callPrice) 291 | return putPrice 292 | 293 | 294 | 295 | 296 | class PanSingletonSims(object): 297 | def __init__(self, lmbd0, param, LGD, DiscFun, mat, pay_freq=4, FullyImplicitMethod=True): 298 | """ 299 | Panl and Singleton (2008) pricing model 300 | :param lmbd0: initial value for log-normal process 301 | :param param: dict of params, {'k', 'theta', 'sigma'} 302 | :param LGD: loss-given default 303 | :param DiscFun: dicount function 304 | :param mat: maturity, int 305 | :param pay_freq: premium frequency of CDs contract 306 | :param FullyImplicitMethod: numerical approximation of default/survival probability 307 | """ 308 | self.LGD = LGD 309 | self.DiscFun = DiscFun 310 | self.mat = mat 311 | self.param = param 312 | self.lmbd0 = lmbd0 313 | self.freq = pay_freq 314 | self.payments = pay_freq * mat 315 | self.kappa = param['kappa_lmbd'] 316 | self.theta = param['theta_lmbd'] 317 | self.sig = param['sigma_lmbd'] 318 | if FullyImplicitMethod: 319 | self.CDS = self.CrankNickPricing() 320 | else: 321 | self.CDS = self.sim_spread() 322 | 323 | def sim_spread(self, Nsims=1000, dt=1./360, burnout=200): 324 | """ 325 | Simulate spreads 326 | :param Nsims: int, number of simulations 327 | :param dt: float, time fraction 328 | :param burnout: int, number of initial simulations to discard 329 | :return: 330 | """ 331 | 332 | T = int(self.mat / dt) 333 | eps = np.random.normal(size=(T + burnout, Nsims)) 334 | ln_lmbdt = np.zeros((T + burnout, Nsims)) 335 | ln_lmbdt[0, :] = np.log(self.lmbd0) 336 | for t in range(1, T + burnout): 337 | ln_lmbdt[t, :] = ln_lmbdt[t - 1, :] + self.kappa * (self.theta - ln_lmbdt[t - 1, :]) * dt + self.sig * eps[t - 1, :] * np.sqrt(dt) 338 | 339 | lmbdt = np.exp(ln_lmbdt[burnout:, :]) 340 | pay_dates = [(i + 1) * 90 for i in range(self.payments)] 341 | surv_prob = 0 342 | for q in range(self.payments): 343 | lq = lmbdt[:pay_dates[q], :] 344 | surv_prob += np.mean(np.exp(-np.sum(lq * dt, axis=0))) 345 | 346 | def_prob = 0 347 | for tq in range(T): 348 | def_prob += np.mean(lmbdt[tq, :] * np.exp(-np.sum(lmbdt[:tq, :] * dt, axis=0))) * dt 349 | 350 | CDS = def_prob * self.LGD / surv_prob 351 | return CDS 352 | 353 | def TDMASolve(self, a, b, c, d): 354 | """ 355 | TDMA solver, a b c d can be NumPy array type or Python list type. 356 | refer to http://en.wikipedia.org/wiki/Tridiagonal_matrix_algorithm 357 | and to http://www.cfd-online.com/Wiki/Tridiagonal_matrix_algorithm_-_TDMA_(Thomas_algorithm) 358 | :return: 359 | """ 360 | nf = len(d) # number of equations 361 | ac, bc, cc, dc = map(np.array, (a, b, c, d)) # copy arrays 362 | for it in range(1, nf): 363 | mc = ac[it - 1] / bc[it - 1] 364 | bc[it] = bc[it] - mc * cc[it - 1] 365 | dc[it] = dc[it] - mc * dc[it - 1] 366 | 367 | xc = bc 368 | xc[-1] = dc[-1] / bc[-1] 369 | 370 | for il in range(nf - 2, -1, -1): 371 | xc[il] = (dc[il] - cc[il] * xc[il + 1]) / bc[il] 372 | 373 | return xc 374 | 375 | def CDSgrid(self, T, DiscFun, SurvProb, DefProb): 376 | """ 377 | Create the grid of CDS spread 378 | :param T: maturity of CDS spread 379 | :param DiscFun: discount function for that date 380 | :param SurvProb: grid of survival probability 381 | :param DefProb: grid of default probability 382 | :return: grid of CDS spreads 383 | """ 384 | freq = 4 385 | pay_dates = [i * 3 for i in range(freq + 1)] 386 | prem_leg = {} 387 | def_leg = {} 388 | for q in range(freq): 389 | surv_prob = SurvProb[1:, pay_dates[q]:pay_dates[q + 1]] 390 | def_prob = DefProb[1:, pay_dates[q]:pay_dates[q + 1]] 391 | disc_fun = DiscFun.values[0, pay_dates[q]:pay_dates[q + 1]] 392 | prem_leg[q] = np.dot(surv_prob, disc_fun) 393 | def_leg[q] = np.dot(def_prob, disc_fun) 394 | 395 | # CDS spread 396 | cds_grid = self.LGD * DataFrame(def_leg).sum(axis=1) / DataFrame(prem_leg).sum(axis=1) 397 | return cds_grid 398 | 399 | def intSprd(self, CDSfitGridT, lmbdGrid): 400 | """ 401 | Routing to interpolate spreads. This runs in parallel 402 | :param date: keep track of date in the parallelization 403 | :param obsCDS: observed spread at date t 404 | :param DiscFun: discount function at date t 405 | :param survProb: grid of survival probability 406 | :param defProb: grid of default probability 407 | :param ProbMeasure: probability measure 'Q' or 'P' 408 | :return: dict called results 409 | """ 410 | expLmbdGrid = np.exp(lmbdGrid) 411 | idx_min = abs(expLmbdGrid - self.lmbd0).argmin() 412 | state_i1, state_i = expLmbdGrid[idx_min - 1], expLmbdGrid[idx_min] 413 | w = (self.lmbd0 - state_i1) / (state_i - state_i1) 414 | if idx_min >= self.M - 1: 415 | idx_min -= 1 416 | CDSfit = {T: (1. - w) * CDSfitGridT[T][idx_min - 1] + w * CDSfitGridT[T][idx_min] for T in self.mat} 417 | return CDSfit 418 | 419 | def FD_method(self, prob_type='SurvProd'): 420 | """ 421 | Implicit Finite Difference method to construct survival and default probability 422 | :param prob_type: 'SurvProd' or 'DefProd' 423 | :param probMeasure: 'P' or 'Q' 424 | :return: grif od probabilities F and default intensity vecX 425 | """ 426 | # number of grid points: intensity 427 | dX = (np.log(self.Xmax) - np.log(self.Xmin)) / self.M 428 | # intensity points 429 | points = np.arange(self.M) 430 | vecX = np.log(self.Xmin) + dX * points 431 | 432 | F = np.zeros((self.M, self.N)) 433 | # drift of the log-intensity of default 434 | driftV = self.kappa * (self.theta - vecX) * self.dt 435 | 436 | # boundary values 437 | F[:, 0] = 1 if prob_type == 'SurvProb' else np.exp(vecX) 438 | 439 | C1 = self.sig ** 2. / (2. * dX ** 2.) + driftV / (2. * dX) 440 | C3 = self.sig ** 2. / (2. * dX ** 2.) - driftV / (2. * dX) 441 | C2 = -self.sig ** 2. / dX ** 2. - np.exp(vecX) - 1. / self.dt 442 | 443 | Ab = np.zeros((3, self.M)) 444 | Ab[0, 1:] = C1[:-1] 445 | Ab[1, :] = C2 446 | Ab[2, :-1] = C3[1:] 447 | 448 | for nn in range(1, self.N): 449 | A = -F[:, nn - 1] / self.dt 450 | A[-1] = 0 451 | ss = la.solve_banded((1, 1), ab=Ab, b=A) 452 | F[:, nn] = ss.flatten() 453 | return F, vecX 454 | 455 | def set_finite_diff_pars(self): 456 | """ 457 | Set parameters for Finite Difference Method 458 | :return: 459 | """ 460 | self.Xmin = 0.000001 461 | self.Xmax = 1 462 | self.dt = 1 / 12 463 | self.M = 200 464 | self.N = 120 465 | 466 | def CrankNickPricing(self): 467 | """ 468 | Crank-Nicholson routine to copmute probabilities numerically 469 | :return: 470 | """ 471 | self.set_finite_diff_pars() 472 | 473 | # Compute Survivial probabilities using finite implicit method 474 | SurvProbGrid, StateGrid = self.FD_method(prob_type='SurvProb') 475 | 476 | # Compute Default probabilities using finite implicit method 477 | DefProbGrid, _ = self.FD_method(prob_type='DefProb') 478 | 479 | CDSfitGridT = {T: self.CDSgrid(T, self.DiscFun, SurvProbGrid, DefProbGrid) for T in self.mat} 480 | 481 | cds = self.intSprd(CDSfitGridT=CDSfitGridT, lmbdGrid=StateGrid) 482 | return cds 483 | 484 | 485 | 486 | if __name__ == '__main__': 487 | import time 488 | param = [0.61, 0.4, .3] 489 | LGD = 0.55 490 | DiscFun = DataFrame(np.ones((50, 390))) 491 | mat = [1, 3, 5, 7, 10] 492 | mat = [1] 493 | lmbd0 = .0599 494 | pay_freq = 4 495 | 496 | # PanSingleton(param, LGD, DiscFun, mat) 497 | time0 = time.time() 498 | out = PanSingletonSims(lmbd0, param, LGD, DiscFun, mat, pay_freq, FullyImplicitMethod=True) 499 | time1 = time.time() 500 | print([out.CDS, 'CrankNicholson time: ', time1 - time0]) 501 | 502 | --------------------------------------------------------------------------------