├── .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 |
4 |
5 |
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 | 
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 | 
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 |
--------------------------------------------------------------------------------