├── .DS_Store ├── BasketOption.pdf ├── CallOption.pdf ├── LICENSE ├── MatlabBenchmark.m ├── PricingForward.pdf ├── README.md ├── RecursiveEquation.py ├── XvaSolver.py ├── __pycache__ ├── RecursiveEquation.cpython-37.pyc ├── equation.cpython-37.pyc ├── solver.cpython-37.pyc ├── xvaEquation.cpython-37.pyc └── xvaSolver.cpython-37.pyc ├── basketCall.py ├── basketCallWithCVA.py ├── callOption.py ├── callOptionStabilityTests.py ├── equation.py ├── exposureBasketOption.xlsx ├── exposureCallOption.xlsx ├── exposurePricingForward.xlsx ├── forward.py ├── forward.sav ├── fvaForward.py ├── solver.py └── xvaEquation.py /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlessandroGnoatto/Deep-xVA-Solver/4b6efc0caef32424a70739c59b1867e80ab7fd6f/.DS_Store -------------------------------------------------------------------------------- /BasketOption.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlessandroGnoatto/Deep-xVA-Solver/4b6efc0caef32424a70739c59b1867e80ab7fd6f/BasketOption.pdf -------------------------------------------------------------------------------- /CallOption.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlessandroGnoatto/Deep-xVA-Solver/4b6efc0caef32424a70739c59b1867e80ab7fd6f/CallOption.pdf -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 frankhan91 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MatlabBenchmark.m: -------------------------------------------------------------------------------- 1 | T = xlsread('exposureBasketOption.xlsx'); 2 | lambdaC = 0.1; 3 | lambdaB = 0.01; 4 | r = 0.01; 5 | recoveryB = 0.4; 6 | recoveryC = 0.3; 7 | 8 | deltaT = 1/100; 9 | 10 | for i = 1 :size(T,2) 11 | EPE(i) = mean(max(T(:,i),0)); 12 | ENE(i) = mean(max(-T(:,i),0)); 13 | CVA(i) = ENE(i) * lambdaB * exp(-(r+lambdaC+lambdaB)*deltaT* i); 14 | DVA(i) = EPE(i) * lambdaB * exp(-(r+lambdaC+lambdaB)*deltaT* i); 15 | end 16 | 17 | disp("DVA short call - vHat > 0") 18 | disp(sum(DVA)*(1-recoveryB)*deltaT) 19 | disp("CVA short call - vHat > 0") 20 | disp(sum(CVA)*(1-recoveryC)*deltaT) 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /PricingForward.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlessandroGnoatto/Deep-xVA-Solver/4b6efc0caef32424a70739c59b1867e80ab7fd6f/PricingForward.pdf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Deep xVA Solver](https://arxiv.org/abs/2005.02633) in TensorFlow (2.0) 2 | 3 | 4 | ## How to run the examples 5 | 6 | Currently we offer two examples from the paper. We compute the exposure of: 7 | 8 | * A forward on a single underlying. 9 | * A basket call option on 100 underlyings. 10 | * Recursive Computation of FVA on a forward 11 | * Recursive Computation of BCVA on a basket call with 100 underlyings. 12 | 13 | To run the examples, simply rung the associated scripts 14 | 15 | * forward.py 16 | * basketCall.py 17 | * fvaForward.py 18 | * basketCallWithCVA.py 19 | 20 | 21 | ## Aknowledgements. 22 | We are grateful to Chang Jiang for the help in the conversion of our code base from Tensorflow 1.x to Tensorflow 2.2. 23 | 24 | 25 | ## Dependencies 26 | 27 | * [TensorFlow >=2.0](https://www.tensorflow.org/) 28 | 29 | 30 | ## Reference 31 | [1] Gnoatto, A., Picarelli, A., and Reisinger, C. Deep xVA solver - A neural network based counterparty credit risk management framework. [[arXiv]](https://arxiv.org/abs/2005.02633) 32 | -------------------------------------------------------------------------------- /RecursiveEquation.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import tensorflow as tf 3 | from tqdm import tqdm 4 | 5 | class RecursiveEquation(object): 6 | """Base class for defining PDE related function.""" 7 | 8 | def __init__(self, eqn_config): 9 | self.dim = eqn_config.dim 10 | self.total_time = eqn_config.total_time 11 | self.num_time_interval = eqn_config.num_time_interval 12 | self.delta_t = self.total_time / self.num_time_interval 13 | self.sqrt_delta_t = np.sqrt(self.delta_t) 14 | self.y_init = None 15 | self.clean_bsde = eqn_config.clean_bsde 16 | self.clean_bsde_model = eqn_config.clean_bsde_model 17 | 18 | def sample(self, num_sample): 19 | """Sample forward SDE.""" 20 | """Sample clean BSDE and the collateral account.""" 21 | raise NotImplementedError 22 | 23 | def f_tf(self, t, x, y, z, vhat, collateral): 24 | """Generator function in the PDE.""" 25 | raise NotImplementedError 26 | 27 | def g_tf(self, t, x, vhat,collateral): 28 | """Terminal condition of the PDE.""" 29 | raise NotImplementedError 30 | 31 | class FVA(RecursiveEquation): 32 | def __init__(self,eqn_config): 33 | self.dim = eqn_config.clean_value.dim 34 | self.total_time = eqn_config.total_time 35 | self.num_time_interval = eqn_config.num_time_interval 36 | self.delta_t = self.total_time / self.num_time_interval 37 | self.sqrt_delta_t = np.sqrt(self.delta_t) 38 | self.y_init = None 39 | self.rate = eqn_config.r # risk-free return 40 | self.r_fl = eqn_config.r_fl # unsecured funding lending rate 41 | self.r_fb = eqn_config.r_fb # unsecured funding borrowing rate 42 | self.clean_value = eqn_config.clean_value # Class Equation, to simulate dw and x 43 | self.clean_value_model = eqn_config.clean_value_model 44 | # Class NonsharedModel to simulate vhat 45 | 46 | 47 | def sample(self, num_sample): 48 | forward_sde = self.clean_value.sample(num_sample) 49 | dw, x = forward_sde 50 | clean_value = self.clean_value_model.simulate_path(forward_sde) 51 | 52 | #this is where we model the collateral account. 53 | collateral = clean_value * 0.0 54 | return dw, x, clean_value, collateral 55 | 56 | def f_tf(self, t, x, y, z,v_clean, collateral): 57 | fva = (self.r_fl-self.rate)*tf.maximum(v_clean-y-collateral,0) - (self.r_fb-self.rate)*tf.maximum(collateral+y-v_clean,0) 58 | discount = -(self.rate)*y 59 | 60 | return fva + discount 61 | 62 | def g_tf(self, t, x,v_clean, collateral): 63 | return 0. 64 | 65 | class BCVA(RecursiveEquation): 66 | def __init__(self,eqn_config): 67 | self.dim = eqn_config.clean_value.dim 68 | self.total_time = eqn_config.total_time 69 | self.num_time_interval = eqn_config.num_time_interval 70 | self.delta_t = self.total_time / self.num_time_interval 71 | self.sqrt_delta_t = np.sqrt(self.delta_t) 72 | self.y_init = None 73 | self._r = eqn_config.r # interest rate 74 | self._recoveryC = eqn_config.recoveryC 75 | self._lambdaC = eqn_config.lambdaC 76 | self._recoveryB = eqn_config.recoveryB 77 | self._lambdaB = eqn_config.lambdaB 78 | self.clean_value = eqn_config.clean_value # Class Equation, to simulate dw and x 79 | self.clean_value_model = eqn_config.clean_value_model 80 | 81 | 82 | def sample(self, num_sample): 83 | forward_sde = self.clean_value.sample(num_sample) 84 | dw, x = forward_sde 85 | clean_value = self.clean_value_model.simulate_path(forward_sde) 86 | 87 | #this is where we model the collateral account. 88 | collateral = clean_value * 0.0 89 | return dw, x, clean_value, collateral 90 | 91 | 92 | def f_tf(self, t, x, y, z,v_clean, collateral): 93 | cva = (1.0-self._recoveryC)*tf.maximum(-v_clean+collateral,0.0)*self._lambdaC 94 | dva = (1.0-self._recoveryB)*tf.maximum(v_clean-collateral,0.0)*self._lambdaB 95 | discount = (self._r + self._lambdaC + self._lambdaB)* y 96 | return -cva + dva - discount 97 | 98 | 99 | 100 | def g_tf(self, t, x,v_clean, collateral): 101 | return 0. 102 | 103 | def monte_carlo(self,num_sample=1024): #monte carlo estimation of CVA and DVA 104 | 105 | print('CVA and DVA Monte Carlo estimation started') 106 | discount = np.exp(-(self._r+self._lambdaB+self._lambdaC)*np.linspace(0,self.total_time,self.num_time_interval+1)) 107 | 108 | estimate = [] 109 | for i in tqdm(range(num_sample//1024)): #split into batches to estimate 110 | dw, x, clean_value, collateral= self.sample(1024) #shape (num_sample, dim=1, num_time_interval+1) 111 | phi_cva = (1-self._recoveryC)*discount*np.maximum(collateral-clean_value,0)*self._lambdaC 112 | phi_dva = (1-self._recoveryB)*discount*np.maximum(clean_value-collateral,0)*self._lambdaB 113 | 114 | #trapeziodal rule 115 | cva = np.sum(phi_cva,axis=-1)-(phi_cva[:,:,-1]+phi_cva[:,:,0])/2 116 | dva = np.sum(phi_dva,axis=-1)-(phi_dva[:,:,-1]+phi_dva[:,:,0])/2 117 | 118 | estimate += list(dva[:,0]-cva[:,0]) 119 | 120 | 121 | if num_sample%1024!= 0: #calculate the remaining number of smaples 122 | dw, x, clean_value, collateral = self.sample(num_sample%1024) #shape (num_sample, dim=1, num_time_interval+1) 123 | phi_cva = (1-self._recoveryC)*discount*np.maximum(collateral-clean_value,0)*self._lambdaC 124 | phi_dva = (1-self._recoveryB)*discount*np.maximum(clean_value-collateral,0)*self._lambdaB 125 | 126 | #trapeziodal rule 127 | cva = np.sum(phi_cva,axis=-1)-(phi_cva[:,:,-1]+phi_cva[:,:,0])/2 128 | dva = np.sum(phi_dva,axis=-1)-(phi_dva[:,:,-1]+phi_dva[:,:,0])/2 129 | 130 | estimate += list(dva[:,0]-cva[:,0]) 131 | estimate = np.array(estimate)*self.delta_t #times time-interval (height of a trapezium) 132 | mean = np.mean(estimate) 133 | std = np.std(estimate)/np.sqrt(num_sample) 134 | 135 | return mean, [mean-3*std,mean+3*std] 136 | -------------------------------------------------------------------------------- /XvaSolver.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | import numpy as np 4 | import tensorflow as tf 5 | from tqdm import tqdm 6 | import tensorflow.keras.layers as layers 7 | DELTA_CLIP = 50.0 8 | 9 | 10 | class XvaSolver(object): 11 | """The fully connected neural network model.""" 12 | def __init__(self, config, bsde): 13 | self.eqn_config = config.eqn_config 14 | self.net_config = config.net_config 15 | self.bsde = bsde 16 | 17 | self.model = NonsharedModel(config, bsde) 18 | #self.y_init = self.model.y_init 19 | 20 | try: 21 | lr_schedule = config.net_config.lr_schedule 22 | except AttributeError: 23 | lr_schedule = tf.keras.optimizers.schedules.PiecewiseConstantDecay( 24 | self.net_config.lr_boundaries, self.net_config.lr_values) 25 | 26 | self.optimizer = tf.keras.optimizers.Adam(learning_rate=lr_schedule, epsilon=1e-8) 27 | 28 | def train(self): 29 | start_time = time.time() 30 | training_history = [] 31 | valid_data = self.bsde.sample(self.net_config.valid_size) 32 | 33 | # begin sgd iteration 34 | for step in tqdm(range(self.net_config.num_iterations+1)): 35 | if step % self.net_config.logging_frequency == 0: 36 | loss = self.loss_fn(valid_data, training=False).numpy() 37 | y_init = self.model.y_init.numpy()[0] 38 | elapsed_time = time.time() - start_time 39 | training_history.append([step, loss, y_init, elapsed_time]) 40 | if self.net_config.verbose: 41 | #logging.info("step: %5u, loss: %.4e, Y0: %.4e, elapsed time: %3u" % ( 42 | # step, loss, y_init, elapsed_time)) 43 | print("step: %5u, loss: %.4e, Y0: %.4e, elapsed time: %3u" % ( 44 | step, loss, y_init, elapsed_time)) 45 | self.train_step(self.bsde.sample(self.net_config.batch_size)) 46 | return np.array(training_history) 47 | 48 | def loss_fn(self, inputs, training): 49 | 50 | dw, x, v_clean, coll = inputs 51 | y_terminal = self.model(inputs, training) 52 | delta = y_terminal - self.bsde.g_tf(self.bsde.total_time, x[:, :, -1],v_clean[:,:,-1], coll[:,:,-1]) 53 | # use linear approximation outside the clipped range 54 | loss = tf.reduce_mean(tf.where(tf.abs(delta) < DELTA_CLIP, tf.square(delta), 55 | 2 * DELTA_CLIP * tf.abs(delta) - DELTA_CLIP ** 2)) 56 | 57 | return loss 58 | 59 | def grad(self, inputs, training): 60 | with tf.GradientTape(persistent=True) as tape: 61 | loss = self.loss_fn(inputs, training) 62 | grad = tape.gradient(loss, self.model.trainable_variables) 63 | del tape 64 | return grad 65 | 66 | @tf.function 67 | def train_step(self, train_data): 68 | grad = self.grad(train_data, training=True) 69 | self.optimizer.apply_gradients(zip(grad, self.model.trainable_variables)) 70 | 71 | 72 | class NonsharedModel(tf.keras.Model): 73 | def __init__(self, config, bsde): 74 | super(NonsharedModel, self).__init__() 75 | self.config = config 76 | self.eqn_config = config.eqn_config 77 | self.net_config = config.net_config 78 | self.bsde = bsde 79 | self.dim = bsde.dim 80 | self.y_init = tf.Variable(np.random.uniform(low=self.net_config.y_init_range[0], 81 | high=self.net_config.y_init_range[1], 82 | size=[1]),dtype=self.net_config.dtype 83 | ) 84 | self.z_init = tf.Variable(np.random.uniform(low=-.1, high=.1, 85 | size=[1, self.eqn_config.dim]),dtype=self.net_config.dtype 86 | ) 87 | 88 | self.subnet = [FeedForwardSubNet(config,bsde.dim) for _ in range(self.bsde.num_time_interval-1)] 89 | 90 | def call(self, inputs, training): 91 | dw, x, v_clean, coll = inputs 92 | time_stamp = np.arange(0, self.eqn_config.num_time_interval) * self.bsde.delta_t 93 | all_one_vec = tf.ones(shape=tf.stack([tf.shape(dw)[0], 1]), dtype=self.net_config.dtype) 94 | y = all_one_vec * self.y_init 95 | z = tf.matmul(all_one_vec, self.z_init) 96 | 97 | for t in range(0, self.bsde.num_time_interval-1): 98 | y = y - self.bsde.delta_t * ( 99 | self.bsde.f_tf(time_stamp[t], x[:, :, t], y, z, v_clean[:,:,t], coll[:,:,t]) 100 | ) + tf.reduce_sum(z * dw[:, :, t], 1, keepdims=True) 101 | try: 102 | z = self.subnet[t](x[:, :, t + 1], training) / self.bsde.dim 103 | except TypeError: 104 | z = self.subnet(tf.concat([time_stamp[t+1]*all_one_vec,x[:, :, t + 1]],axis=1), training=training) / self.bsde.dim 105 | # terminal time 106 | y = y - self.bsde.delta_t * self.bsde.f_tf(time_stamp[-1], x[:, :, -2], y, z,v_clean[:,:,-2],coll[:,:,-2]) + \ 107 | tf.reduce_sum(z * dw[:, :, -1], 1, keepdims=True) 108 | return y 109 | 110 | def predict_step(self, data): 111 | dw, x, v_clean, coll = data[0] 112 | time_stamp = np.arange(0, self.eqn_config.num_time_interval) * self.bsde.delta_t 113 | all_one_vec = tf.ones(shape=tf.stack([tf.shape(dw)[0], 1]), dtype=self.net_config.dtype) 114 | y = all_one_vec * self.y_init 115 | z = tf.matmul(all_one_vec, self.z_init) 116 | 117 | history = tf.TensorArray(self.net_config.dtype,size=self.bsde.num_time_interval+1) 118 | history = history.write(0,y) 119 | 120 | for t in range(0, self.bsde.num_time_interval-1): 121 | y = y - self.bsde.delta_t * ( 122 | self.bsde.f_tf(time_stamp[t], x[:, :, t], y, z,v_clean[:,:,t],coll[:,:,t]) 123 | ) + tf.reduce_sum(z * dw[:, :, t], 1, keepdims=True) 124 | 125 | history = history.write(t+1,y) 126 | try: 127 | z = self.subnet[t](x[:, :, t + 1], training=False) / self.bsde.dim 128 | except TypeError: 129 | z = self.subnet(tf.concat([time_stamp[t+1]*all_one_vec,x[:, :, t + 1]],axis=1), training=False) / self.bsde.dim 130 | # terminal time 131 | y = y - self.bsde.delta_t * self.bsde.f_tf(time_stamp[-1], x[:, :, -2], y, z,v_clean[:,:,-2], coll[:,:,-2]) + \ 132 | tf.reduce_sum(z * dw[:, :, -1], 1, keepdims=True) 133 | 134 | history = history.write(self.bsde.num_time_interval,y) 135 | history = tf.transpose(history.stack(),perm=[1,2,0]) 136 | return dw,x,v_clean,coll,history 137 | 138 | def simulate_path(self,num_sample): 139 | return self.predict(num_sample)[4] 140 | 141 | 142 | class FeedForwardSubNet(tf.keras.Model): 143 | def __init__(self, config,dim): 144 | super(FeedForwardSubNet, self).__init__() 145 | num_hiddens = config.net_config.num_hiddens 146 | self.bn_layers = [ 147 | tf.keras.layers.BatchNormalization( 148 | momentum=0.99, 149 | epsilon=1e-6, 150 | beta_initializer=tf.random_normal_initializer(0.0, stddev=0.1), 151 | gamma_initializer=tf.random_uniform_initializer(0.1, 0.5) 152 | ) 153 | for _ in range(len(num_hiddens) + 2)] 154 | self.dense_layers = [tf.keras.layers.Dense(num_hiddens[i], 155 | use_bias=False, 156 | activation=None,) 157 | for i in range(len(num_hiddens))] 158 | # final output should be gradient of size dim 159 | self.dense_layers.append(tf.keras.layers.Dense(dim, activation=None)) 160 | 161 | def call(self, x, training): 162 | """structure: bn -> (dense -> bn -> relu) * len(num_hiddens) -> dense """ 163 | x = self.bn_layers[0](x, training) 164 | for i in range(len(self.dense_layers) - 1): 165 | x = self.dense_layers[i](x) 166 | x = self.bn_layers[i+1](x, training) 167 | x = tf.nn.relu(x) 168 | x = self.dense_layers[-1](x) 169 | return x 170 | 171 | ### univeral neural networks instead of one neural network at each time point 172 | def get_universal_neural_network(input_dim): 173 | input = layers.Input(shape=(input_dim,)) 174 | x = layers.BatchNormalization()(input) 175 | for i in range(5): 176 | x = layers.Dense(input_dim+10,'relu',False)(x) 177 | x = layers.BatchNormalization()(x) 178 | output = layers.Dense(input_dim-1,'relu')(x) 179 | #output = layers.Dense(2*dim,'relu')(x) 180 | return tf.keras.Model(input,output) 181 | ''' 182 | def get_universal_neural_network(input_dim,num_neurons=20,num_hidden_blocks=4): 183 | 184 | input = tf.keras.Input(shape=(input_dim,)) 185 | x = layers.BatchNormalization()(input) 186 | s = layers.Dense(num_neurons,activation='relu',use_bias=False)(x) 187 | s = layers.BatchNormalization()(s) 188 | for i in range(num_hidden_blocks-1): 189 | z = layers.add([layers.Dense(num_neurons,None,False)(x),layers.Dense(num_neurons,None,False)(s)]) 190 | z = Add_bias(num_neurons)(z) 191 | z = layers.Activation(tf.nn.sigmoid)(z) 192 | 193 | g = layers.add([layers.Dense(num_neurons,None,False)(x),layers.Dense(num_neurons,None,False)(s)]) 194 | g = Add_bias(num_neurons)(g) 195 | g = layers.Activation(tf.nn.sigmoid)(g) 196 | r = layers.add([layers.Dense(num_neurons,None,False)(x),layers.Dense(num_neurons,None,False)(s)]) 197 | r = Add_bias(num_neurons)(r) 198 | r = layers.Activation(tf.nn.sigmoid)(r) 199 | h = layers.add([layers.Dense(num_neurons,None,False)(x),layers.Dense(num_neurons,None,False)(layers.multiply([s,r]))]) 200 | h = Add_bias(num_neurons)(h) 201 | h = layers.Activation(tf.nn.relu)(h) 202 | s = layers.add([layers.multiply([1-g,h]),layers.multiply([z,s])]) 203 | s = layers.BatchNormalization()(s) 204 | 205 | output = layers.Dense(input_dim-1,None)(s) 206 | return tf.keras.Model(input,output) 207 | ''' 208 | 209 | class Add_bias(tf.keras.layers.Layer): 210 | def __init__(self,units): 211 | super(Add_bias, self).__init__() 212 | self.units = units 213 | def build(self, input_shape): 214 | self.b = self.add_weight(shape=(self.units,), 215 | initializer='zeros', 216 | trainable=True) 217 | def call(self, inputs): 218 | return inputs + self.b 219 | 220 | 221 | 222 | 223 | -------------------------------------------------------------------------------- /__pycache__/RecursiveEquation.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlessandroGnoatto/Deep-xVA-Solver/4b6efc0caef32424a70739c59b1867e80ab7fd6f/__pycache__/RecursiveEquation.cpython-37.pyc -------------------------------------------------------------------------------- /__pycache__/equation.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlessandroGnoatto/Deep-xVA-Solver/4b6efc0caef32424a70739c59b1867e80ab7fd6f/__pycache__/equation.cpython-37.pyc -------------------------------------------------------------------------------- /__pycache__/solver.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlessandroGnoatto/Deep-xVA-Solver/4b6efc0caef32424a70739c59b1867e80ab7fd6f/__pycache__/solver.cpython-37.pyc -------------------------------------------------------------------------------- /__pycache__/xvaEquation.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlessandroGnoatto/Deep-xVA-Solver/4b6efc0caef32424a70739c59b1867e80ab7fd6f/__pycache__/xvaEquation.cpython-37.pyc -------------------------------------------------------------------------------- /__pycache__/xvaSolver.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlessandroGnoatto/Deep-xVA-Solver/4b6efc0caef32424a70739c59b1867e80ab7fd6f/__pycache__/xvaSolver.cpython-37.pyc -------------------------------------------------------------------------------- /basketCall.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | import tensorflow as tf 4 | from solver import BSDESolver 5 | import xvaEquation as eqn 6 | import munch 7 | import pandas as pd 8 | 9 | 10 | 11 | if __name__ == "__main__": 12 | dim = 100 #dimension of brownian motion 13 | P = 2048 #number of outer Monte Carlo Loops 14 | batch_size = 64 15 | total_time = 1.0 16 | num_time_interval = 200 17 | strike = 100 18 | r = 0.01 19 | sigma = 0.25 20 | x_init = 100 21 | config = { 22 | "eqn_config": { 23 | "_comment": "a basket call option", 24 | "eqn_name": "BasketOption", 25 | "total_time": total_time, 26 | "dim": dim, 27 | "num_time_interval": num_time_interval, 28 | "strike":strike, 29 | "r":r, 30 | "sigma":sigma, 31 | "x_init":x_init, 32 | "strike":strike 33 | 34 | }, 35 | "net_config": { 36 | "y_init_range": [150, 170], 37 | "num_hiddens": [dim+10, dim+10], 38 | "lr_values": [5e-2, 5e-3], 39 | "lr_boundaries": [2000], 40 | "num_iterations": 4000, 41 | "batch_size": batch_size, 42 | "valid_size": 256, 43 | "logging_frequency": 100, 44 | "dtype": "float64", 45 | "verbose": True 46 | } 47 | } 48 | config = munch.munchify(config) 49 | bsde = getattr(eqn, config.eqn_config.eqn_name)(config.eqn_config) 50 | tf.keras.backend.set_floatx(config.net_config.dtype) 51 | 52 | #apply algorithm 1 53 | bsde_solver = BSDESolver(config, bsde) 54 | training_history = bsde_solver.train() 55 | 56 | #Simulate the BSDE after training - MtM scenarios 57 | simulations = bsde_solver.model.simulate_path(bsde.sample(P)) 58 | 59 | #estimated epected positive and negative exposure 60 | time_stamp = np.linspace(0,1,num_time_interval+1) 61 | epe = np.mean(np.exp(-r*time_stamp)*np.maximum(simulations,0),axis=0) 62 | ene = np.mean(np.exp(-r*time_stamp)*np.minimum(simulations,0),axis=0) 63 | exact = 157.9914 #101.0721 #398.08 64 | 65 | epe_exact = np.array([exact for s in time_stamp[1:]]) 66 | ene_exact = np.array([0.0 for s in time_stamp[1:]]) 67 | 68 | fig = plt.figure() 69 | plt.plot(time_stamp,[exact] + list(epe_exact),'b--',label='DEPE = exact solution',) 70 | plt.plot(time_stamp,np.transpose(epe),'b',label='DEPE = deep solver approximation') 71 | 72 | plt.plot(time_stamp,[0.0]+ list(ene_exact),'r--',label='DNPE = exact solution',) 73 | plt.plot(time_stamp,np.transpose(ene),'r',label='DNPE = deep solver approximation') 74 | 75 | plt.xlabel('t') 76 | plt.legend() 77 | 78 | plt.show() 79 | fig.savefig(config.eqn_config.eqn_name + '.pdf',format = 'pdf') 80 | 81 | df = pd.DataFrame(simulations[:,0,:]) 82 | filepath = 'exposure' + config.eqn_config.eqn_name + '.xlsx' 83 | df.to_excel(filepath, index=False) 84 | 85 | 86 | #Check Monte Carlo price 87 | dw, x = bsde.sample(10000) 88 | portfolio = tf.reduce_sum(x, 1,keepdims=True) 89 | payoff=tf.math.maximum(portfolio-dim * strike,0) 90 | average = tf.reduce_mean(payoff,0,keepdims=True) 91 | mcprice = np.exp(- r * total_time) * average[:,:,num_time_interval] 92 | print(mcprice) -------------------------------------------------------------------------------- /basketCallWithCVA.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | import tensorflow as tf 4 | import xvaEquation as eqn 5 | from solver import BSDESolver 6 | from XvaSolver import XvaSolver 7 | import RecursiveEquation as receqn 8 | import munch 9 | import pandas as pd 10 | 11 | 12 | 13 | if __name__ == "__main__": 14 | dim = 100 #dimension of brownian motion 15 | P = 2048 #number of outer Monte Carlo Loops 16 | batch_size = 64 17 | total_time = 1.0 18 | num_time_interval = 100 19 | r = 0.01 20 | sigma = 0.25 21 | x_init = 100 22 | strike = x_init*dim 23 | exact = 157.99 24 | 25 | config = { 26 | 27 | "eqn_config": { 28 | "_comment": "a basket call option", 29 | "eqn_name": "BasketOption", 30 | "total_time": total_time, 31 | "dim": dim, 32 | "num_time_interval": num_time_interval, 33 | "strike":strike, 34 | "r":r, 35 | "sigma":sigma, 36 | "x_init":x_init 37 | }, 38 | 39 | "net_config": { 40 | "y_init_range": [150, 170],#[154.37,165.41], #set to None when not sure 41 | "num_hiddens": [dim+10, dim+10], 42 | "lr_values": [5e-2, 5e-3],#[5e-1,5e-2, 5e-3], 43 | "lr_boundaries": [2000],#[1000,2000], 44 | "num_iterations": 4000, 45 | "batch_size": batch_size, 46 | "valid_size": 128, 47 | "logging_frequency": 100, 48 | "dtype": "float64", 49 | "verbose": True 50 | } 51 | } 52 | config = munch.munchify(config) 53 | bsde = getattr(eqn, config.eqn_config.eqn_name)(config.eqn_config) 54 | tf.keras.backend.set_floatx(config.net_config.dtype) 55 | 56 | #apply algorithm 1 57 | bsde_solver = BSDESolver(config, bsde) 58 | training_history = bsde_solver.train() 59 | 60 | #Simulate the BSDE after training - MtM scenarios 61 | simulations = bsde_solver.model.simulate_path(bsde.sample(P)) 62 | 63 | #estimated epected positive and negative exposure 64 | time_stamp = np.linspace(0,1,num_time_interval+1) 65 | epe = np.mean(np.exp(-r*time_stamp)*np.maximum(simulations,0),axis=0) 66 | ene = np.mean(np.exp(-r*time_stamp)*np.minimum(simulations,0),axis=0) 67 | 68 | epe_exact = np.array([exact for s in time_stamp[1:]]) 69 | ene_exact = np.array([0.0 for s in time_stamp[1:]]) 70 | 71 | fig = plt.figure() 72 | plt.plot(time_stamp,[exact] + list(epe_exact),'b--',label='DEPE = exact solution',) 73 | plt.plot(time_stamp,np.transpose(epe),'b',label='DEPE = deep solver approximation') 74 | 75 | plt.plot(time_stamp,[0.0]+ list(ene_exact),'r--',label='DNPE = exact solution',) 76 | plt.plot(time_stamp,np.transpose(ene),'r',label='DNPE = deep solver approximation') 77 | 78 | plt.xlabel('t') 79 | plt.legend() 80 | 81 | plt.show() 82 | fig.savefig(config.eqn_config.eqn_name + '.pdf',format = 'pdf') 83 | 84 | df = pd.DataFrame(simulations[:,0,:]) 85 | filepath = 'exposure' + config.eqn_config.eqn_name + '.xlsx' 86 | df.to_excel(filepath, index=False) 87 | 88 | configBCVA = { 89 | "eqn_config": { 90 | "_comment": "BCVA on a basket call", 91 | "eqn_name": "BCVA", 92 | "dim": dim, 93 | "total_time": total_time, 94 | "num_time_interval": num_time_interval, 95 | "r":r, 96 | "recoveryC" : 0.3, 97 | "lambdaC" : 0.1, 98 | "recoveryB" : 0.4, 99 | "lambdaB" : 0.01, 100 | "clean_value": bsde, 101 | "clean_value_model": bsde_solver.model 102 | }, 103 | "net_config": { 104 | "y_init_range": [0, 20], 105 | "num_hiddens": [dim+10, dim+10], 106 | "lr_values": [5e-2, 5e-3], 107 | "lr_boundaries": [2000], 108 | "num_iterations": 4000, 109 | "batch_size": batch_size, 110 | "valid_size": 128, 111 | "logging_frequency": 100, 112 | "dtype": "float64", 113 | "verbose": True 114 | } 115 | } 116 | configBCVA = munch.munchify(configBCVA) 117 | bcvabsde = getattr(receqn, configBCVA.eqn_config.eqn_name)(configBCVA.eqn_config) 118 | tf.keras.backend.set_floatx(configBCVA.net_config.dtype) 119 | 120 | #apply algorithm 3 121 | bcva_solver = XvaSolver(configBCVA, bcvabsde) 122 | #loss: 1.7611e-01, Y0: 6.9664e-01, 123 | bcva_training_history = bcva_solver.train() 124 | 125 | bcva_simulations = bcva_solver.model.simulate_path(bcvabsde.sample(P)) 126 | #(0.699395244753698, [0.6903630282972714, 0.7084274612101246]) 127 | print(bcvabsde.monte_carlo(100000)) -------------------------------------------------------------------------------- /callOption.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | import tensorflow as tf 4 | from solver import BSDESolver 5 | import xvaEquation as eqn 6 | import munch 7 | import pandas as pd 8 | 9 | 10 | 11 | if __name__ == "__main__": 12 | dim = 1 #dimension of brownian motion 13 | P = 2048 #number of outer Monte Carlo Loops 14 | batch_size = 64 15 | total_time = 1.0 16 | num_time_interval = 200 17 | strike = 100 18 | r = 0.01 19 | sigma = 0.25 20 | x_init = 100 21 | config = { 22 | "eqn_config": { 23 | "_comment": "a basket call option", 24 | "eqn_name": "CallOption", 25 | "total_time": total_time, 26 | "dim": dim, 27 | "num_time_interval": num_time_interval, 28 | "strike":strike, 29 | "r":r, 30 | "sigma":sigma, 31 | "x_init":x_init, 32 | "strike":strike 33 | 34 | }, 35 | "net_config": { 36 | "y_init_range": [9, 11], 37 | "num_hiddens": [dim+20, dim+20], 38 | "lr_values": [5e-2, 5e-3], 39 | "lr_boundaries": [2000], 40 | "num_iterations": 4000, 41 | "batch_size": batch_size, 42 | "valid_size": 1024, 43 | "logging_frequency": 100, 44 | "dtype": "float64", 45 | "verbose": True 46 | } 47 | } 48 | config = munch.munchify(config) 49 | bsde = getattr(eqn, config.eqn_config.eqn_name)(config.eqn_config) 50 | tf.keras.backend.set_floatx(config.net_config.dtype) 51 | 52 | #apply algorithm 1 53 | bsde_solver = BSDESolver(config, bsde) 54 | training_history = bsde_solver.train() 55 | 56 | #Simulate the BSDE after training - MtM scenarios 57 | simulations = bsde_solver.model.simulate(bsde.sample(P)) 58 | 59 | #estimated epected positive and negative exposure 60 | time_stamp = np.linspace(0,1,num_time_interval+1) 61 | epe = np.mean(np.exp(-r*time_stamp)*np.maximum(simulations,0),axis=0) 62 | ene = np.mean(np.exp(-r*time_stamp)*np.minimum(simulations,0),axis=0) 63 | 64 | 65 | 66 | exact = bsde.SolExact(0,x_init) 67 | epe_exact = np.array([exact for s in time_stamp[1:]]) 68 | ene_exact = np.array([0.0 for s in time_stamp[1:]]) 69 | 70 | fig = plt.figure() 71 | plt.plot(time_stamp,[exact] + list(epe_exact),'b--',label='DEPE = exact solution',) 72 | plt.plot(time_stamp,epe,'b',label='DEPE = deep solver approximation') 73 | 74 | plt.plot(time_stamp,[0.0]+ list(ene_exact),'r--',label='DNPE = exact solution',) 75 | plt.plot(time_stamp,ene,'r',label='DNPE = deep solver approximation') 76 | 77 | plt.xlabel('t') 78 | plt.legend() 79 | 80 | plt.show() 81 | fig.savefig(config.eqn_config.eqn_name + '.pdf',format = 'pdf') 82 | 83 | df = pd.DataFrame(simulations[:,0,:]) 84 | filepath = 'exposure' + config.eqn_config.eqn_name + '.xlsx' 85 | df.to_excel(filepath, index=False) 86 | -------------------------------------------------------------------------------- /callOptionStabilityTests.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | import tensorflow as tf 4 | from solver import BSDESolver 5 | import xvaEquation as eqn 6 | import munch 7 | import pandas as pd 8 | 9 | 10 | 11 | if __name__ == "__main__": 12 | dim = 1 #dimension of brownian motion 13 | P = 2048 #number of outer Monte Carlo Loops 14 | batch_size = 64 15 | total_time = 1.0 16 | num_time_interval = 200 17 | strike = 100 18 | r = 0.01 19 | sigma = 0.25 20 | x_init = 100 21 | config = { 22 | "eqn_config": { 23 | "_comment": "a basket call option", 24 | "eqn_name": "CallOption", 25 | "total_time": total_time, 26 | "dim": dim, 27 | "num_time_interval": num_time_interval, 28 | "strike":strike, 29 | "r":r, 30 | "sigma":sigma, 31 | "x_init":x_init, 32 | "strike":strike 33 | 34 | }, 35 | "net_config": { 36 | "y_init_range": [2, 20], 37 | "num_hiddens": [dim + 80, dim + 80, dim + 80, dim + 80, dim + 80, dim + 80, dim + 80, dim + 80], 38 | "lr_values": [5e-3, 5e-3], 39 | "lr_boundaries": [2000], 40 | "num_iterations": 4000, 41 | "batch_size": batch_size, 42 | "valid_size": 1024, 43 | "logging_frequency": 100, 44 | "dtype": "float64", 45 | "verbose": True 46 | } 47 | } 48 | config = munch.munchify(config) 49 | bsde = getattr(eqn, config.eqn_config.eqn_name)(config.eqn_config) 50 | tf.keras.backend.set_floatx(config.net_config.dtype) 51 | 52 | #apply algorithm 1 53 | bsde_solver = BSDESolver(config, bsde) 54 | training_history = bsde_solver.train() 55 | 56 | #Simulate the BSDE after training - MtM scenarios 57 | simulations = bsde_solver.model.simulate_path(bsde.sample(P)) 58 | 59 | #estimated epected positive and negative exposure 60 | time_stamp = np.linspace(0,1,num_time_interval+1) 61 | epe = np.mean(np.exp(-r*time_stamp)*np.maximum(simulations,0),axis=0) 62 | ene = np.mean(np.exp(-r*time_stamp)*np.minimum(simulations,0),axis=0) 63 | 64 | 65 | 66 | exact = bsde.SolExact(0,x_init) 67 | epe_exact = np.array([exact for s in time_stamp[1:]]) 68 | ene_exact = np.array([0.0 for s in time_stamp[1:]]) 69 | 70 | 71 | fig = plt.figure() 72 | plt.plot(time_stamp,[exact]+list(epe_exact),'b--',label='DEPE = exact solution') 73 | plt.plot(time_stamp,np.transpose(epe),'b',label='DEPE = deep solver approximation') 74 | 75 | plt.plot(time_stamp,[0.0]+list(ene_exact),'r--',label='DNPE = exact solution') 76 | plt.plot(time_stamp,np.transpose(ene),'r',label='DNPE = deep solver approximation') 77 | 78 | plt.xlabel('t') 79 | plt.legend() 80 | 81 | plt.show() 82 | fig.savefig(config.eqn_config.eqn_name + '.pdf',format = 'pdf') 83 | 84 | df = pd.DataFrame(simulations[:,0,:]) 85 | filepath = 'exposure' + config.eqn_config.eqn_name + '.xlsx' 86 | df.to_excel(filepath, index=False) 87 | -------------------------------------------------------------------------------- /equation.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | class Equation(object): 5 | """Base class for defining PDE related function.""" 6 | 7 | def __init__(self, eqn_config): 8 | self.dim = eqn_config.dim 9 | self.total_time = eqn_config.total_time 10 | self.num_time_interval = eqn_config.num_time_interval 11 | self.delta_t = self.total_time / self.num_time_interval 12 | self.sqrt_delta_t = np.sqrt(self.delta_t) 13 | self.y_init = None 14 | 15 | def sample(self, num_sample): 16 | """Sample forward SDE.""" 17 | raise NotImplementedError 18 | 19 | def f_tf(self, t, x, y, z): 20 | """Generator function in the PDE.""" 21 | raise NotImplementedError 22 | 23 | def g_tf(self, t, x): 24 | """Terminal condition of the PDE.""" 25 | raise NotImplementedError 26 | 27 | -------------------------------------------------------------------------------- /exposureBasketOption.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlessandroGnoatto/Deep-xVA-Solver/4b6efc0caef32424a70739c59b1867e80ab7fd6f/exposureBasketOption.xlsx -------------------------------------------------------------------------------- /exposureCallOption.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlessandroGnoatto/Deep-xVA-Solver/4b6efc0caef32424a70739c59b1867e80ab7fd6f/exposureCallOption.xlsx -------------------------------------------------------------------------------- /exposurePricingForward.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlessandroGnoatto/Deep-xVA-Solver/4b6efc0caef32424a70739c59b1867e80ab7fd6f/exposurePricingForward.xlsx -------------------------------------------------------------------------------- /forward.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | import tensorflow as tf 4 | from solver import BSDESolver 5 | import xvaEquation as eqn 6 | import munch 7 | from scipy.stats import norm 8 | import pandas as pd 9 | 10 | 11 | 12 | if __name__ == "__main__": 13 | dim = 1 #dimension of brownian motion 14 | P = 2048 #number of outer Monte Carlo Loops 15 | batch_size = 64 16 | total_time = 1.0 17 | num_time_interval=200 18 | strike = 100 19 | r = 0.0 20 | mu = 0.0 21 | sigma=0.25 22 | x_init=100 23 | config = { 24 | "eqn_config": { 25 | "_comment": "a forward contract", 26 | "eqn_name": "PricingForward", 27 | "total_time": total_time, 28 | "dim": dim, 29 | "num_time_interval": num_time_interval, 30 | "strike":strike, 31 | "r":r, 32 | "sigma":sigma, 33 | "x_init":x_init 34 | 35 | }, 36 | "net_config": { 37 | "y_init_range": [-5, 5], 38 | "num_hiddens": [dim+20, dim+20], 39 | "lr_values": [5e-2, 5e-3], 40 | "lr_boundaries": [2000], 41 | "num_iterations": 4000, 42 | "batch_size": batch_size, 43 | "valid_size": 256, 44 | "logging_frequency": 100, 45 | "dtype": "float64", 46 | "verbose": True 47 | } 48 | } 49 | config = munch.munchify(config) 50 | bsde = getattr(eqn, config.eqn_config.eqn_name)(config.eqn_config) 51 | tf.keras.backend.set_floatx(config.net_config.dtype) 52 | 53 | #apply algorithm 1 54 | bsde_solver = BSDESolver(config, bsde) 55 | training_history = bsde_solver.train() 56 | 57 | #Simulate the BSDE after training - MtM scenarios 58 | simulations = bsde_solver.model.simulate_path(bsde.sample(P)) 59 | 60 | #estimated epected positive and negative exposure 61 | time_stamp = np.linspace(0,1,num_time_interval+1) 62 | epe = np.mean(np.exp(-r*time_stamp)*np.maximum(simulations,0),axis=0) 63 | ene = np.mean(np.exp(-r*time_stamp)*np.minimum(simulations,0),axis=0) 64 | 65 | #exact solution 66 | rv = norm() 67 | 68 | d1 = np.array([(-r * s + np.log(x_init/strike) + (r+sigma**2/2)*s)/sigma/np.sqrt(s) 69 | for s in time_stamp[1:]]) 70 | d2 = np.array([d1[i]-sigma*np.sqrt(s) for i,s in enumerate(time_stamp[1:])]) 71 | 72 | epe_exact = x_init*rv.cdf(d1) - strike*np.exp(-r)*rv.cdf(d2) 73 | ene_exact = x_init*rv.cdf(-d1) - strike*np.exp(-r)*rv.cdf(-d2) 74 | 75 | fig = plt.figure() 76 | plt.plot(time_stamp,[0.0]+list(epe_exact),'b--',label='DEPE = exact solution') 77 | plt.plot(time_stamp,np.transpose(epe),'b',label='DEPE = deep solver approximation') 78 | 79 | plt.plot(time_stamp,[0.0]+list(ene_exact),'r--',label='DNPE = exact solution') 80 | plt.plot(time_stamp,np.transpose(ene),'r',label='DNPE = deep solver approximation') 81 | 82 | plt.xlabel('t') 83 | plt.legend() 84 | 85 | plt.show() 86 | fig.savefig(config.eqn_config.eqn_name + '.pdf',format = 'pdf') 87 | 88 | df = pd.DataFrame(simulations[:,0,:]) 89 | filepath = 'exposure' + config.eqn_config.eqn_name + '.xlsx' 90 | df.to_excel(filepath, index=False) 91 | -------------------------------------------------------------------------------- /forward.sav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlessandroGnoatto/Deep-xVA-Solver/4b6efc0caef32424a70739c59b1867e80ab7fd6f/forward.sav -------------------------------------------------------------------------------- /fvaForward.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | import tensorflow as tf 4 | from solver import BSDESolver 5 | from XvaSolver import XvaSolver 6 | import xvaEquation as eqn 7 | import RecursiveEquation as receqn 8 | import munch 9 | from scipy.stats import norm 10 | 11 | 12 | 13 | if __name__ == "__main__": 14 | dim = 1 #dimension of brownian motion 15 | P = 2048 #number of outer Monte Carlo Loops 16 | batch_size = 64 17 | total_time = 1.0 18 | num_time_interval=100 19 | strike = 100 20 | r = 0.02 21 | sigma=0.25 22 | x_init=100 23 | config = { 24 | "eqn_config": { 25 | "_comment": "a forward contract", 26 | "eqn_name": "PricingForward", 27 | "total_time": total_time, 28 | "dim": dim, 29 | "num_time_interval": num_time_interval, 30 | "strike":strike, 31 | "r":r, 32 | "sigma":sigma, 33 | "x_init":x_init 34 | 35 | }, 36 | "net_config": { 37 | "y_init_range": [-5, 5], 38 | "num_hiddens": [dim+20, dim+20], 39 | "lr_values": [5e-2, 5e-3], 40 | "lr_boundaries": [2000], 41 | "num_iterations": 4000, 42 | "batch_size": batch_size, 43 | "valid_size": 256, 44 | "logging_frequency": 100, 45 | "dtype": "float64", 46 | "verbose": True 47 | } 48 | } 49 | config = munch.munchify(config) 50 | bsde = getattr(eqn, config.eqn_config.eqn_name)(config.eqn_config) 51 | tf.keras.backend.set_floatx(config.net_config.dtype) 52 | 53 | #apply algorithm 1 54 | bsde_solver = BSDESolver(config, bsde) 55 | training_history = bsde_solver.train() 56 | 57 | 58 | #apply trained model to evaluate value of the forward contract via Monte Carlo 59 | simulations = bsde_solver.model.simulate_path(bsde.sample(P)) 60 | 61 | #estimated epected positive and negative exposure 62 | time_stamp = np.linspace(0,1,num_time_interval+1) 63 | epe = np.mean(np.exp(-r*time_stamp)*np.maximum(simulations,0),axis=0) 64 | ene = np.mean(np.exp(-r*time_stamp)*np.minimum(simulations,0),axis=0) 65 | 66 | #exact solution 67 | rv = norm() 68 | 69 | d1 = np.array([(-r * s + np.log(x_init/strike) + (r+sigma**2/2)*s)/sigma/np.sqrt(s) 70 | for s in time_stamp[1:]]) 71 | d2 = np.array([d1[i]-sigma*np.sqrt(s) for i,s in enumerate(time_stamp[1:])]) 72 | 73 | epe_exact = x_init*rv.cdf(d1) - strike*np.exp(-r)*rv.cdf(d2) 74 | ene_exact = x_init*rv.cdf(-d1) - strike*np.exp(-r)*rv.cdf(-d2) 75 | 76 | plt.figure() 77 | plt.plot(time_stamp,[epe_exact[0]]+list(epe_exact),'b--',label='DEPE = exact solution') 78 | plt.plot(time_stamp,epe[0],'b',label='DEPE = deep solver approximation') 79 | 80 | plt.plot(time_stamp,[ene_exact[0]]+list(ene_exact),'r--',label='DNPE = exact solution') 81 | plt.plot(time_stamp,ene[0],'r',label='DNPE = deep solver approximation') 82 | 83 | plt.xlabel('t') 84 | plt.legend() 85 | 86 | plt.show() 87 | 88 | # bsde_solver.model.save('testmodel.tf',save_format='tf') 89 | 90 | # XVA computation step. 91 | r_f = 0.04 92 | configFVA = { 93 | "eqn_config": { 94 | "_comment": "XVA on a forward", 95 | "eqn_name": "FVA", 96 | "total_time": total_time, 97 | "num_time_interval": num_time_interval, 98 | "r":r, 99 | "r_fl": r_f, 100 | "r_fb": r_f, 101 | "r_cl": 0.00, 102 | "r_cl": 0.00, 103 | "clean_value": bsde, 104 | "clean_value_model": bsde_solver.model 105 | }, 106 | "net_config": { 107 | "y_init_range": [-5, 5], 108 | "num_hiddens": [dim+20, dim+20], 109 | "lr_values": [5e-2, 5e-3], 110 | "lr_boundaries": [2000], 111 | "num_iterations": 4000, 112 | "batch_size": batch_size, 113 | "valid_size": 256, 114 | "logging_frequency": 100, 115 | "dtype": "float64", 116 | "verbose": True 117 | } 118 | } 119 | configFVA = munch.munchify(configFVA) 120 | fvabsde = getattr(receqn, configFVA.eqn_config.eqn_name)(configFVA.eqn_config) 121 | tf.keras.backend.set_floatx(configFVA.net_config.dtype) 122 | 123 | #apply algorithm 3 124 | xva_solver = XvaSolver(config, fvabsde) 125 | xva_training_history = xva_solver.train() 126 | 127 | fva_simulations = xva_solver.model.simulate_path(fvabsde.sample(P)) 128 | 129 | print("Exact Values from analytic formulas") 130 | exactVhat = x_init - strike*np.exp(-r * total_time) 131 | exactV = np.exp(-(r_f - r) * total_time)*x_init - strike*np.exp(-r_f * total_time) 132 | exactFVA = exactVhat - exactV 133 | print("exactV = " + str(exactV)) 134 | print("exactVhat = " + str(exactVhat)) 135 | print("exactFVA = " + str(exactFVA)) 136 | 137 | 138 | print("FVA from Algorithm 3") 139 | fvaFromSolver = fva_simulations[0,0,0] 140 | print("fvaFromSolver = " +str(fvaFromSolver) ) 141 | fvaError = fva_simulations[0,0,0] - exactFVA 142 | print("error = "+ str(fvaError)) 143 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /solver.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | import numpy as np 4 | import tensorflow as tf 5 | from tqdm import tqdm 6 | import tensorflow.keras.layers as layers 7 | DELTA_CLIP = 50.0 8 | 9 | 10 | class BSDESolver(object): 11 | """The fully connected neural network model.""" 12 | def __init__(self, config, bsde): 13 | self.eqn_config = config.eqn_config 14 | self.net_config = config.net_config 15 | self.bsde = bsde 16 | 17 | self.model = NonsharedModel(config, bsde) 18 | #self.y_init = self.model.y_init 19 | 20 | try: 21 | lr_schedule = config.net_config.lr_schedule 22 | except AttributeError: 23 | lr_schedule = tf.keras.optimizers.schedules.PiecewiseConstantDecay( 24 | self.net_config.lr_boundaries, self.net_config.lr_values) 25 | 26 | self.optimizer = tf.keras.optimizers.Adam(learning_rate=lr_schedule, epsilon=1e-8) 27 | 28 | def train(self): 29 | start_time = time.time() 30 | training_history = [] 31 | valid_data = self.bsde.sample(self.net_config.valid_size) 32 | 33 | # begin sgd iteration 34 | for step in tqdm(range(self.net_config.num_iterations+1)): 35 | if step % self.net_config.logging_frequency == 0: 36 | loss = self.loss_fn(valid_data, training=False).numpy() 37 | y_init = self.model.y_init.numpy()[0] 38 | elapsed_time = time.time() - start_time 39 | training_history.append([step, loss, y_init, elapsed_time]) 40 | if self.net_config.verbose: 41 | #logging.info("step: %5u, loss: %.4e, Y0: %.4e, elapsed time: %3u" % ( 42 | # step, loss, y_init, elapsed_time)) 43 | print("step: %5u, loss: %.4e, Y0: %.4e, elapsed time: %3u" % ( 44 | step, loss, y_init, elapsed_time)) 45 | self.train_step(self.bsde.sample(self.net_config.batch_size)) 46 | return np.array(training_history) 47 | 48 | def loss_fn(self, inputs, training): 49 | 50 | dw, x = inputs 51 | y_terminal = self.model(inputs, training) 52 | delta = y_terminal - self.bsde.g_tf(self.bsde.total_time, x[:, :, -1]) 53 | # use linear approximation outside the clipped range 54 | loss = tf.reduce_mean(tf.where(tf.abs(delta) < DELTA_CLIP, tf.square(delta), 55 | 2 * DELTA_CLIP * tf.abs(delta) - DELTA_CLIP ** 2)) 56 | 57 | loss += 1000*(tf.maximum(self.model.y_init[0]-self.net_config.y_init_range[1],0)+tf.maximum(self.net_config.y_init_range[0]-self.model.y_init[0],0)) 58 | return loss 59 | 60 | def grad(self, inputs, training): 61 | with tf.GradientTape(persistent=True) as tape: 62 | loss = self.loss_fn(inputs, training) 63 | grad = tape.gradient(loss, self.model.trainable_variables) 64 | del tape 65 | return grad 66 | 67 | @tf.function 68 | def train_step(self, train_data): 69 | grad = self.grad(train_data, training=True) 70 | self.optimizer.apply_gradients(zip(grad, self.model.trainable_variables)) 71 | 72 | 73 | class NonsharedModel(tf.keras.Model): 74 | def __init__(self, config, bsde): 75 | super(NonsharedModel, self).__init__() 76 | self.config = config 77 | self.eqn_config = config.eqn_config 78 | self.net_config = config.net_config 79 | self.bsde = bsde 80 | self.dim = bsde.dim 81 | self.y_init = tf.Variable(np.random.uniform(low=self.net_config.y_init_range[0], 82 | high=self.net_config.y_init_range[1], 83 | size=[1]),dtype=self.net_config.dtype 84 | ) 85 | self.z_init = tf.Variable(np.random.uniform(low=-.1, high=.1, 86 | size=[1, self.eqn_config.dim]),dtype=self.net_config.dtype 87 | ) 88 | 89 | self.subnet = [FeedForwardSubNet(config,bsde.dim) for _ in range(self.bsde.num_time_interval-1)] 90 | 91 | def call(self, inputs, training): 92 | dw, x = inputs 93 | time_stamp = np.arange(0, self.eqn_config.num_time_interval) * self.bsde.delta_t 94 | all_one_vec = tf.ones(shape=tf.stack([tf.shape(dw)[0], 1]), dtype=self.net_config.dtype) 95 | y = all_one_vec * self.y_init 96 | z = tf.matmul(all_one_vec, self.z_init) 97 | 98 | for t in range(0, self.bsde.num_time_interval-1): 99 | y = y - self.bsde.delta_t * ( 100 | self.bsde.f_tf(time_stamp[t], x[:, :, t], y, z) 101 | ) + tf.reduce_sum(z * dw[:, :, t], 1, keepdims=True) 102 | try: 103 | z = self.subnet[t](x[:, :, t + 1], training) / self.bsde.dim 104 | except TypeError: 105 | z = self.subnet(tf.concat([time_stamp[t+1]*all_one_vec,x[:, :, t + 1]],axis=1), training=training) / self.bsde.dim 106 | # terminal time 107 | y = y - self.bsde.delta_t * self.bsde.f_tf(time_stamp[-1], x[:, :, -2], y, z) + \ 108 | tf.reduce_sum(z * dw[:, :, -1], 1, keepdims=True) 109 | return y 110 | 111 | def predict_step(self, data): 112 | dw, x = data[0] 113 | time_stamp = np.arange(0, self.eqn_config.num_time_interval) * self.bsde.delta_t 114 | all_one_vec = tf.ones(shape=tf.stack([tf.shape(dw)[0], 1]), dtype=self.net_config.dtype) 115 | y = all_one_vec * self.y_init 116 | z = tf.matmul(all_one_vec, self.z_init) 117 | 118 | history = tf.TensorArray(self.net_config.dtype,size=self.bsde.num_time_interval+1) 119 | history = history.write(0,y) 120 | 121 | for t in range(0, self.bsde.num_time_interval-1): 122 | y = y - self.bsde.delta_t * ( 123 | self.bsde.f_tf(time_stamp[t], x[:, :, t], y, z) 124 | ) + tf.reduce_sum(z * dw[:, :, t], 1, keepdims=True) 125 | 126 | history = history.write(t+1,y) 127 | try: 128 | z = self.subnet[t](x[:, :, t + 1], training=False) / self.bsde.dim 129 | except TypeError: 130 | z = self.subnet(tf.concat([time_stamp[t+1]*all_one_vec,x[:, :, t + 1]],axis=1), training=False) / self.bsde.dim 131 | # terminal time 132 | y = y - self.bsde.delta_t * self.bsde.f_tf(time_stamp[-1], x[:, :, -2], y, z) + \ 133 | tf.reduce_sum(z * dw[:, :, -1], 1, keepdims=True) 134 | 135 | history = history.write(self.bsde.num_time_interval,y) 136 | history = tf.transpose(history.stack(),perm=[1,2,0]) 137 | return dw,x,history 138 | 139 | def simulate_path(self,num_sample): 140 | return self.predict(num_sample)[2] 141 | 142 | 143 | class FeedForwardSubNet(tf.keras.Model): 144 | def __init__(self, config,dim): 145 | super(FeedForwardSubNet, self).__init__() 146 | num_hiddens = config.net_config.num_hiddens 147 | self.bn_layers = [ 148 | tf.keras.layers.BatchNormalization( 149 | momentum=0.99, 150 | epsilon=1e-6, 151 | beta_initializer=tf.random_normal_initializer(0.0, stddev=0.1), 152 | gamma_initializer=tf.random_uniform_initializer(0.1, 0.5) 153 | ) 154 | for _ in range(len(num_hiddens) + 2)] 155 | self.dense_layers = [tf.keras.layers.Dense(num_hiddens[i], 156 | use_bias=False, 157 | activation=None,) 158 | for i in range(len(num_hiddens))] 159 | # final output should be gradient of size dim 160 | self.dense_layers.append(tf.keras.layers.Dense(dim, activation=None)) 161 | 162 | def call(self, x, training): 163 | """structure: bn -> (dense -> bn -> relu) * len(num_hiddens) -> dense """ 164 | x = self.bn_layers[0](x, training) 165 | for i in range(len(self.dense_layers) - 1): 166 | x = self.dense_layers[i](x) 167 | x = self.bn_layers[i+1](x, training) 168 | x = tf.nn.relu(x) 169 | x = self.dense_layers[-1](x) 170 | return x 171 | 172 | ### univeral neural networks instead of one neural network at each time point 173 | def get_universal_neural_network(input_dim): 174 | input = layers.Input(shape=(input_dim,)) 175 | x = layers.BatchNormalization()(input) 176 | for i in range(5): 177 | x = layers.Dense(input_dim+10,'relu',False)(x) 178 | x = layers.BatchNormalization()(x) 179 | output = layers.Dense(input_dim-1,'relu')(x) 180 | #output = layers.Dense(2*dim,'relu')(x) 181 | return tf.keras.Model(input,output) 182 | ''' 183 | def get_universal_neural_network(input_dim,num_neurons=20,num_hidden_blocks=4): 184 | 185 | input = tf.keras.Input(shape=(input_dim,)) 186 | x = layers.BatchNormalization()(input) 187 | s = layers.Dense(num_neurons,activation='relu',use_bias=False)(x) 188 | s = layers.BatchNormalization()(s) 189 | for i in range(num_hidden_blocks-1): 190 | z = layers.add([layers.Dense(num_neurons,None,False)(x),layers.Dense(num_neurons,None,False)(s)]) 191 | z = Add_bias(num_neurons)(z) 192 | z = layers.Activation(tf.nn.sigmoid)(z) 193 | 194 | g = layers.add([layers.Dense(num_neurons,None,False)(x),layers.Dense(num_neurons,None,False)(s)]) 195 | g = Add_bias(num_neurons)(g) 196 | g = layers.Activation(tf.nn.sigmoid)(g) 197 | r = layers.add([layers.Dense(num_neurons,None,False)(x),layers.Dense(num_neurons,None,False)(s)]) 198 | r = Add_bias(num_neurons)(r) 199 | r = layers.Activation(tf.nn.sigmoid)(r) 200 | h = layers.add([layers.Dense(num_neurons,None,False)(x),layers.Dense(num_neurons,None,False)(layers.multiply([s,r]))]) 201 | h = Add_bias(num_neurons)(h) 202 | h = layers.Activation(tf.nn.relu)(h) 203 | s = layers.add([layers.multiply([1-g,h]),layers.multiply([z,s])]) 204 | s = layers.BatchNormalization()(s) 205 | 206 | output = layers.Dense(input_dim-1,None)(s) 207 | return tf.keras.Model(input,output) 208 | ''' 209 | 210 | class Add_bias(tf.keras.layers.Layer): 211 | def __init__(self,units): 212 | super(Add_bias, self).__init__() 213 | self.units = units 214 | def build(self, input_shape): 215 | self.b = self.add_weight(shape=(self.units,), 216 | initializer='zeros', 217 | trainable=True) 218 | def call(self, inputs): 219 | return inputs + self.b 220 | 221 | 222 | 223 | 224 | -------------------------------------------------------------------------------- /xvaEquation.py: -------------------------------------------------------------------------------- 1 | from equation import Equation 2 | import numpy as np 3 | import tensorflow as tf 4 | from scipy.stats import multivariate_normal as normal 5 | 6 | class PricingForward(Equation): 7 | def __init__(self,eqn_config): 8 | super(PricingForward, self).__init__(eqn_config) 9 | self.strike = eqn_config.strike 10 | self.x_init = np.ones(self.dim) * eqn_config.x_init # initial value of x, the underlying 11 | self.sigma = eqn_config.sigma 12 | self.r = eqn_config.r 13 | self.useExplict = False #whether to use explict formula to evaluate dyanamics of x 14 | 15 | 16 | def sample(self, num_sample): 17 | 18 | dw_sample = normal.rvs(size=[num_sample, 19 | self.dim, 20 | self.num_time_interval]) * self.sqrt_delta_t 21 | if self.dim==1: 22 | dw_sample = np.expand_dims(dw_sample,axis=0) 23 | dw_sample = np.swapaxes(dw_sample,0,1) 24 | 25 | x_sample = np.zeros([num_sample, self.dim, self.num_time_interval + 1]) 26 | x_sample[:, :, 0] = np.ones([num_sample, self.dim]) * self.x_init 27 | 28 | if self.useExplict: #use analytic solution of linear SDE 29 | factor = np.exp((self.r-(self.sigma**2)/2)*self.delta_t) 30 | for i in range(self.num_time_interval): 31 | x_sample[:, :, i + 1] = (factor * np.exp(self.sigma * dw_sample[:, :, i])) * x_sample[:, :, i] 32 | else: #use Euler-Maruyama scheme 33 | for i in range(self.num_time_interval): 34 | x_sample[:, :, i + 1] = (1 + self.r * self.delta_t) * x_sample[:, :, i] + (self.sigma * x_sample[:, :, i] * dw_sample[:, :, i]) 35 | 36 | 37 | return dw_sample, x_sample 38 | 39 | 40 | def f_tf(self, t, x, y, z): 41 | return -self.r * y 42 | 43 | def g_tf(self, t, x): 44 | return x - self.strike 45 | 46 | class BasketOption(Equation): 47 | def __init__(self, eqn_config): 48 | super(BasketOption, self).__init__(eqn_config) 49 | self.strike = eqn_config.strike 50 | self.x_init = np.ones(self.dim) * eqn_config.x_init # initial value of x, the underlying 51 | self.sigma = eqn_config.sigma # volatility 52 | self.rate = eqn_config.r # risk-free rate 53 | self.useExplict = True #whether to use explict formula to evaluate dyanamics of x 54 | 55 | def sample(self, num_sample,use_gpu=False): 56 | 57 | dw_sample = normal.rvs(size=[num_sample, 58 | self.dim, 59 | self.num_time_interval]) * self.sqrt_delta_t 60 | if self.dim==1: 61 | dw_sample = np.expand_dims(dw_sample,axis=0) 62 | dw_sample = np.swapaxes(dw_sample,0,1) 63 | 64 | x_sample = np.zeros([num_sample, self.dim, self.num_time_interval + 1]) 65 | x_sample[:, :, 0] = np.ones([num_sample, self.dim]) * self.x_init 66 | 67 | if self.useExplict: #use analytic solution of linear SDE 68 | factor = np.exp((self.rate-(self.sigma**2)/2)*self.delta_t) 69 | for i in range(self.num_time_interval): 70 | x_sample[:, :, i + 1] = (factor * np.exp(self.sigma * dw_sample[:, :, i])) * x_sample[:, :, i] 71 | else: #use Euler-Maruyama scheme 72 | for i in range(self.num_time_interval): 73 | x_sample[:, :, i + 1] = (1 + self.rate * self.delta_t) * x_sample[:, :, i] + (self.sigma * x_sample[:, :, i] * dw_sample[:, :, i]) 74 | 75 | return dw_sample, x_sample 76 | 77 | def f_tf(self, t, x, y, z): 78 | return -self.rate * y 79 | 80 | def g_tf(self, t, x): 81 | temp = tf.reduce_sum(x, 1,keepdims=True) 82 | return tf.maximum(temp - self.strike, 0) 83 | 84 | class CallOption(Equation): 85 | def __init__(self, eqn_config): 86 | super(CallOption, self).__init__(eqn_config) 87 | self.strike = eqn_config.strike 88 | self.x_init = np.ones(self.dim) * eqn_config.x_init 89 | self.sigma = eqn_config.sigma 90 | self.r = eqn_config.r 91 | self.useExplict = False #whether to use explict formula to evaluate dyanamics of x 92 | 93 | def sample(self, num_sample): 94 | 95 | dw_sample = normal.rvs(size=[num_sample, 96 | self.dim, 97 | self.num_time_interval]) * self.sqrt_delta_t 98 | if self.dim==1: 99 | dw_sample = np.expand_dims(dw_sample,axis=0) 100 | dw_sample = np.swapaxes(dw_sample,0,1) 101 | 102 | x_sample = np.zeros([num_sample, self.dim, self.num_time_interval + 1]) 103 | x_sample[:, :, 0] = np.ones([num_sample, self.dim]) * self.x_init 104 | 105 | if self.useExplict: #use analytic solution of linear SDE 106 | factor = np.exp((self.r-(self.sigma**2)/2)*self.delta_t) 107 | for i in range(self.num_time_interval): 108 | x_sample[:, :, i + 1] = (factor * np.exp(self.sigma * dw_sample[:, :, i])) * x_sample[:, :, i] 109 | else: #use Euler-Maruyama scheme 110 | for i in range(self.num_time_interval): 111 | x_sample[:, :, i + 1] = (1 + self.r * self.delta_t) * x_sample[:, :, i] + (self.sigma * x_sample[:, :, i] * dw_sample[:, :, i]) 112 | 113 | 114 | return dw_sample, x_sample 115 | 116 | 117 | def f_tf(self, t, x, y, z): 118 | return -self.r * y 119 | 120 | 121 | def g_tf(self, t, x): 122 | return tf.maximum( x - self.strike, 0) 123 | 124 | def SolExact(self, t, x): 125 | d1 = (np.log(x / self.strike) + ( self.r + 0.5 * self.sigma ** 2) * (self.total_time - t) ) / (self.sigma * np.sqrt(self.total_time - t)) 126 | d2 = (np.log(x / self.strike) + ( self.r - 0.5 * self.sigma ** 2) * (self.total_time - t) ) / (self.sigma * np.sqrt(self.total_time - t)) 127 | 128 | call = (x * normal.cdf(d1, 0.0, 1.0) - self.strike * np.exp(-self.r * (self.total_time - t) ) * normal.cdf(d2, 0.0, 1.0)) 129 | return call 130 | 131 | class CVA(Equation): 132 | def __init__(self, dim, total_time, num_time_interval): 133 | super(CVA, self).__init__(dim, total_time, num_time_interval) 134 | self._x0 = 100 135 | self._x_init = np.ones(self._dim) * self._x0 # x0 dinamica forward, vettore x0(i) = 100 per i=1...PricingCall.dim 136 | self._sigma = 0.25 # volatility dinamica forward 137 | self._r = 0.0 # interest rate 138 | self._recoveryC = 0.3 139 | self._lambdaC = 0.10 140 | self._recoveryB = 0.4 141 | self._lambdaB = 0.01 142 | 143 | 144 | def sample(self, num_sample): 145 | 146 | dw_sample = normal.rvs(size=[num_sample, 147 | self._dim, 148 | self._num_time_interval]) * self._sqrt_delta_t 149 | 150 | x_sample = np.zeros([num_sample, self._dim, self._num_time_interval + 1]) 151 | x_sample[:, :, 0] = np.ones([num_sample, self._dim]) * self._x_init 152 | 153 | factor = np.exp((self._r-(self._sigma**2)/2)*self._delta_t) 154 | for i in range(self._num_time_interval): 155 | x_sample[:, :, i + 1] = (factor * np.exp(self._sigma * dw_sample[:, :, i])) * x_sample[:, :, i] 156 | return dw_sample, x_sample 157 | 158 | 159 | def f_tf(self, t, x, y, z, vHat): 160 | return (-(1.0-self._recoveryC)*tf.maximum(-vHat,0.0)*self._lambdaC-(self._r + self._lambdaC + self._lambdaB)* y 161 | + (1.0-self._recoveryB)*tf.maximum(vHat,0.0)*self._lambdaB-(self._r + self._lambdaC + self._lambdaB)* y) 162 | 163 | 164 | def g_tf(self, t, x): 165 | return 0.0 166 | 167 | class Portfolio(): 168 | # takes in a list of NonsharedModel (financial derivatives) and can be passed as a clean_value process to an XVA object 169 | def __init__(self,assets,weights=None): 170 | # assets: a list of NonsharedModel 171 | # !!!Warning!!! Assumes independence between random drivers of each asset 172 | self.assets = assets 173 | self.num_assets = len(assets) 174 | if weights is None: 175 | self.weights = [1 for i in range(self.num_assets)] 176 | else: 177 | self.weights = weights 178 | self.dim = sum([asset.bsde.dim for asset in assets ]) 179 | 180 | def sample(self, num_sample): 181 | 182 | w,x,v = self.assets[0].predict(self.assets[0].bsde.sample(num_sample)) 183 | if self.num_assets>1: 184 | v = v*self.weights[0] 185 | for i, weight in enumerate(self.weights[1:]): 186 | w_,x_,v_=self.assets[i+1].sample(self.assets[0].bsde.sample(num_sample)) 187 | w = np.concatenate((w,w_),axis=1) 188 | x = np.concatenate((x,x_),axis=1) 189 | v = v + v_*weight 190 | return w,x,v 191 | 192 | class XVA(Equation): 193 | def __init__(self,eqn_config,clean_value): 194 | self.dim = clean_value.dim 195 | self.total_time = eqn_config.total_time 196 | self.num_time_interval = eqn_config.num_time_interval 197 | self.delta_t = self.total_time / self.num_time_interval 198 | self.sqrt_delta_t = np.sqrt(self.delta_t) 199 | self.y_init = None 200 | self.rate = eqn_config.r # risk-free return 201 | self.intensityB = eqn_config.intensityB # default intensity of the bank 202 | self.intensityC = eqn_config.intensityC # default intensity of the counterparty 203 | self.r_fl = eqn_config.r_fl # unsecured funding lending rate 204 | self.r_fb = eqn_config.r_fb # unsecured funding borrowing rate 205 | self.r_cl = eqn_config.r_cl # interest rate on posted collateral 206 | self.r_cb = eqn_config.r_cb # interest rate on received collateral 207 | self.clean_value = clean_value # Class NonsharedModel, respresenting the clean value process 208 | self.collateral = eqn_config.collateral 209 | self.isXVA = True #indicating that we are dealing with xva 210 | self.dim = clean_value.dim 211 | try: 212 | self.num_assets = clean_value.num_assets # when clean_value is a of class Portfolio 213 | except AttributeError: 214 | self.num_assets = None # when clean_value is of class NonsharedModel, set to None 215 | 216 | #setting (default) value for recovery rate 217 | try: 218 | self.R_B = eqn_config.Recovery_Bank 219 | except AttributeError: 220 | self.R_B = 0.6 221 | try: 222 | self.R_C = eqn_config.Recovery_Counterparty 223 | except AttributeError: 224 | self.R_C = 0.6 225 | 226 | def sample(self, num_sample): 227 | if self.num_assets is None: 228 | return self.clean_value.predict(self.clean_value.bsde.sample(num_sample)) 229 | else: 230 | return self.clean_value.sample(num_sample) 231 | 232 | def f_tf(self, t, x, y, z,v_clean): 233 | # x: underlying, y:xva, z:control 234 | #cva = (1-self.R_C)*tf.maximum(self.collateral-v_clean,0)*self.intensityC 235 | #dva = (1-self.R_B)*tf.maximum(v_clean-self.collateral,0)*self.intensityB 236 | fva = (self.r_fl-self.rate)*tf.maximum(v_clean-y-self.collateral,0) - (self.r_fb-self.rate)*tf.maximum(self.collateral+y-v_clean,0) 237 | #colva = (self.r_cl-self.rate)*max(self.collateral,0) - (self.r_cb-self.rate)*max(-self.collateral,0) 238 | discount = -(self.rate+self.intensityB+self.intensityC)*y 239 | 240 | #return -cva + dva + fva + colva + discount 241 | return fva + discount 242 | 243 | def g_tf(self, t, x,v_clean): 244 | return 0. 245 | 246 | def monte_carlo(self,num_sample=1024): #monte carlo estimation of CVA and DVA 247 | 248 | print('CVA and DVA Monte Carlo estimation started') 249 | discount = np.exp(-(self.rate+self.intensityB+self.intensityC)*np.linspace(0,self.total_time,self.num_time_interval+1)) 250 | 251 | estimate = [] 252 | for i in tqdm(range(num_sample//1024)): #split into batches to estimate 253 | v = self.sample(1024)[1] #shape (num_sample, dim=1, num_time_interval+1) 254 | phi_cva = (1-self.R_C)*discount*np.maximum(self.collateral-v,0)*self.intensityC 255 | phi_dva = (1-self.R_B)*discount*np.maximum(v-self.collateral,0)*self.intensityB 256 | 257 | #trapeziodal rule 258 | cva = np.sum(phi_cva,axis=-1)-(phi_cva[:,:,-1]+phi_cva[:,:,0])/2 259 | dva = np.sum(phi_dva,axis=-1)-(phi_dva[:,:,-1]+phi_dva[:,:,0])/2 260 | 261 | estimate += list(dva[:,0]-cva[:,0]) 262 | 263 | 264 | if num_sample%1024!= 0: #calculate the remaining number of smaples 265 | v = self.sample(num_sample%1024)[1] #shape (num_sample, dim=1, num_time_interval+1) 266 | phi_cva = (1-self.R_C)*discount*np.maximum(self.collateral-v,0)*self.intensityC 267 | phi_dva = (1-self.R_B)*discount*np.maximum(v-self.collateral,0)*self.intensityB 268 | 269 | #trapeziodal rule 270 | cva = np.sum(phi_cva,axis=-1)-(phi_cva[:,:,-1]+phi_cva[:,:,0])/2 271 | dva = np.sum(phi_dva,axis=-1)-(phi_dva[:,:,-1]+phi_dva[:,:,0])/2 272 | 273 | estimate += list(dva[:,0]-cva[:,0]) 274 | estimate = np.array(estimate)*self.delta_t #times time-interval (height of a trapezium) 275 | mean = np.mean(estimate) 276 | std = np.std(estimate)/np.sqrt(num_sample) 277 | 278 | return mean, [mean-3*std,mean+3*std] --------------------------------------------------------------------------------