Repository: LechGrzelak/FinancialEngineering_IR_xVA Branch: main Commit: 6b898e5ae7f8 Files: 36 Total size: 275.6 KB Directory structure: gitextract__8ev3d3k/ ├── LICENSE ├── Lecture 02-Understanding of Filtrations and Measures/ │ └── Materials/ │ ├── Black_Scholes_Jumps.py │ └── Martingale.py ├── Lecture 03-The HJM Framework/ │ └── Materials/ │ ├── CIR_IR_paths.py │ ├── Ho-Lee-ZCBs.py │ ├── Hull-White-Paths.py │ └── Hull-White-ZCBs.py ├── Lecture 04-Yield Curve Dynamics under Short Rate/ │ └── Materials/ │ ├── Hull-White-CompRateSim.py │ ├── Hull-White-ZCBs2.py │ └── Hull_White_1F_2F_Comparison.py ├── Lecture 05-Interest Rate Products/ │ └── Materials/ │ ├── HW_Caplets.py │ ├── HW_OptionsOnZCBs.py │ └── Swaps_HW.py ├── Lecture 06-Construction of Yield Curve and Multi-Curve/ │ └── Materials/ │ ├── MultiCurveBuild.py │ ├── YieldCurveBuildGreeks.py │ └── YieldCurveBuild_Treasury.py ├── Lecture 07-Pricing of Swaptions and Negative Interest Rates/ │ └── Materials/ │ ├── HW_CapletsAndFloorlets.py │ ├── JamshidianTrick.py │ └── ShiftedLognormal.py ├── Lecture 08-Mortgages and Prepayments/ │ └── Materials/ │ ├── AnnuityMortgage.py │ ├── BulletMortgage.py │ ├── Incentives.py │ └── StochasticAmortizingSwap.py ├── Lecture 09-Hybrid Models and Stochastic Interest Rates/ │ └── Materials/ │ ├── BSHW_Comparison.py │ ├── BSHW_ImpliedVolatility.py │ ├── H1_HW_COS_vs_MC.py │ ├── SZHW_ImpliedVolatilities.py │ └── SZHW_MonteCarlo_DiversificationProduct.py ├── Lecture 10-Foreign Exchange (FX) and Inflation/ │ └── Materials/ │ └── H1_HW_COS_vs_MC_FX.py ├── Lecture 11-Market Model and Convexity Adjustments/ │ └── Materials/ │ ├── ConvexityCorrection.py │ └── DD_ImpliedVolatility.py ├── Lecture 12-Valuation Adjustments- xVA (CVA,BCVA and FVA)/ │ └── Materials/ │ └── Exposures_HW_Netting.py ├── Lecture 13-Value-at-Risk and Expected Shortfall/ │ └── Materials/ │ ├── HistoricalVaR_Calculation.py │ ├── MonteCarloVaR.py │ └── MrktData.xlsx └── README.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: LICENSE ================================================ BSD 3-Clause License Copyright (c) 2024, leszek Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: Lecture 02-Understanding of Filtrations and Measures/Materials/Black_Scholes_Jumps.py ================================================ #%% """ Created on July 05 2021 Impact of conditional expectation pricing (Black-Scholes with Jump volatility) This code is purely educational and comes from "Financial Engineering" course by L.A. Grzelak The course is based on the book “Mathematical Modeling and Computation in Finance: With Exercises and Python and MATLAB Computer Codes”, by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing Europe Ltd, 2019. @author: Lech A. Grzelak """ import numpy as np import matplotlib.pyplot as plt import enum import scipy.stats as st # This class defines puts and calls class OptionType(enum.Enum): CALL = 1.0 PUT = -1.0 def GeneratePaths(NoOfPaths,NoOfSteps,S0,T,muJ,sigmaJ,r): # Create empty matrices for Poisson process and for compensated Poisson process X = np.zeros([NoOfPaths, NoOfSteps+1]) S = np.zeros([NoOfPaths, NoOfSteps+1]) time = np.zeros([NoOfSteps+1]) dt = T / float(NoOfSteps) X[:,0] = np.log(S0) S[:,0] = S0 Z = np.random.normal(0.0,1.0,[NoOfPaths,NoOfSteps]) J = np.random.normal(muJ,sigmaJ,[NoOfPaths,NoOfSteps]) for i in range(0,NoOfSteps): # making sure that samples from normal have mean 0 and variance 1 if NoOfPaths > 1: Z[:,i] = (Z[:,i] - np.mean(Z[:,i])) / np.std(Z[:,i]) X[:,i+1] = X[:,i] + (r - 0.5*J[:,i]**2.0)*dt +J[:,i]*np.sqrt(dt)* Z[:,i] time[i+1] = time[i] +dt S = np.exp(X) paths = {"time":time,"X":X,"S":S,"J":J} return paths def EUOptionPriceFromMCPaths(CP,S,K,T,r): # S is a vector of Monte Carlo samples at T if CP == OptionType.CALL: return np.exp(-r*T)*np.mean(np.maximum(S-K,0.0)) elif CP == OptionType.PUT: return np.exp(-r*T)*np.mean(np.maximum(K-S,0.0)) def BS_Call_Put_Option_Price(CP,S_0,K,sigma,t,T,r): K = np.array(K).reshape([len(K),1]) d1 = (np.log(S_0 / K) + (r + 0.5 * np.power(sigma,2.0)) * (T-t)) / (sigma * np.sqrt(T-t)) d2 = d1 - sigma * np.sqrt(T-t) if CP == OptionType.CALL: value = st.norm.cdf(d1) * S_0 - st.norm.cdf(d2) * K * np.exp(-r * (T-t)) elif CP == OptionType.PUT: value = st.norm.cdf(-d2) * K * np.exp(-r * (T-t)) - st.norm.cdf(-d1)*S_0 return value def CallOption_CondExpectation(NoOfPaths,T,S0,K,J,r): # Jumps at time T J_i = J[:,-1] result = np.zeros([NoOfPaths]) for j in range(0,NoOfPaths): sigma = J_i[j] result[j] = BS_Call_Put_Option_Price(OptionType.CALL,S0,[K],sigma,0.0,T,r) return np.mean(result) def mainCalculation(): NoOfPaths = 25 NoOfSteps = 500 T = 5 muJ = 0.3 sigmaJ = 0.005 S0 =100 r =0.00 Paths = GeneratePaths(NoOfPaths,NoOfSteps,S0, T,muJ,sigmaJ,r) timeGrid = Paths["time"] X = Paths["X"] S = Paths["S"] plt.figure(1) plt.plot(timeGrid, np.transpose(X)) plt.grid() plt.xlabel("time") plt.ylabel("X(t)") plt.figure(2) plt.plot(timeGrid, np.transpose(S)) plt.grid() plt.xlabel("time") plt.ylabel("S(t)") # Check the convergence for a given strike K = 80 CP =OptionType.CALL NGrid = range(100,10000,1000) NoOfRuns = len(NGrid) resultMC = np.zeros([NoOfRuns]) resultCondExp = np.zeros([NoOfRuns]) for (i,N) in enumerate(NGrid): print(N) Paths = GeneratePaths(N,NoOfSteps,S0, T,muJ,sigmaJ,r) timeGrid = Paths["time"] S = Paths["S"] resultMC[i] = EUOptionPriceFromMCPaths(CP,S[:,-1],K,T,r) J = Paths["J"] resultCondExp[i]=CallOption_CondExpectation(N,T,S0,K,J,r) plt.figure(3) plt.plot(NGrid,resultMC) plt.plot(NGrid,resultCondExp) plt.legend(['MC','Conditional Expectation']) plt.title('Call Option Price- Convergence') plt.xlabel('Number of Paths') plt.ylabel('Option price for a given strike, K') plt.grid() mainCalculation() ================================================ FILE: Lecture 02-Understanding of Filtrations and Measures/Materials/Martingale.py ================================================ #%% """ Created on July 05 2021 Simulation of, E(W(t)|F(s)) = W(s) using nested Monte Carlo This code is purely educational and comes from "Financial Engineering" course by L.A. Grzelak The course is based on the book “Mathematical Modeling and Computation in Finance: With Exercises and Python and MATLAB Computer Codes”, by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing Europe Ltd, 2019. @author: Lech A. Grzelak """ import numpy as np import matplotlib.pyplot as plt t = 10 s = 5 NoOfPaths=1000 NoOfSteps=10 # First part to caclulate E(W(t)|F(0)) = W(0)=0 def martingaleA(): W_t = np.random.normal(0.0,pow(t,0.5),[NoOfPaths,1]) E_W_t = np.mean(W_t) print("mean value equals to: %.2f while the expected value is W(0) =%0.2f " %(E_W_t,0.0)) # Second part requiring nested Monte Carlo simulation E(W(t)|F(s)) = W(s) def martingaleB(): Z = np.random.normal(0.0,1.0,[NoOfPaths,NoOfSteps]) W = np.zeros([NoOfPaths,NoOfSteps+1]) # time-step from [t0,s] dt1 = s / float(NoOfSteps) for i in range(0,NoOfSteps): # making sure that samples from normal have mean 0 and variance 1 Z[:,i] = (Z[:,i] - np.mean(Z[:,i])) / np.std(Z[:,i]) W[:,i+1] = W[:,i] + pow(dt1,0.5)*Z[:,i] #W_s is the last column of W W_s = W[:,-1] #for every path W(s) we perform sub-simulation until time t and calculate #the expectation # time-step from [s,t] dt2 = (t-s)/float(NoOfSteps); W_t = np.zeros([NoOfPaths,NoOfSteps+1]); #Store the results E_W_t = np.zeros([NoOfPaths]) Error=[] for i in range(0,NoOfPaths): #Sub-simulation from time "s" until "t" W_t[:,0] = W_s[i]; Z = np.random.normal(0.0,1.0,[NoOfPaths,NoOfSteps]) for j in range(0,NoOfSteps): #this is a scaling that ensures that Z has mean 0 and variance 1 Z[:,j] = (Z[:,j]-np.mean(Z[:,j])) / np.std(Z[:,j]); #path simulation, from "s" until "t" W_t[:,j+1] = W_t[:,j] + pow(dt2,0.5)*Z[:,j]; E_W_t[i]=np.mean(W_t[:,-1]) Error.append(E_W_t[i]-W_s[i]) #Generate a plot for the first path if i==0: plt.plot(np.linspace(0,s,NoOfSteps+1),W[0,:]) for j in range(0,NoOfPaths): plt.plot(np.linspace(s,t,NoOfSteps+1),W_t[j,:]) plt.xlabel("time") plt.ylabel("W(t)") plt.grid() print(Error) error = np.max(np.abs(E_W_t-W_s)) print("The error is equal to: %.18f"%(error)) martingaleB() ================================================ FILE: Lecture 03-The HJM Framework/Materials/CIR_IR_paths.py ================================================ #%% """ Created on July 11 2021 Monte Carlo Paths for the CIR Process This code is purely educational and comes from "Financial Engineering" course by L.A. Grzelak The course is based on the book “Mathematical Modeling and Computation in Finance: With Exercises and Python and MATLAB Computer Codes”, by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing Europe Ltd, 2019. @author: Lech A. Grzelak """ import numpy as np import matplotlib.pyplot as plt def GeneratePathsCIREuler(NoOfPaths,NoOfSteps,T,lambd,r0,theta,gamma): Z = np.random.normal(0.0,1.0,[NoOfPaths,NoOfSteps]) W = np.zeros([NoOfPaths, NoOfSteps+1]) R = np.zeros([NoOfPaths, NoOfSteps+1]) R[:,0]=r0 time = np.zeros([NoOfSteps+1]) dt = T / float(NoOfSteps) for i in range(0,NoOfSteps): # making sure that samples from normal have mean 0 and variance 1 if NoOfPaths > 1: Z[:,i] = (Z[:,i] - np.mean(Z[:,i])) / np.std(Z[:,i]) W[:,i+1] = W[:,i] + np.power(dt, 0.5)*Z[:,i] R[:,i+1] = R[:,i] + lambd*(theta - R[:,i]) * dt + gamma* np.sqrt(R[:,i]) * (W[:,i+1]-W[:,i]) # Truncated boundary condition R[:,i+1] = np.maximum(R[:,i+1],0.0) time[i+1] = time[i] +dt # Outputs paths = {"time":time,"R":R} return paths def mainCalculation(): NoOfPaths = 1 NoOfSteps = 500 T = 50.0 lambd = 0.1 gamma = 0.05 r0 = 0.05 theta = 0.05 # Effect of mean reversion lambda plt.figure(1) legend = [] lambdVec = [0.01, 0.2, 5.0] for lambdTemp in lambdVec: np.random.seed(2) Paths = GeneratePathsCIREuler(NoOfPaths,NoOfSteps,T,lambdTemp,r0,theta,gamma) legend.append('lambda={0}'.format(lambdTemp)) timeGrid = Paths["time"] R = Paths["R"] plt.plot(timeGrid, np.transpose(R)) plt.grid() plt.xlabel("time") plt.ylabel("R(t)") plt.legend(legend) # Effect of the volatility plt.figure(2) legend = [] gammaVec = [0.1, 0.2, 0.3] for gammaTemp in gammaVec: np.random.seed(2) Paths = GeneratePathsCIREuler(NoOfPaths,NoOfSteps,T,lambd,r0,theta,gammaTemp) legend.append('gamma={0}'.format(gammaTemp)) timeGrid = Paths["time"] R = Paths["R"] plt.plot(timeGrid, np.transpose(R)) plt.grid() plt.xlabel("time") plt.ylabel("R(t)") plt.legend(legend) mainCalculation() ================================================ FILE: Lecture 03-The HJM Framework/Materials/Ho-Lee-ZCBs.py ================================================ #%% """ Created on July 12 2021 Ho-Lee Model, Simulation of the Model + Computation of ZCBs, P(0,t) This code is purely educational and comes from "Financial Engineering" course by L.A. Grzelak The course is based on the book “Mathematical Modeling and Computation in Finance: With Exercises and Python and MATLAB Computer Codes”, by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing Europe Ltd, 2019. @author: Lech A. Grzelak """ import numpy as np import matplotlib.pyplot as plt def f0T(t,P0T): # time-step needed for differentiation dt = 0.01 expr = - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) return expr def GeneratePathsHoLeeEuler(NoOfPaths,NoOfSteps,T,P0T,sigma): # Initial interest rate is a forward rate at time t->0 r0 = f0T(0.01,P0T) theta = lambda t: (f0T(t+dt,P0T)-f0T(t-dt,P0T))/(2.0*dt) + sigma**2.0*t Z = np.random.normal(0.0,1.0,[NoOfPaths,NoOfSteps]) W = np.zeros([NoOfPaths, NoOfSteps+1]) R = np.zeros([NoOfPaths, NoOfSteps+1]) M = np.zeros([NoOfPaths, NoOfSteps+1]) M[:,0]= 1.0 R[:,0]=r0 time = np.zeros([NoOfSteps+1]) dt = T / float(NoOfSteps) for i in range(0,NoOfSteps): # making sure that samples from normal have mean 0 and variance 1 if NoOfPaths > 1: Z[:,i] = (Z[:,i] - np.mean(Z[:,i])) / np.std(Z[:,i]) W[:,i+1] = W[:,i] + np.power(dt, 0.5)*Z[:,i] R[:,i+1] = R[:,i] + theta(time[i]) * dt + sigma* (W[:,i+1]-W[:,i]) M[:,i+1] = M[:,i] * np.exp((R[:,i+1]+R[:,i])*0.5*dt) time[i+1] = time[i] +dt # Outputs paths = {"time":time,"R":R,"M":M} return paths def mainCalculation(): NoOfPaths = 25000 NoOfSteps = 500 sigma = 0.007 # We define a ZCB curve (obtained from the market) P0T = lambda T: np.exp(-0.1*T) # In this experiment we compare ZCB from the Market and Monte Carlo "Monte Carlo part" T = 40 paths= GeneratePathsHoLeeEuler(NoOfPaths,NoOfSteps,T,P0T,sigma) M = paths["M"] ti = paths["time"] # Here we compare the price of an option on a ZCB from Monte Carlo and Analytical expression P_t = np.zeros([NoOfSteps+1]) for i in range(0,NoOfSteps+1): P_t[i] = np.mean(1.0/M[:,i]) plt.figure(1) plt.grid() plt.xlabel('T') plt.ylabel('P(0,T)') plt.plot(ti,P0T(ti)) plt.plot(ti,P_t,'--r') plt.legend(['P(0,t) market','P(0,t) Monte Carlo']) plt.title('ZCBs from Ho-Lee Model') mainCalculation() ================================================ FILE: Lecture 03-The HJM Framework/Materials/Hull-White-Paths.py ================================================ #%% """ Created on July 12 2021 Monte Carlo paths for the Hull-White model This code is purely educational and comes from "Financial Engineering" course by L.A. Grzelak The course is based on the book “Mathematical Modeling and Computation in Finance: With Exercises and Python and MATLAB Computer Codes”, by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing Europe Ltd, 2019. @author: Lech A. Grzelak """ import numpy as np import matplotlib.pyplot as plt def GeneratePathsHWEuler(NoOfPaths,NoOfSteps,T,P0T, lambd, eta): # time-step needed for differentiation dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) # Initial interest rate is a forward rate at time t->0 r0 = f0T(0.00001) theta = lambda t: 1.0/lambd * (f0T(t+dt)-f0T(t-dt))/(2.0*dt) + f0T(t) + \ eta*eta/(2.0*lambd*lambd)*(1.0-np.exp(-2.0*lambd*t)) Z = np.random.normal(0.0,1.0,[NoOfPaths,NoOfSteps]) W = np.zeros([NoOfPaths, NoOfSteps+1]) R = np.zeros([NoOfPaths, NoOfSteps+1]) R[:,0]=r0 time = np.zeros([NoOfSteps+1]) dt = T / float(NoOfSteps) for i in range(0,NoOfSteps): # making sure that samples from normal have mean 0 and variance 1 if NoOfPaths > 1: Z[:,i] = (Z[:,i] - np.mean(Z[:,i])) / np.std(Z[:,i]) W[:,i+1] = W[:,i] + np.power(dt, 0.5)*Z[:,i] R[:,i+1] = R[:,i] + lambd*(theta(time[i]) - R[:,i]) * dt + eta* (W[:,i+1]-W[:,i]) time[i+1] = time[i] +dt # Outputs paths = {"time":time,"R":R} return paths def mainCalculation(): NoOfPaths = 1 NoOfSteps = 5000 T = 50.0 lambd = 0.5 eta = 0.01 # We define a ZCB curve (obtained from the market) P0T = lambda T: np.exp(-0.05*T) # Effect of mean reversion lambda plt.figure(1) legend = [] lambdVec = [-0.01, 0.2, 5.0] for lambdTemp in lambdVec: np.random.seed(2) Paths = GeneratePathsHWEuler(NoOfPaths,NoOfSteps,T,P0T, lambdTemp, eta) legend.append('lambda={0}'.format(lambdTemp)) timeGrid = Paths["time"] R = Paths["R"] plt.plot(timeGrid, np.transpose(R)) plt.grid() plt.xlabel("time") plt.ylabel("R(t)") plt.legend(legend) # Effect of the volatility plt.figure(2) legend = [] etaVec = [0.1, 0.2, 0.3] for etaTemp in etaVec: np.random.seed(2) Paths = GeneratePathsHWEuler(NoOfPaths,NoOfSteps,T,P0T, lambd, etaTemp) legend.append('eta={0}'.format(etaTemp)) timeGrid = Paths["time"] R = Paths["R"] plt.plot(timeGrid, np.transpose(R)) plt.grid() plt.xlabel("time") plt.ylabel("R(t)") plt.legend(legend) mainCalculation() ================================================ FILE: Lecture 03-The HJM Framework/Materials/Hull-White-ZCBs.py ================================================ #%% """ Created on July 12 2021 Hull-White Model, Simulation of the Model This code is purely educational and comes from "Financial Engineering" course by L.A. Grzelak The course is based on the book “Mathematical Modeling and Computation in Finance: With Exercises and Python and MATLAB Computer Codes”, by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing Europe Ltd, 2019. @author: Lech A. Grzelak """ import numpy as np import matplotlib.pyplot as plt def f0T(t,P0T): # time-step needed for differentiation dt = 0.01 expr = - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) return expr def GeneratePathsHWEuler(NoOfPaths,NoOfSteps,T,P0T, lambd, eta): # Initial interest rate is a forward rate at time t->0 r0 = f0T(0.01,P0T) theta = lambda t: 1.0/lambd * (f0T(t+dt,P0T)-f0T(t-dt,P0T))/(2.0*dt) + f0T(t,P0T) + eta*eta/(2.0*lambd*lambd)*(1.0-np.exp(-2.0*lambd*t)) #theta = lambda t: 0.1 +t -t #print("changed theta") Z = np.random.normal(0.0,1.0,[NoOfPaths,NoOfSteps]) W = np.zeros([NoOfPaths, NoOfSteps+1]) R = np.zeros([NoOfPaths, NoOfSteps+1]) M = np.zeros([NoOfPaths, NoOfSteps+1]) M[:,0]= 1.0 R[:,0]= r0 time = np.zeros([NoOfSteps+1]) dt = T / float(NoOfSteps) for i in range(0,NoOfSteps): # making sure that samples from normal have mean 0 and variance 1 if NoOfPaths > 1: Z[:,i] = (Z[:,i] - np.mean(Z[:,i])) / np.std(Z[:,i]) W[:,i+1] = W[:,i] + np.power(dt, 0.5)*Z[:,i] R[:,i+1] = R[:,i] + lambd*(theta(time[i]) - R[:,i]) * dt + eta* (W[:,i+1]-W[:,i]) M[:,i+1] = M[:,i] * np.exp((R[:,i+1]+R[:,i])*0.5*dt) time[i+1] = time[i] +dt # Outputs paths = {"time":time,"R":R,"M":M} return paths def HW_theta(lambd,eta,P0T): dt = 0.01 theta = lambda t: 1.0/lambd * (f0T(t+dt,P0T)-f0T(t-dt,P0T))/(2.0*dt) + f0T(t,P0T) + eta*eta/(2.0*lambd*lambd)*(1.0-np.exp(-2.0*lambd*t)) #print("CHANGED THETA") return theta#lambda t: 0.1+t-t def mainCalculation(): NoOfPaths = 25000 NoOfSteps = 25 lambd = 0.02 eta = 0.02 # We define a ZCB curve (obtained from the market) P0T = lambda T: np.exp(-0.1*T) # In this experiment we compare ZCB from the Market and Monte Carlo "Monte Carlo part" T = 40 paths= GeneratePathsHWEuler(NoOfPaths,NoOfSteps,T,P0T, lambd, eta) M = paths["M"] ti = paths["time"] #dt = timeGrid[1]-timeGrid[0] # Here we compare the price of an option on a ZCB from Monte Carlo the Market P_tMC = np.zeros([NoOfSteps+1]) for i in range(0,NoOfSteps+1): P_tMC[i] = np.mean(1.0/M[:,i]) plt.figure(1) plt.grid() plt.xlabel('T') plt.ylabel('P(0,T)') plt.plot(ti,P0T(ti)) plt.plot(ti,P_tMC,'--r') plt.legend(['P(0,t) market','P(0,t) Monte Carlo']) plt.title('ZCBs from Hull-White Model') mainCalculation() ================================================ FILE: Lecture 04-Yield Curve Dynamics under Short Rate/Materials/Hull-White-CompRateSim.py ================================================ #%% """ Created on July 12 2021 Yield Curve shapes and the Hull-White Model 1 Factor This code is purely educational and comes from "Financial Engineering" course by L.A. Grzelak The course is based on the book “Mathematical Modeling and Computation in Finance: With Exercises and Python and MATLAB Computer Codes”, by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing Europe Ltd, 2019. @author: Lech A. Grzelak """ import numpy as np import matplotlib.pyplot as plt import scipy.integrate as integrate #from scipy.interpolate import interp1d from scipy import interpolate def f0T(t,P0T): # time-step needed for differentiation dt = 0.01 expr = - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) return expr def GeneratePathsHWEuler(NoOfPaths,NoOfSteps,T,P0T, lambd, eta): # Initial interest rate is a forward rate at time t->0 r0 = f0T(0.01,P0T) theta = lambda t: 1.0/lambd * (f0T(t+dt,P0T)-f0T(t-dt,P0T))/(2.0*dt) + f0T(t,P0T) + eta*eta/(2.0*lambd*lambd)*(1.0-np.exp(-2.0*lambd*t)) #theta = lambda t: 0.1 +t -t #print("changed theta") Z = np.random.normal(0.0,1.0,[NoOfPaths,NoOfSteps]) W = np.zeros([NoOfPaths, NoOfSteps+1]) R = np.zeros([NoOfPaths, NoOfSteps+1]) R[:,0]=r0 time = np.zeros([NoOfSteps+1]) dt = T / float(NoOfSteps) for i in range(0,NoOfSteps): # making sure that samples from normal have mean 0 and variance 1 if NoOfPaths > 1: Z[:,i] = (Z[:,i] - np.mean(Z[:,i])) / np.std(Z[:,i]) W[:,i+1] = W[:,i] + np.power(dt, 0.5)*Z[:,i] R[:,i+1] = R[:,i] + lambd*(theta(time[i]) - R[:,i]) * dt + eta* (W[:,i+1]-W[:,i]) time[i+1] = time[i] +dt # Outputs paths = {"time":time,"R":R} return paths def HW_theta(lambd,eta,P0T): dt = 0.01 theta = lambda t: 1.0/lambd * (f0T(t+dt,P0T)-f0T(t-dt,P0T))/(2.0*dt) + f0T(t,P0T) + eta*eta/(2.0*lambd*lambd)*(1.0-np.exp(-2.0*lambd*t)) #print("CHANGED THETA") return theta#lambda t: 0.1+t-t def HW_A(lambd,eta,P0T,T1,T2): tau = T2-T1 zGrid = np.linspace(0.0,tau,250) B_r = lambda tau: 1.0/lambd * (np.exp(-lambd *tau)-1.0) theta = HW_theta(lambd,eta,P0T) temp1 = lambd * integrate.trapz(theta(T2-zGrid)*B_r(zGrid),zGrid) temp2 = eta*eta/(4.0*np.power(lambd,3.0)) * (np.exp(-2.0*lambd*tau)*(4*np.exp(lambd*tau)-1.0) -3.0) + eta*eta*tau/(2.0*lambd*lambd) return temp1 + temp2 def HW_B(lambd,eta,T1,T2): return 1.0/lambd *(np.exp(-lambd*(T2-T1))-1.0) def HW2F_ZCB(lambd1,lambd2,eta1,eta2,rho,P0T,T1,T2,xT1,yT1): V = lambda t,T: (eta1**2.0)/(lambd1**2.0)*((T2-T1)+2.0/lambd1*np.exp(-lambd1*(T2-T1))-1.0/(2.0*lambd1)*np.exp(-2.0*lambd1*(T2-T1))-3.0/(2.0*lambd1))+\ +(eta2**2.0)/(lambd2**2.0)*((T2-T1)+2.0/lambd2*np.exp(-lambd2*(T2-T1))-1.0/(2.0*lambd2)*np.exp(-2.0*lambd2*(T2-T1))-3.0/(2.0*lambd2))+\ + 2.0*rho*eta1*eta2/(lambd1*lambd2)*(T2-T1 + 1.0/lambd1*(np.exp(-lambd1*(T2-T1))-1.0)+ 1.0/lambd2*(np.exp(-lambd2*(T2-T1))-1.0) - 1.0/(lambd1+lambd2)*(np.exp(-(lambd1+lambd2)*(T2-T1))-1.0)) intPhi = - np.log(P0T(T2)/P0T(T1)*np.exp(-0.5*(V(0,T2)-V(0,T1)))) A = 1.0/lambd1 * (1.0-np.exp(-lambd1 * (T2-T1))) B = 1.0/lambd2 * (1.0-np.exp(-lambd2 * (T2-T1))) return np.exp(- intPhi -A * xT1 - B * yT1 + 0.5 * V(T1,T2)) def HW_ZCB(lambd,eta,P0T,T1,T2,rT1): B_r = HW_B(lambd,eta,T1,T2) A_r = HW_A(lambd,eta,P0T,T1,T2) return np.exp(A_r + B_r *rT1) def HW_r_0(P0T,lambd,eta): r0 = f0T(0.001,P0T) return r0 def mainCalculation(): NoOfPaths = 20000 NoOfSteps = 100 #HW1F lambd = 0.01 eta = 0.002 # We define a ZCB curve (obtained from the market) ti = [0.0,0.00273972600000000,0.0876712330000000,0.172602740000000,0.257534247000000,0.342465753000000,0.427397260000000,0.512328767000000,0.597260274000000,0.682191781000000,0.767123288000000,0.852054795000000,0.936986301000000,1.02191780800000,1.10684931500000,1.19178082200000,1.27671232900000,1.36164383600000,1.44657534200000,1.53150684900000,1.61643835600000,1.70136986300000,1.78630137000000,1.87123287700000,1.95616438400000,2.04109589000000,2.12602739700000,2.21095890400000,2.29589041100000,2.38082191800000,2.46575342500000,2.55068493200000,2.63561643800000,2.72054794500000,2.80547945200000,2.89041095900000,2.97534246600000,3.06027397300000,3.14520547900000,3.23013698600000,3.31506849300000,3.40000000000000,3.48493150700000,3.56986301400000,3.65479452100000,3.73972602700000,3.82465753400000,3.90958904100000,3.99452054800000,4.07945205500000,4.16438356200000,4.24931506800000,4.33424657500000,4.41917808200000,4.50410958900000,4.58904109600000,4.67397260300000,4.75890411000000,4.84383561600000,4.92876712300000,5.01369863000000,5.09863013700000,5.18356164400000,5.26849315100000,5.35342465800000,5.43835616400000,5.52328767100000,5.60821917800000,5.69315068500000,5.77808219200000,5.86301369900000,5.94794520500000,6.03287671200000,6.11780821900000,6.20273972600000,6.28767123300000,6.37260274000000,6.45753424700000,6.54246575300000,6.62739726000000,6.71232876700000,6.79726027400000,6.88219178100000,6.96712328800000,7.05205479500000,7.13698630100000,7.22191780800000,7.30684931500000,7.39178082200000,7.47671232900000,7.56164383600000,7.64657534200000,7.73150684900000,7.81643835600000,7.90136986300000,7.98630137000000,8.07123287700000,8.15616438400000,8.24109589000000,8.32602739700000,8.41095890400000,8.49589041100000,8.58082191800000,8.66575342500000,8.75068493200000,8.83561643800000,8.92054794500000,9.00547945200000,9.09041095900000,9.17534246600000,9.26027397300000,9.34520547900000,9.43013698600000,9.51506849300000,9.60000000000000,9.68493150700000,9.76986301400000,9.85479452100000,9.93972602700000,10.0246575300000,10.1095890400000,10.1945205500000,10.2794520500000,10.3643835600000,10.4493150700000,10.5342465800000,10.6191780800000,10.7041095900000,10.7890411000000,10.8739726000000,10.9589041100000,11.0438356200000,11.1287671200000,11.2136986300000,11.2986301400000,11.3835616400000,11.4684931500000,11.5534246600000,11.6383561600000,11.7232876700000,11.8082191800000,11.8931506800000,11.9780821900000,12.0630137000000,12.1479452100000,12.2328767100000,12.3178082200000,12.4027397300000,12.4876712300000,12.5726027400000,12.6575342500000,12.7424657500000,12.8273972600000,12.9123287700000,12.9972602700000,13.0821917800000,13.1671232900000,13.2520547900000,13.3369863000000,13.4219178100000,13.5068493200000,13.5917808200000,13.6767123300000,13.7616438400000,13.8465753400000,13.9315068500000,14.0164383600000,14.1013698600000,14.1863013700000,14.2712328800000,14.3561643800000,14.4410958900000,14.5260274000000,14.6109589000000,14.6958904100000,14.7808219200000,14.8657534200000,14.9506849300000,15.0356164400000,15.1205479500000,15.2054794500000,15.2904109600000,15.3753424700000,15.4602739700000,15.5452054800000,15.6301369900000,15.7150684900000,15.8000000000000,15.8849315100000,15.9698630100000,16.0547945200000,16.1397260300000,16.2246575300000,16.3095890400000,16.3945205500000,16.4794520500000,16.5643835600000,16.6493150700000,16.7342465800000,16.8191780800000,16.9041095900000,16.9890411000000,17.0739726000000,17.1589041100000,17.2438356200000,17.3287671200000,17.4136986300000,17.4986301400000,17.5835616400000,17.6684931500000,17.7534246600000,17.8383561600000,17.9232876700000,18.0082191800000,18.0931506800000,18.1780821900000,18.2630137000000,18.3479452100000,18.4328767100000,18.5178082200000,18.6027397300000,18.6876712300000,18.7726027400000,18.8575342500000,18.9424657500000,19.0273972600000,19.1123287700000,19.1972602700000,19.2821917800000,19.3671232900000,19.4520547900000,19.5369863000000,19.6219178100000,19.7068493200000,19.7917808200000,19.8767123300000,19.9616438400000,20.0465753400000,20.1315068500000,20.2164383600000,20.3013698600000,20.3863013700000,20.4712328800000,20.5561643800000,20.6410958900000,20.7260274000000,20.8109589000000,20.8958904100000,20.9808219200000,21.0657534200000,21.1506849300000,21.2356164400000,21.3205479500000,21.4054794500000,21.4904109600000,21.5753424700000,21.6602739700000,21.7452054800000,21.8301369900000,21.9150684900000,22,22.0849315100000,22.1698630100000,22.2547945200000,22.3397260300000,22.4246575300000,22.5095890400000,22.5945205500000,22.6794520500000,22.7643835600000,22.8493150700000,22.9342465800000,23.0191780800000,23.1041095900000,23.1890411000000,23.2739726000000,23.3589041100000,23.4438356200000,23.5287671200000,23.6136986300000,23.6986301400000,23.7835616400000,23.8684931500000,23.9534246600000,24.0383561600000,24.1232876700000,24.2082191800000,24.2931506800000,24.3780821900000,24.4630137000000,24.5479452100000,24.6328767100000,24.7178082200000,24.8027397300000,24.8876712300000,24.9726027400000,25.0575342500000,25.1424657500000,25.2273972600000,25.3123287700000,25.3972602700000,25.4821917800000,25.5671232900000,25.6520547900000,25.7369863000000,25.8219178100000,25.9068493200000,25.9917808200000,26.0767123300000,26.1616438400000,26.2465753400000,26.3315068500000,26.4164383600000,26.5013698600000,26.5863013700000,26.6712328800000,26.7561643800000,26.8410958900000,26.9260274000000,27.0109589000000,27.0958904100000,27.1808219200000,27.2657534200000,27.3506849300000,27.4356164400000,27.5205479500000,27.6054794500000,27.6904109600000,27.7753424700000,27.8602739700000,27.9452054800000,28.0301369900000,28.1150684900000,28.2000000000000,28.2849315100000,28.3698630100000,28.4547945200000,28.5397260300000,28.6246575300000,28.7095890400000,28.7945205500000,28.8794520500000,28.9643835600000,29.0493150700000,29.1342465800000,29.2191780800000,29.3041095900000,29.3890411000000,29.4739726000000,29.5589041100000,29.6438356200000,29.7287671200000,29.8136986300000,29.8986301400000,29.9835616400000,30.0684931500000,30.1534246600000,30.2383561600000,30.3232876700000,30.4082191800000,30.4931506800000,30.5780821900000,30.6630137000000,30.7479452100000,30.8328767100000,30.9178082200000,31.0027397300000,31.0876712300000,31.1726027400000,31.2575342500000,31.3424657500000,31.4273972600000,31.5123287700000,31.5972602700000,31.6821917800000,31.7671232900000,31.8520547900000,31.9369863000000,32.0219178100000,32.1068493200000,32.1917808200000,32.2767123300000,32.3616438400000,32.4465753400000,32.5315068500000,32.6164383600000,32.7013698600000,32.7863013700000,32.8712328800000,32.9561643800000,33.0410958900000,33.1260274000000,33.2109589000000,33.2958904100000,33.3808219200000,33.4657534200000,33.5506849300000,33.6356164400000,33.7205479500000,33.8054794500000,33.8904109600000,33.9753424700000,34.0602739700000,34.1452054800000,34.2301369900000,34.3150684900000,34.4000000000000,34.4849315100000,34.5698630100000,34.6547945200000,34.7397260300000,34.8246575300000,34.9095890400000,34.9945205500000,35.0794520500000,35.1643835600000,35.2493150700000,35.3342465800000,35.4191780800000,35.5041095900000,35.5890411000000,35.6739726000000,35.7589041100000,35.8438356200000,35.9287671200000,36.0136986300000,36.0986301400000,36.1835616400000,36.2684931500000,36.3534246600000,36.4383561600000,36.5232876700000,36.6082191800000,36.6931506800000,36.7780821900000,36.8630137000000,36.9479452100000,37.0328767100000,37.1178082200000,37.2027397300000,37.2876712300000,37.3726027400000,37.4575342500000,37.5424657500000,37.6273972600000,37.7123287700000,37.7972602700000,37.8821917800000,37.9671232900000,38.0520547900000,38.1369863000000,38.2219178100000,38.3068493200000,38.3917808200000,38.4767123300000,38.5616438400000,38.6465753400000,38.7315068500000,38.8164383600000,38.9013698600000,38.9863013700000,39.0712328800000,39.1561643800000,39.2410958900000,39.3260274000000,39.4109589000000,39.4958904100000,39.5808219200000,39.6657534200000,39.7506849300000,39.8356164400000,39.9205479500000,40.0054794500000] Pi = [1.0,0.999966573000000,0.998930882000000,0.997824062000000,0.996511145000000,0.995199956000000,0.993821602000000,0.992277014000000,0.990734827000000,0.989164324000000,0.987428762000000,0.985704346000000,0.983946708000000,0.982068207000000,0.980193293000000,0.978281187000000,0.976255832000000,0.974234670000000,0.972174514000000,0.970028236000000,0.967886697000000,0.965693800000000,0.963440984000000,0.961193424000000,0.958903753000000,0.956575247000000,0.954252397000000,0.951842433000000,0.949228406000000,0.946482248000000,0.943632525000000,0.940707509000000,0.937735160000000,0.934743101000000,0.931758611000000,0.928808623000000,0.925919731000000,0.923110403000000,0.920338655000000,0.917589739000000,0.914858924000000,0.912141532000000,0.909432941000000,0.906728583000000,0.904023944000000,0.901314567000000,0.898596044000000,0.895864025000000,0.893114214000000,0.890346348000000,0.887569841000000,0.884786140000000,0.881996090000000,0.879200528000000,0.876400278000000,0.873596157000000,0.870788975000000,0.867979528000000,0.865168605000000,0.862356987000000,0.859545442000000,0.856732322000000,0.853915005000000,0.851094371000000,0.848271318000000,0.845446732000000,0.842621487000000,0.839796449000000,0.836972470000000,0.834150393000000,0.831331050000000,0.828515261000000,0.825703790000000,0.822893949000000,0.820084325000000,0.817275636000000,0.814468590000000,0.811663887000000,0.808862219000000,0.806064266000000,0.803270701000000,0.800482187000000,0.797699379000000,0.794922921000000,0.792153338000000,0.789389717000000,0.786631547000000,0.783878633000000,0.781130782000000,0.778387801000000,0.775649505000000,0.772915708000000,0.770186227000000,0.767460882000000,0.764739497000000,0.762021898000000,0.759308200000000,0.756599781000000,0.753896558000000,0.751198194000000,0.748504356000000,0.745814715000000,0.743128950000000,0.740446741000000,0.737767776000000,0.735091745000000,0.732418345000000,0.729747277000000,0.727078670000000,0.724413443000000,0.721751645000000,0.719093279000000,0.716438346000000,0.713786850000000,0.711138792000000,0.708494177000000,0.705853006000000,0.703215284000000,0.700581015000000,0.697950202000000,0.695320544000000,0.692690217000000,0.690059929000000,0.687430378000000,0.684802253000000,0.682176235000000,0.679552992000000,0.676933185000000,0.674317462000000,0.671706465000000,0.669100824000000,0.666501159000000,0.663908082000000,0.661322194000000,0.658744089000000,0.656174348000000,0.653613545000000,0.651062245000000,0.648521002000000,0.645990363000000,0.643470865000000,0.640963036000000,0.638467395000000,0.635983411000000,0.633503959000000,0.631028021000000,0.628556105000000,0.626088714000000,0.623626343000000,0.621169478000000,0.618718599000000,0.616274178000000,0.613836679000000,0.611406558000000,0.608984265000000,0.606570242000000,0.604164924000000,0.601768738000000,0.599382105000000,0.597005438000000,0.594639144000000,0.592283621000000,0.589939262000000,0.587606453000000,0.585285574000000,0.582976996000000,0.580681085000000,0.578398202000000,0.576128698000000,0.573872922000000,0.571631214000000,0.569403908000000,0.567191334000000,0.564993814000000,0.562811666000000,0.560645202000000,0.558494727000000,0.556360543000000,0.554242913000000,0.552138115000000,0.550043470000000,0.547959012000000,0.545884776000000,0.543820795000000,0.541767101000000,0.539723726000000,0.537690699000000,0.535668051000000,0.533655809000000,0.531654001000000,0.529662652000000,0.527681790000000,0.525711438000000,0.523751620000000,0.521802360000000,0.519863678000000,0.517935598000000,0.516018137000000,0.514111318000000,0.512215157000000,0.510329674000000,0.508454885000000,0.506590807000000,0.504737456000000,0.502894847000000,0.501062994000000,0.499241910000000,0.497431609000000,0.495632103000000,0.493843403000000,0.492065521000000,0.490298466000000,0.488542249000000,0.486796879000000,0.485062364000000,0.483338711000000,0.481625929000000,0.479924024000000,0.478233003000000,0.476552870000000,0.474883632000000,0.473225293000000,0.471577858000000,0.469941330000000,0.468315712000000,0.466701007000000,0.465097218000000,0.463504347000000,0.461922395000000,0.460351364000000,0.458791253000000,0.457242065000000,0.455703798000000,0.454176452000000,0.452660027000000,0.451154522000000,0.449659935000000,0.448176257000000,0.446703083000000,0.445240105000000,0.443787209000000,0.442344286000000,0.440911226000000,0.439487920000000,0.438074261000000,0.436670143000000,0.435275460000000,0.433890109000000,0.432513986000000,0.431146989000000,0.429789017000000,0.428439971000000,0.427099751000000,0.425768260000000,0.424445399000000,0.423131074000000,0.421825189000000,0.420527650000000,0.419238363000000,0.417957237000000,0.416684180000000,0.415419101000000,0.414161910000000,0.412912520000000,0.411670842000000,0.410436789000000,0.409210275000000,0.407991213000000,0.406779521000000,0.405575114000000,0.404377909000000,0.403187824000000,0.402004777000000,0.400828688000000,0.399659478000000,0.398497066000000,0.397341375000000,0.396192326000000,0.395049844000000,0.393913852000000,0.392784274000000,0.391661036000000,0.390544064000000,0.389433283000000,0.388328623000000,0.387230010000000,0.386137373000000,0.385050641000000,0.383969745000000,0.382894615000000,0.381825182000000,0.380761378000000,0.379703135000000,0.378650387000000,0.377603067000000,0.376561110000000,0.375524516000000,0.374495509000000,0.373475055000000,0.372462955000000,0.371459016000000,0.370463043000000,0.369474847000000,0.368494238000000,0.367521029000000,0.366555034000000,0.365596072000000,0.364643959000000,0.363698516000000,0.362759566000000,0.361826932000000,0.360900441000000,0.359979918000000,0.359065194000000,0.358156099000000,0.357252465000000,0.356354126000000,0.355460918000000,0.354572678000000,0.353689243000000,0.352810455000000,0.351936156000000,0.351066187000000,0.350200393000000,0.349338622000000,0.348480719000000,0.347626535000000,0.346775919000000,0.345928722000000,0.345084799000000,0.344244003000000,0.343406190000000,0.342571217000000,0.341738943000000,0.340909228000000,0.340081932000000,0.339256917000000,0.338434048000000,0.337613190000000,0.336794207000000,0.335976969000000,0.335161343000000,0.334347200000000,0.333534411000000,0.332722847000000,0.331912384000000,0.331102895000000,0.330294257000000,0.329486347000000,0.328679044000000,0.327872227000000,0.327065778000000,0.326259578000000,0.325453511000000,0.324647462000000,0.323841447000000,0.323037558000000,0.322236538000000,0.321438361000000,0.320642997000000,0.319850421000000,0.319060604000000,0.318273519000000,0.317489140000000,0.316707441000000,0.315928394000000,0.315151974000000,0.314378155000000,0.313606911000000,0.312838216000000,0.312072045000000,0.311308373000000,0.310547175000000,0.309788426000000,0.309032102000000,0.308278178000000,0.307526629000000,0.306777432000000,0.306030564000000,0.305285999000000,0.304543716000000,0.303803690000000,0.303065898000000,0.302330318000000,0.301596926000000,0.300865701000000,0.300136619000000,0.299409659000000,0.298684798000000,0.297962014000000,0.297241286000000,0.296522593000000,0.295805912000000,0.295091222000000,0.294378503000000,0.293667734000000,0.292958894000000,0.292251962000000,0.291546917000000,0.290843741000000,0.290142412000000,0.289442910000000,0.288745217000000,0.288049311000000,0.287355175000000,0.286662788000000,0.285972131000000,0.285283185000000,0.284595932000000,0.283910353000000,0.283226429000000,0.282544142000000,0.281863473000000,0.281184405000000,0.280506920000000,0.279830999000000,0.279156625000000,0.278483781000000,0.277812449000000,0.277142612000000,0.276474253000000,0.275807354000000,0.275141900000000,0.274477873000000,0.273815257000000,0.273154036000000,0.272494193000000,0.271835712000000,0.271178577000000,0.270522772000000,0.269868283000000,0.269215092000000,0.268563184000000,0.267912545000000,0.267263159000000,0.266615011000000,0.265968086000000,0.265322370000000,0.264677847000000,0.264034503000000,0.263392324000000,0.262751295000000,0.262111403000000,0.261472633000000,0.260834972000000,0.260198405000000,0.259562919000000,0.258928501000000,0.258295136000000,0.257662813000000,0.257031517000000,0.256401236000000,0.255771957000000,0.255143667000000,0.254516353000000,0.253890002000000,0.253264603000000,0.252640144000000,0.252016611000000,0.251393992000000,0.250772277000000,0.250151453000000,0.249531508000000,0.248912431000000,0.248294210000000,0.247676834000000,0.247060291000000,0.246444571000000,0.245829663000000,0.245215555000000,0.244602237000000,0.243989698000000,0.243377928000000] interpolator = interpolate.splrep(ti, np.log(Pi), s=0.00001) P0T = lambda T: np.exp(interpolate.splev(T, interpolator, der=0)) r0 = HW_r_0(P0T,lambd,eta) # In this experiment we compare ZCB from the Market and Analytical expression N = 20 T_end0 = 49.0 Tgrid= np.linspace(0.1,T_end0,N) Exact = np.zeros([N,1]) Proxy_1FHW= np.zeros ([N,1]) Yield = np.zeros ([N,1]) for i,Ti in enumerate(Tgrid): Proxy_1FHW[i] = HW_ZCB(lambd,eta,P0T,0.0,Ti,r0) Exact[i] = P0T(Ti) Yield[i] = -np.log(Proxy_1FHW[i])/Ti plt.figure(1) plt.grid() plt.xlabel('T') plt.xlabel('ZCB, P(0,t)') plt.plot(Tgrid,Exact,'-k') plt.plot(Tgrid,Proxy_1FHW,'--r') plt.legend(["Analytcal ZCB","ZCB- 1F Model","ZCB- 2F Model"]) plt.title('P(0,T) from Monte Carlo vs. Analytical expression') # Yield plt.figure(2) plt.grid() plt.plot(Tgrid,Yield,'-k') plt.xlabel('T') plt.ylabel('yield, r(0,T)') "Monte Carlo part" T_end = 10.0 paths= GeneratePathsHWEuler(NoOfPaths,NoOfSteps,T_end ,P0T, lambd, eta) r = paths["R"] timeGrid = paths["time"] # Yield Curves on the last point plt.figure(3) plt.xlabel('time') plt.ylabel('r(t)') plt.title('MC Paths + Yield Curve (Hull-White)') plt.grid() T_end2 = T_end + 40.0 Tgrid2= np.linspace(T_end+0.001,T_end2-0.01,N) ZCB = np.zeros([N,1]) r_T = r[:,-1] for i in range(0,20): for j,Tj in enumerate(Tgrid2): ZCB[j] = HW_ZCB(lambd,eta,P0T,T_end,Tj,r_T[i]) Yield[j] = -np.log(ZCB[j])/(Tj-T_end) plt.plot(Tgrid2, Yield) plt.plot(timeGrid,r[i,:]) mainCalculation() ================================================ FILE: Lecture 04-Yield Curve Dynamics under Short Rate/Materials/Hull-White-ZCBs2.py ================================================ #%% """ Created on July 12 2021 Hull-White Model, Simulation of the Model + Computation of ZCBs, P(0,t) This code is purely educational and comes from "Financial Engineering" course by L.A. Grzelak The course is based on the book “Mathematical Modeling and Computation in Finance: With Exercises and Python and MATLAB Computer Codes”, by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing Europe Ltd, 2019. @author: Lech A. Grzelak """ import numpy as np import matplotlib.pyplot as plt import scipy.integrate as integrate def f0T(t,P0T): # time-step needed for differentiation dt = 0.01 expr = - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) return expr def GeneratePathsHWEuler(NoOfPaths,NoOfSteps,T,P0T, lambd, eta): # Initial interest rate is a forward rate at time t->0 r0 = f0T(0.01,P0T) theta = lambda t: 1.0/lambd * (f0T(t+dt,P0T)-f0T(t-dt,P0T))/(2.0*dt) + f0T(t,P0T) + eta*eta/(2.0*lambd*lambd)*(1.0-np.exp(-2.0*lambd*t)) #theta = lambda t: 0.1 +t -t #print("changed theta") Z = np.random.normal(0.0,1.0,[NoOfPaths,NoOfSteps]) W = np.zeros([NoOfPaths, NoOfSteps+1]) R = np.zeros([NoOfPaths, NoOfSteps+1]) M = np.zeros([NoOfPaths, NoOfSteps+1]) M[:,0]= 1.0 R[:,0]= r0 time = np.zeros([NoOfSteps+1]) dt = T / float(NoOfSteps) for i in range(0,NoOfSteps): # making sure that samples from normal have mean 0 and variance 1 if NoOfPaths > 1: Z[:,i] = (Z[:,i] - np.mean(Z[:,i])) / np.std(Z[:,i]) W[:,i+1] = W[:,i] + np.power(dt, 0.5)*Z[:,i] R[:,i+1] = R[:,i] + lambd*(theta(time[i]) - R[:,i]) * dt + eta* (W[:,i+1]-W[:,i]) M[:,i+1] = M[:,i] * np.exp((R[:,i+1]+R[:,i])*0.5*dt) time[i+1] = time[i] +dt # Outputs paths = {"time":time,"R":R,"M":M} return paths def HW_theta(lambd,eta,P0T): dt = 0.01 theta = lambda t: 1.0/lambd * (f0T(t+dt,P0T)-f0T(t-dt,P0T))/(2.0*dt) + f0T(t,P0T) + eta*eta/(2.0*lambd*lambd)*(1.0-np.exp(-2.0*lambd*t)) #print("CHANGED THETA") return theta#lambda t: 0.1+t-t def HW_A(lambd,eta,P0T,T1,T2): tau = T2-T1 zGrid = np.linspace(0.0,tau,250) B_r = lambda tau: 1.0/lambd * (np.exp(-lambd *tau)-1.0) theta = HW_theta(lambd,eta,P0T) temp1 = lambd * integrate.trapz(theta(T2-zGrid)*B_r(zGrid),zGrid) temp2 = eta*eta/(4.0*np.power(lambd,3.0)) * (np.exp(-2.0*lambd*tau)*(4*np.exp(lambd*tau)-1.0) -3.0) + eta*eta*tau/(2.0*lambd*lambd) return temp1 + temp2 def HW_B(lambd,eta,T1,T2): return 1.0/lambd *(np.exp(-lambd*(T2-T1))-1.0) def HW_ZCB(lambd,eta,P0T,T1,T2,rT1): B_r = HW_B(lambd,eta,T1,T2) A_r = HW_A(lambd,eta,P0T,T1,T2) return np.exp(A_r + B_r *rT1) def mainCalculation(): NoOfPaths = 20000 NoOfSteps = 25 lambd = 0.04 eta = 0.01 # We define a ZCB curve (obtained from the market) P0T = lambda T: np.exp(-0.1*T) # Initial r_0 r0 = f0T(0.01,P0T) # In this experiment we compare ZCB from the Market and Monte Carlo "Monte Carlo part" T = 40 paths= GeneratePathsHWEuler(NoOfPaths,NoOfSteps,T,P0T, lambd, eta) M = paths["M"] ti = paths["time"] #dt = timeGrid[1]-timeGrid[0] # Here we compare the price of an option on a ZCB from Monte Carlo the Market P_tMC = np.zeros([NoOfSteps+1]) for i in range(0,NoOfSteps+1): P_tMC[i] = np.mean(1.0/M[:,i]) # Analytical expression for ZCB using the Affine properties of the HW model P_tHW = np.zeros([NoOfSteps+1]) for i in range(0,NoOfSteps+1): P_tHW[i] = HW_ZCB(lambd,eta,P0T,0.0,ti[i],r0) plt.figure(1) plt.grid() plt.xlabel('T') plt.ylabel('P(0,T)') plt.plot(ti,P0T(ti)) plt.plot(ti,P_tMC,'--r') plt.plot(ti,P_tHW,'.k') plt.legend(['P(0,t) market','P(0,t) Monte Carlo','P(0,t) from Affine form']) plt.title('ZCBs from Hull-White Model') mainCalculation() ================================================ FILE: Lecture 04-Yield Curve Dynamics under Short Rate/Materials/Hull_White_1F_2F_Comparison.py ================================================ #%% """ Created on July 12 2021 Yield Curve shapes and the Hull-White Model (1 and 2 factor cases) This code is purely educational and comes from "Financial Engineering" course by L.A. Grzelak The course is based on the book “Mathematical Modeling and Computation in Finance: With Exercises and Python and MATLAB Computer Codes”, by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing Europe Ltd, 2019. @author: Lech A. Grzelak """ import numpy as np import matplotlib.pyplot as plt import scipy.integrate as integrate from scipy import interpolate def GeneratePathsHW2FEuler(NoOfPaths,NoOfSteps,T,P0T, lambd1,lambd2, eta1,eta2,rho): # time-step needed for differentiation phi = lambda t: f0T(t,P0T) + (eta1**2.0)/(2.0*lambd1**2.0)*(1.0-np.exp(-lambd1*t))*(1.0-np.exp(-lambd1*t))+\ +(eta2**2.0)/(2.0*lambd2**2.0)*(1.0-np.exp(-lambd2*t))*(1.0-np.exp(-lambd2*t))+\ + rho*eta1*eta2/(lambd1*lambd2)*(1.0-np.exp(-lambd1*t))*(1.0-np.exp(-lambd2*t)) Z1 = np.random.normal(0.0,1.0,[NoOfPaths,NoOfSteps]) Z2 = np.random.normal(0.0,1.0,[NoOfPaths,NoOfSteps]) W1 = np.zeros([NoOfPaths, NoOfSteps+1]) W2 = np.zeros([NoOfPaths, NoOfSteps+1]) X = np.zeros([NoOfPaths, NoOfSteps+1]) Y = np.zeros([NoOfPaths, NoOfSteps+1]) R = np.zeros([NoOfPaths, NoOfSteps+1]) R[:,0] = phi(0) time = np.zeros([NoOfSteps+1]) dt = T / float(NoOfSteps) for i in range(0,NoOfSteps): # making sure that samples from normal have mean 0 and variance 1 if NoOfPaths > 1: Z1[:,i] = (Z1[:,i] - np.mean(Z1[:,i])) / np.std(Z1[:,i]) Z2[:,i] = (Z2[:,i] - np.mean(Z2[:,i])) / np.std(Z2[:,i]) Z2[:,i] = rho * Z1[:,i] + np.sqrt(1.0-rho**2)*Z2[:,i] W1[:,i+1] = W1[:,i] + np.power(dt, 0.5)*Z1[:,i] W2[:,i+1] = W2[:,i] + np.power(dt, 0.5)*Z2[:,i] X[:,i+1] = X[:,i] - lambd1*X[:,i] * dt + eta1* (W1[:,i+1]-W1[:,i]) Y[:,i+1] = Y[:,i] - lambd2*Y[:,i] * dt + eta2* (W2[:,i+1]-W2[:,i]) time[i+1] = time[i] +dt R[:,i+1] = X[:,i+1] + Y[:,i+1] + phi(time[i+1]) # Outputs paths = {"time":time,"R":R,"X":X,"Y":Y} return paths def f0T(t,P0T): # time-step needed for differentiation dt = 0.01 expr = - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) return expr def GeneratePathsHWEuler(NoOfPaths,NoOfSteps,T,P0T, lambd, eta): # Initial interest rate is a forward rate at time t->0 r0 = f0T(0.01,P0T) theta = lambda t: 1.0/lambd * (f0T(t+dt,P0T)-f0T(t-dt,P0T))/(2.0*dt) + f0T(t,P0T) + eta*eta/(2.0*lambd*lambd)*(1.0-np.exp(-2.0*lambd*t)) #theta = lambda t: 0.1 +t -t #print("changed theta") Z = np.random.normal(0.0,1.0,[NoOfPaths,NoOfSteps]) W = np.zeros([NoOfPaths, NoOfSteps+1]) R = np.zeros([NoOfPaths, NoOfSteps+1]) R[:,0]=r0 time = np.zeros([NoOfSteps+1]) dt = T / float(NoOfSteps) for i in range(0,NoOfSteps): # making sure that samples from normal have mean 0 and variance 1 if NoOfPaths > 1: Z[:,i] = (Z[:,i] - np.mean(Z[:,i])) / np.std(Z[:,i]) W[:,i+1] = W[:,i] + np.power(dt, 0.5)*Z[:,i] R[:,i+1] = R[:,i] + lambd*(theta(time[i]) - R[:,i]) * dt + eta* (W[:,i+1]-W[:,i]) time[i+1] = time[i] +dt # Outputs paths = {"time":time,"R":R} return paths def HW_theta(lambd,eta,P0T): dt = 0.01 theta = lambda t: 1.0/lambd * (f0T(t+dt,P0T)-f0T(t-dt,P0T))/(2.0*dt) + f0T(t,P0T) + eta*eta/(2.0*lambd*lambd)*(1.0-np.exp(-2.0*lambd*t)) #print("CHANGED THETA") return theta#lambda t: 0.1+t-t def HW_A(lambd,eta,P0T,T1,T2): tau = T2-T1 zGrid = np.linspace(0.0,tau,250) B_r = lambda tau: 1.0/lambd * (np.exp(-lambd *tau)-1.0) theta = HW_theta(lambd,eta,P0T) temp1 = lambd * integrate.trapz(theta(T2-zGrid)*B_r(zGrid),zGrid) temp2 = eta*eta/(4.0*np.power(lambd,3.0)) * (np.exp(-2.0*lambd*tau)*(4*np.exp(lambd*tau)-1.0) -3.0) + eta*eta*tau/(2.0*lambd*lambd) return temp1 + temp2 def HW_B(lambd,eta,T1,T2): return 1.0/lambd *(np.exp(-lambd*(T2-T1))-1.0) def HW2F_ZCB(lambd1,lambd2,eta1,eta2,rho,P0T,T1,T2,xT1,yT1): V = lambda t,T: (eta1**2.0)/(lambd1**2.0)*((T-t)+2.0/lambd1*np.exp(-lambd1*(T-t))-1.0/(2.0*lambd1)*np.exp(-2.0*lambd1*(T-t))-3.0/(2.0*lambd1))+\ +(eta2**2.0)/(lambd2**2.0)*((T-t)+2.0/lambd2*np.exp(-lambd2*(T-t))-1.0/(2.0*lambd2)*np.exp(-2.0*lambd2*(T-t))-3.0/(2.0*lambd2))+\ + 2.0*rho*eta1*eta2/(lambd1*lambd2)*(T-t + 1.0/lambd1*(np.exp(-lambd1*(T-t))-1.0)+ 1.0/lambd2*(np.exp(-lambd2*(T-t))-1.0) - 1.0/(lambd1+lambd2)*(np.exp(-(lambd1+lambd2)*(T-t))-1.0)) intPhi = - np.log(P0T(T2)/P0T(T1)*np.exp(-0.5*(V(0,T2)-V(0,T1)))) A = 1.0/lambd1 * (1.0-np.exp(-lambd1 * (T2-T1))) B = 1.0/lambd2 * (1.0-np.exp(-lambd2 * (T2-T1))) return np.exp(- intPhi -A * xT1 - B * yT1 + 0.5 * V(T1,T2)) def HW_ZCB(lambd,eta,P0T,T1,T2,rT1): B_r = HW_B(lambd,eta,T1,T2) A_r = HW_A(lambd,eta,P0T,T1,T2) return np.exp(A_r + B_r *rT1) def HW_r_0(P0T,lambd,eta): r0 = f0T(0.001,P0T) return r0 def mainCalculation(): NoOfPaths = 2000 NoOfSteps = 100 #HW1F lambd = 0.01 eta = 0.002 #HW2F lambd2 = 0.1 eta2 = 0.002 rho = -0.2 # We define a ZCB curve (obtained from the market), 1-D points with a B-spline curve. ti = [0.0,0.00273972600000000,0.0876712330000000,0.172602740000000,0.257534247000000,0.342465753000000,0.427397260000000,0.512328767000000,0.597260274000000,0.682191781000000,0.767123288000000,0.852054795000000,0.936986301000000,1.02191780800000,1.10684931500000,1.19178082200000,1.27671232900000,1.36164383600000,1.44657534200000,1.53150684900000,1.61643835600000,1.70136986300000,1.78630137000000,1.87123287700000,1.95616438400000,2.04109589000000,2.12602739700000,2.21095890400000,2.29589041100000,2.38082191800000,2.46575342500000,2.55068493200000,2.63561643800000,2.72054794500000,2.80547945200000,2.89041095900000,2.97534246600000,3.06027397300000,3.14520547900000,3.23013698600000,3.31506849300000,3.40000000000000,3.48493150700000,3.56986301400000,3.65479452100000,3.73972602700000,3.82465753400000,3.90958904100000,3.99452054800000,4.07945205500000,4.16438356200000,4.24931506800000,4.33424657500000,4.41917808200000,4.50410958900000,4.58904109600000,4.67397260300000,4.75890411000000,4.84383561600000,4.92876712300000,5.01369863000000,5.09863013700000,5.18356164400000,5.26849315100000,5.35342465800000,5.43835616400000,5.52328767100000,5.60821917800000,5.69315068500000,5.77808219200000,5.86301369900000,5.94794520500000,6.03287671200000,6.11780821900000,6.20273972600000,6.28767123300000,6.37260274000000,6.45753424700000,6.54246575300000,6.62739726000000,6.71232876700000,6.79726027400000,6.88219178100000,6.96712328800000,7.05205479500000,7.13698630100000,7.22191780800000,7.30684931500000,7.39178082200000,7.47671232900000,7.56164383600000,7.64657534200000,7.73150684900000,7.81643835600000,7.90136986300000,7.98630137000000,8.07123287700000,8.15616438400000,8.24109589000000,8.32602739700000,8.41095890400000,8.49589041100000,8.58082191800000,8.66575342500000,8.75068493200000,8.83561643800000,8.92054794500000,9.00547945200000,9.09041095900000,9.17534246600000,9.26027397300000,9.34520547900000,9.43013698600000,9.51506849300000,9.60000000000000,9.68493150700000,9.76986301400000,9.85479452100000,9.93972602700000,10.0246575300000,10.1095890400000,10.1945205500000,10.2794520500000,10.3643835600000,10.4493150700000,10.5342465800000,10.6191780800000,10.7041095900000,10.7890411000000,10.8739726000000,10.9589041100000,11.0438356200000,11.1287671200000,11.2136986300000,11.2986301400000,11.3835616400000,11.4684931500000,11.5534246600000,11.6383561600000,11.7232876700000,11.8082191800000,11.8931506800000,11.9780821900000,12.0630137000000,12.1479452100000,12.2328767100000,12.3178082200000,12.4027397300000,12.4876712300000,12.5726027400000,12.6575342500000,12.7424657500000,12.8273972600000,12.9123287700000,12.9972602700000,13.0821917800000,13.1671232900000,13.2520547900000,13.3369863000000,13.4219178100000,13.5068493200000,13.5917808200000,13.6767123300000,13.7616438400000,13.8465753400000,13.9315068500000,14.0164383600000,14.1013698600000,14.1863013700000,14.2712328800000,14.3561643800000,14.4410958900000,14.5260274000000,14.6109589000000,14.6958904100000,14.7808219200000,14.8657534200000,14.9506849300000,15.0356164400000,15.1205479500000,15.2054794500000,15.2904109600000,15.3753424700000,15.4602739700000,15.5452054800000,15.6301369900000,15.7150684900000,15.8000000000000,15.8849315100000,15.9698630100000,16.0547945200000,16.1397260300000,16.2246575300000,16.3095890400000,16.3945205500000,16.4794520500000,16.5643835600000,16.6493150700000,16.7342465800000,16.8191780800000,16.9041095900000,16.9890411000000,17.0739726000000,17.1589041100000,17.2438356200000,17.3287671200000,17.4136986300000,17.4986301400000,17.5835616400000,17.6684931500000,17.7534246600000,17.8383561600000,17.9232876700000,18.0082191800000,18.0931506800000,18.1780821900000,18.2630137000000,18.3479452100000,18.4328767100000,18.5178082200000,18.6027397300000,18.6876712300000,18.7726027400000,18.8575342500000,18.9424657500000,19.0273972600000,19.1123287700000,19.1972602700000,19.2821917800000,19.3671232900000,19.4520547900000,19.5369863000000,19.6219178100000,19.7068493200000,19.7917808200000,19.8767123300000,19.9616438400000,20.0465753400000,20.1315068500000,20.2164383600000,20.3013698600000,20.3863013700000,20.4712328800000,20.5561643800000,20.6410958900000,20.7260274000000,20.8109589000000,20.8958904100000,20.9808219200000,21.0657534200000,21.1506849300000,21.2356164400000,21.3205479500000,21.4054794500000,21.4904109600000,21.5753424700000,21.6602739700000,21.7452054800000,21.8301369900000,21.9150684900000,22,22.0849315100000,22.1698630100000,22.2547945200000,22.3397260300000,22.4246575300000,22.5095890400000,22.5945205500000,22.6794520500000,22.7643835600000,22.8493150700000,22.9342465800000,23.0191780800000,23.1041095900000,23.1890411000000,23.2739726000000,23.3589041100000,23.4438356200000,23.5287671200000,23.6136986300000,23.6986301400000,23.7835616400000,23.8684931500000,23.9534246600000,24.0383561600000,24.1232876700000,24.2082191800000,24.2931506800000,24.3780821900000,24.4630137000000,24.5479452100000,24.6328767100000,24.7178082200000,24.8027397300000,24.8876712300000,24.9726027400000,25.0575342500000,25.1424657500000,25.2273972600000,25.3123287700000,25.3972602700000,25.4821917800000,25.5671232900000,25.6520547900000,25.7369863000000,25.8219178100000,25.9068493200000,25.9917808200000,26.0767123300000,26.1616438400000,26.2465753400000,26.3315068500000,26.4164383600000,26.5013698600000,26.5863013700000,26.6712328800000,26.7561643800000,26.8410958900000,26.9260274000000,27.0109589000000,27.0958904100000,27.1808219200000,27.2657534200000,27.3506849300000,27.4356164400000,27.5205479500000,27.6054794500000,27.6904109600000,27.7753424700000,27.8602739700000,27.9452054800000,28.0301369900000,28.1150684900000,28.2000000000000,28.2849315100000,28.3698630100000,28.4547945200000,28.5397260300000,28.6246575300000,28.7095890400000,28.7945205500000,28.8794520500000,28.9643835600000,29.0493150700000,29.1342465800000,29.2191780800000,29.3041095900000,29.3890411000000,29.4739726000000,29.5589041100000,29.6438356200000,29.7287671200000,29.8136986300000,29.8986301400000,29.9835616400000,30.0684931500000,30.1534246600000,30.2383561600000,30.3232876700000,30.4082191800000,30.4931506800000,30.5780821900000,30.6630137000000,30.7479452100000,30.8328767100000,30.9178082200000,31.0027397300000,31.0876712300000,31.1726027400000,31.2575342500000,31.3424657500000,31.4273972600000,31.5123287700000,31.5972602700000,31.6821917800000,31.7671232900000,31.8520547900000,31.9369863000000,32.0219178100000,32.1068493200000,32.1917808200000,32.2767123300000,32.3616438400000,32.4465753400000,32.5315068500000,32.6164383600000,32.7013698600000,32.7863013700000,32.8712328800000,32.9561643800000,33.0410958900000,33.1260274000000,33.2109589000000,33.2958904100000,33.3808219200000,33.4657534200000,33.5506849300000,33.6356164400000,33.7205479500000,33.8054794500000,33.8904109600000,33.9753424700000,34.0602739700000,34.1452054800000,34.2301369900000,34.3150684900000,34.4000000000000,34.4849315100000,34.5698630100000,34.6547945200000,34.7397260300000,34.8246575300000,34.9095890400000,34.9945205500000,35.0794520500000,35.1643835600000,35.2493150700000,35.3342465800000,35.4191780800000,35.5041095900000,35.5890411000000,35.6739726000000,35.7589041100000,35.8438356200000,35.9287671200000,36.0136986300000,36.0986301400000,36.1835616400000,36.2684931500000,36.3534246600000,36.4383561600000,36.5232876700000,36.6082191800000,36.6931506800000,36.7780821900000,36.8630137000000,36.9479452100000,37.0328767100000,37.1178082200000,37.2027397300000,37.2876712300000,37.3726027400000,37.4575342500000,37.5424657500000,37.6273972600000,37.7123287700000,37.7972602700000,37.8821917800000,37.9671232900000,38.0520547900000,38.1369863000000,38.2219178100000,38.3068493200000,38.3917808200000,38.4767123300000,38.5616438400000,38.6465753400000,38.7315068500000,38.8164383600000,38.9013698600000,38.9863013700000,39.0712328800000,39.1561643800000,39.2410958900000,39.3260274000000,39.4109589000000,39.4958904100000,39.5808219200000,39.6657534200000,39.7506849300000,39.8356164400000,39.9205479500000,40.0054794500000] Pi = [1.0,0.999966573000000,0.998930882000000,0.997824062000000,0.996511145000000,0.995199956000000,0.993821602000000,0.992277014000000,0.990734827000000,0.989164324000000,0.987428762000000,0.985704346000000,0.983946708000000,0.982068207000000,0.980193293000000,0.978281187000000,0.976255832000000,0.974234670000000,0.972174514000000,0.970028236000000,0.967886697000000,0.965693800000000,0.963440984000000,0.961193424000000,0.958903753000000,0.956575247000000,0.954252397000000,0.951842433000000,0.949228406000000,0.946482248000000,0.943632525000000,0.940707509000000,0.937735160000000,0.934743101000000,0.931758611000000,0.928808623000000,0.925919731000000,0.923110403000000,0.920338655000000,0.917589739000000,0.914858924000000,0.912141532000000,0.909432941000000,0.906728583000000,0.904023944000000,0.901314567000000,0.898596044000000,0.895864025000000,0.893114214000000,0.890346348000000,0.887569841000000,0.884786140000000,0.881996090000000,0.879200528000000,0.876400278000000,0.873596157000000,0.870788975000000,0.867979528000000,0.865168605000000,0.862356987000000,0.859545442000000,0.856732322000000,0.853915005000000,0.851094371000000,0.848271318000000,0.845446732000000,0.842621487000000,0.839796449000000,0.836972470000000,0.834150393000000,0.831331050000000,0.828515261000000,0.825703790000000,0.822893949000000,0.820084325000000,0.817275636000000,0.814468590000000,0.811663887000000,0.808862219000000,0.806064266000000,0.803270701000000,0.800482187000000,0.797699379000000,0.794922921000000,0.792153338000000,0.789389717000000,0.786631547000000,0.783878633000000,0.781130782000000,0.778387801000000,0.775649505000000,0.772915708000000,0.770186227000000,0.767460882000000,0.764739497000000,0.762021898000000,0.759308200000000,0.756599781000000,0.753896558000000,0.751198194000000,0.748504356000000,0.745814715000000,0.743128950000000,0.740446741000000,0.737767776000000,0.735091745000000,0.732418345000000,0.729747277000000,0.727078670000000,0.724413443000000,0.721751645000000,0.719093279000000,0.716438346000000,0.713786850000000,0.711138792000000,0.708494177000000,0.705853006000000,0.703215284000000,0.700581015000000,0.697950202000000,0.695320544000000,0.692690217000000,0.690059929000000,0.687430378000000,0.684802253000000,0.682176235000000,0.679552992000000,0.676933185000000,0.674317462000000,0.671706465000000,0.669100824000000,0.666501159000000,0.663908082000000,0.661322194000000,0.658744089000000,0.656174348000000,0.653613545000000,0.651062245000000,0.648521002000000,0.645990363000000,0.643470865000000,0.640963036000000,0.638467395000000,0.635983411000000,0.633503959000000,0.631028021000000,0.628556105000000,0.626088714000000,0.623626343000000,0.621169478000000,0.618718599000000,0.616274178000000,0.613836679000000,0.611406558000000,0.608984265000000,0.606570242000000,0.604164924000000,0.601768738000000,0.599382105000000,0.597005438000000,0.594639144000000,0.592283621000000,0.589939262000000,0.587606453000000,0.585285574000000,0.582976996000000,0.580681085000000,0.578398202000000,0.576128698000000,0.573872922000000,0.571631214000000,0.569403908000000,0.567191334000000,0.564993814000000,0.562811666000000,0.560645202000000,0.558494727000000,0.556360543000000,0.554242913000000,0.552138115000000,0.550043470000000,0.547959012000000,0.545884776000000,0.543820795000000,0.541767101000000,0.539723726000000,0.537690699000000,0.535668051000000,0.533655809000000,0.531654001000000,0.529662652000000,0.527681790000000,0.525711438000000,0.523751620000000,0.521802360000000,0.519863678000000,0.517935598000000,0.516018137000000,0.514111318000000,0.512215157000000,0.510329674000000,0.508454885000000,0.506590807000000,0.504737456000000,0.502894847000000,0.501062994000000,0.499241910000000,0.497431609000000,0.495632103000000,0.493843403000000,0.492065521000000,0.490298466000000,0.488542249000000,0.486796879000000,0.485062364000000,0.483338711000000,0.481625929000000,0.479924024000000,0.478233003000000,0.476552870000000,0.474883632000000,0.473225293000000,0.471577858000000,0.469941330000000,0.468315712000000,0.466701007000000,0.465097218000000,0.463504347000000,0.461922395000000,0.460351364000000,0.458791253000000,0.457242065000000,0.455703798000000,0.454176452000000,0.452660027000000,0.451154522000000,0.449659935000000,0.448176257000000,0.446703083000000,0.445240105000000,0.443787209000000,0.442344286000000,0.440911226000000,0.439487920000000,0.438074261000000,0.436670143000000,0.435275460000000,0.433890109000000,0.432513986000000,0.431146989000000,0.429789017000000,0.428439971000000,0.427099751000000,0.425768260000000,0.424445399000000,0.423131074000000,0.421825189000000,0.420527650000000,0.419238363000000,0.417957237000000,0.416684180000000,0.415419101000000,0.414161910000000,0.412912520000000,0.411670842000000,0.410436789000000,0.409210275000000,0.407991213000000,0.406779521000000,0.405575114000000,0.404377909000000,0.403187824000000,0.402004777000000,0.400828688000000,0.399659478000000,0.398497066000000,0.397341375000000,0.396192326000000,0.395049844000000,0.393913852000000,0.392784274000000,0.391661036000000,0.390544064000000,0.389433283000000,0.388328623000000,0.387230010000000,0.386137373000000,0.385050641000000,0.383969745000000,0.382894615000000,0.381825182000000,0.380761378000000,0.379703135000000,0.378650387000000,0.377603067000000,0.376561110000000,0.375524516000000,0.374495509000000,0.373475055000000,0.372462955000000,0.371459016000000,0.370463043000000,0.369474847000000,0.368494238000000,0.367521029000000,0.366555034000000,0.365596072000000,0.364643959000000,0.363698516000000,0.362759566000000,0.361826932000000,0.360900441000000,0.359979918000000,0.359065194000000,0.358156099000000,0.357252465000000,0.356354126000000,0.355460918000000,0.354572678000000,0.353689243000000,0.352810455000000,0.351936156000000,0.351066187000000,0.350200393000000,0.349338622000000,0.348480719000000,0.347626535000000,0.346775919000000,0.345928722000000,0.345084799000000,0.344244003000000,0.343406190000000,0.342571217000000,0.341738943000000,0.340909228000000,0.340081932000000,0.339256917000000,0.338434048000000,0.337613190000000,0.336794207000000,0.335976969000000,0.335161343000000,0.334347200000000,0.333534411000000,0.332722847000000,0.331912384000000,0.331102895000000,0.330294257000000,0.329486347000000,0.328679044000000,0.327872227000000,0.327065778000000,0.326259578000000,0.325453511000000,0.324647462000000,0.323841447000000,0.323037558000000,0.322236538000000,0.321438361000000,0.320642997000000,0.319850421000000,0.319060604000000,0.318273519000000,0.317489140000000,0.316707441000000,0.315928394000000,0.315151974000000,0.314378155000000,0.313606911000000,0.312838216000000,0.312072045000000,0.311308373000000,0.310547175000000,0.309788426000000,0.309032102000000,0.308278178000000,0.307526629000000,0.306777432000000,0.306030564000000,0.305285999000000,0.304543716000000,0.303803690000000,0.303065898000000,0.302330318000000,0.301596926000000,0.300865701000000,0.300136619000000,0.299409659000000,0.298684798000000,0.297962014000000,0.297241286000000,0.296522593000000,0.295805912000000,0.295091222000000,0.294378503000000,0.293667734000000,0.292958894000000,0.292251962000000,0.291546917000000,0.290843741000000,0.290142412000000,0.289442910000000,0.288745217000000,0.288049311000000,0.287355175000000,0.286662788000000,0.285972131000000,0.285283185000000,0.284595932000000,0.283910353000000,0.283226429000000,0.282544142000000,0.281863473000000,0.281184405000000,0.280506920000000,0.279830999000000,0.279156625000000,0.278483781000000,0.277812449000000,0.277142612000000,0.276474253000000,0.275807354000000,0.275141900000000,0.274477873000000,0.273815257000000,0.273154036000000,0.272494193000000,0.271835712000000,0.271178577000000,0.270522772000000,0.269868283000000,0.269215092000000,0.268563184000000,0.267912545000000,0.267263159000000,0.266615011000000,0.265968086000000,0.265322370000000,0.264677847000000,0.264034503000000,0.263392324000000,0.262751295000000,0.262111403000000,0.261472633000000,0.260834972000000,0.260198405000000,0.259562919000000,0.258928501000000,0.258295136000000,0.257662813000000,0.257031517000000,0.256401236000000,0.255771957000000,0.255143667000000,0.254516353000000,0.253890002000000,0.253264603000000,0.252640144000000,0.252016611000000,0.251393992000000,0.250772277000000,0.250151453000000,0.249531508000000,0.248912431000000,0.248294210000000,0.247676834000000,0.247060291000000,0.246444571000000,0.245829663000000,0.245215555000000,0.244602237000000,0.243989698000000,0.243377928000000] interpolator = interpolate.splrep(ti, Pi, s=0.0001) P0T = lambda T: interpolate.splev(T, interpolator, der=0) r0 = HW_r_0(P0T,lambd,eta) # In this experiment we compare ZCB from the Market and Analytical expression N = 20 T_end0 = 39.0 Tgrid= np.linspace(0.1,T_end0,N) Exact = np.zeros([N,1]) Proxy_1FHW= np.zeros ([N,1]) Proxy_2FHW= np.zeros ([N,1]) Yield = np.zeros ([N,1]) for i,Ti in enumerate(Tgrid): Proxy_1FHW[i] = HW_ZCB(lambd,eta,P0T,0.0,Ti,r0) Proxy_2FHW[i] = HW2F_ZCB(lambd,lambd2,eta,eta2,rho,P0T,0.0,Ti,0.0,0.0) Exact[i] = P0T(Ti) Yield[i] = -np.log(Proxy_1FHW[i])/Ti plt.figure(1) plt.grid() plt.plot(Tgrid,Exact,'-k') plt.plot(Tgrid,Proxy_1FHW,'--r') plt.plot(Tgrid,Proxy_2FHW,'.k') plt.legend(["Analytcal ZCB","ZCB- 1F Model","ZCB- 2F Model"]) plt.title('P(0,T) from Monte Carlo vs. Analytical expression') "Monte Carlo part" T_end = 10.0 paths= GeneratePathsHWEuler(NoOfPaths,NoOfSteps,T_end ,P0T, lambd, eta) r = paths["R"] timeGrid = paths["time"] # Yield #r = -np.log(Proxy)/T plt.figure(2) plt.grid() plt.plot(Tgrid,Yield,'-k') # Yield Curves on the last point plt.figure(3) plt.xlabel('time') plt.ylabel('r(t)') plt.title('MC Paths + Yield Curve (Hull-White)') plt.grid() T_end2 = T_end + 40.0 Tgrid2= np.linspace(T_end+0.001,T_end2-0.01,N) ZCB = np.zeros([N,1]) r_T = r[:,-1] for i in range(0,20): for j,Tj in enumerate(Tgrid2): ZCB[j] = HW_ZCB(lambd,eta,P0T,T_end,Tj,r_T[i]) Yield[j] = -np.log(ZCB[j])/(Tj-T_end) plt.plot(Tgrid2, Yield) plt.plot(timeGrid,r[i,:]) # Analysis for the 2F model paths= GeneratePathsHW2FEuler(NoOfPaths,NoOfSteps,T_end,P0T, lambd,lambd2, eta,eta2,rho) x = paths["X"] y = paths["Y"] r = paths["R"] timeGrid = paths["time"] # Yield Curves on the last point plt.figure(4) plt.xlabel('time') plt.ylabel('r(t)') plt.title('MC Paths + Yield Curve (Hull-White 2F)') plt.grid() x_T = x[:,-1] y_T = y[:,-1] r_T = r[:,-1] Tgrid2= np.linspace(T_end+0.001,T_end2,N) ZCB = np.zeros([N,1]) for i in range(0,20): for j,Tj in enumerate(Tgrid2): ZCB[j] = HW2F_ZCB(lambd,lambd2,eta,eta2,rho,P0T,T_end,Tj,x_T[i],y_T[i]) #HW_ZCB(lambd,eta,P0T,T_end,Tj,r_T[i]) Yield[j] = -np.log(ZCB[j])/(Tj-T_end) plt.plot(timeGrid,r[i,:]) plt.plot(Tgrid2, Yield) mainCalculation() ================================================ FILE: Lecture 05-Interest Rate Products/Materials/HW_Caplets.py ================================================ #%% """ Created on July 12 2021 Caplets under the Hull-White Model This code is purely educational and comes from "Financial Engineering" course by L.A. Grzelak The course is based on the book “Mathematical Modeling and Computation in Finance: With Exercises and Python and MATLAB Computer Codes”, by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing Europe Ltd, 2019. @author: Lech A. Grzelak """ import numpy as np import enum import matplotlib.pyplot as plt import scipy.stats as st import scipy.integrate as integrate # This class defines puts and calls class OptionType(enum.Enum): CALL = 1.0 PUT = -1.0 def GeneratePathsHWEuler(NoOfPaths,NoOfSteps,T,P0T, lambd, eta): # time-step needed for differentiation dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) # Initial interest rate is a forward rate at time t->0 r0 = f0T(0.00001) theta = lambda t: 1.0/lambd * (f0T(t+dt)-f0T(t-dt))/(2.0*dt) + f0T(t) + eta*eta/(2.0*lambd*lambd)*(1.0-np.exp(-2.0*lambd*t)) #theta = lambda t: 0.1 +t -t #print("changed theta") Z = np.random.normal(0.0,1.0,[NoOfPaths,NoOfSteps]) W = np.zeros([NoOfPaths, NoOfSteps+1]) R = np.zeros([NoOfPaths, NoOfSteps+1]) R[:,0]=r0 time = np.zeros([NoOfSteps+1]) dt = T / float(NoOfSteps) for i in range(0,NoOfSteps): # making sure that samples from normal have mean 0 and variance 1 if NoOfPaths > 1: Z[:,i] = (Z[:,i] - np.mean(Z[:,i])) / np.std(Z[:,i]) W[:,i+1] = W[:,i] + np.power(dt, 0.5)*Z[:,i] R[:,i+1] = R[:,i] + lambd*(theta(time[i]) - R[:,i]) * dt + eta* (W[:,i+1]-W[:,i]) time[i+1] = time[i] +dt # Outputs paths = {"time":time,"R":R} return paths def HW_theta(lambd,eta,P0T): dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) theta = lambda t: 1.0/lambd * (f0T(t+dt)-f0T(t-dt))/(2.0*dt) + f0T(t) + eta*eta/(2.0*lambd*lambd)*(1.0-np.exp(-2.0*lambd*t)) #print("CHANGED THETA") return theta#lambda t: 0.1+t-t def HW_A(lambd,eta,P0T,T1,T2): tau = T2-T1 zGrid = np.linspace(0.0,tau,250) B_r = lambda tau: 1.0/lambd * (np.exp(-lambd *tau)-1.0) theta = HW_theta(lambd,eta,P0T) temp1 = lambd * integrate.trapz(theta(T2-zGrid)*B_r(zGrid),zGrid) temp2 = eta*eta/(4.0*np.power(lambd,3.0)) * (np.exp(-2.0*lambd*tau)*(4*np.exp(lambd*tau)-1.0) -3.0) + eta*eta*tau/(2.0*lambd*lambd) return temp1 + temp2 def HW_B(lambd,eta,T1,T2): return 1.0/lambd *(np.exp(-lambd*(T2-T1))-1.0) def HW_ZCB(lambd,eta,P0T,T1,T2,rT1): B_r = HW_B(lambd,eta,T1,T2) A_r = HW_A(lambd,eta,P0T,T1,T2) return np.exp(A_r + B_r *rT1) def HWMean_r(P0T,lambd,eta,T): # time-step needed for differentiation dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2.0*dt) # Initial interest rate is a forward rate at time t->0 r0 = f0T(0.00001) theta = HW_theta(lambd,eta,P0T) zGrid = np.linspace(0.0,T,2500) temp =lambda z: theta(z) * np.exp(-lambd*(T-z)) r_mean = r0*np.exp(-lambd*T) + lambd * integrate.trapz(temp(zGrid),zGrid) return r_mean def HW_r_0(P0T,lambd,eta): # time-step needed for differentiation dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) # Initial interest rate is a forward rate at time t->0 r0 = f0T(0.00001) return r0 def HW_Mu_FrwdMeasure(P0T,lambd,eta,T): # time-step needed for differentiation dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) # Initial interest rate is a forward rate at time t->0 r0 = f0T(0.00001) theta = HW_theta(lambd,eta,P0T) zGrid = np.linspace(0.0,T,500) theta_hat =lambda t,T: theta(t) + eta*eta / lambd *1.0/lambd * (np.exp(-lambd*(T-t))-1.0) temp =lambda z: theta_hat(z,T) * np.exp(-lambd*(T-z)) r_mean = r0*np.exp(-lambd*T) + lambd * integrate.trapz(temp(zGrid),zGrid) return r_mean def HWVar_r(lambd,eta,T): return eta*eta/(2.0*lambd) *( 1.0-np.exp(-2.0*lambd *T)) def HWDensity(P0T,lambd,eta,T): r_mean = HWMean_r(P0T,lambd,eta,T) r_var = HWVar_r(lambd,eta,T) return lambda x: st.norm.pdf(x,r_mean,np.sqrt(r_var)) def HW_CapletFloorletPrice(CP,N,K,lambd,eta,P0T,T1,T2): if CP == OptionType.CALL: N_new = N * (1.0+(T2-T1)*K) K_new = 1.0 + (T2-T1)*K caplet = N_new*HW_ZCB_CallPutPrice(OptionType.PUT,1.0/K_new,lambd,eta,P0T,T1,T2) value= caplet elif CP==OptionType.PUT: value = 0.0 # In the homework assignmnet you need to fill this part in return value def HW_ZCB_CallPutPrice(CP,K,lambd,eta,P0T,T1,T2): B_r = HW_B(lambd,eta,T1,T2) A_r = HW_A(lambd,eta,P0T,T1,T2) mu_r = HW_Mu_FrwdMeasure(P0T,lambd,eta,T1) v_r = np.sqrt(HWVar_r(lambd,eta,T1)) K_hat = K * np.exp(-A_r) a = (np.log(K_hat) - B_r*mu_r)/(B_r*v_r) d1 = a - B_r*v_r d2 = d1 + B_r*v_r term1 = np.exp(0.5* B_r*B_r*v_r*v_r + B_r*mu_r)*st.norm.cdf(d1) - K_hat * st.norm.cdf(d2) value =P0T(T1) * np.exp(A_r) * term1 if CP == OptionType.CALL: return value elif CP==OptionType.PUT: return value - P0T(T2) + K*P0T(T1) def mainCalculation(): CP= OptionType.CALL NoOfPaths = 20000 NoOfSteps = 1000 lambd = 0.02 eta = 0.02 # We define a ZCB curve (obtained from the market) P0T = lambda T: np.exp(-0.1*T)#np.exp(-0.03*T*T-0.1*T) r0 = HW_r_0(P0T,lambd,eta) # In this experiment we compare ZCB from the Market and Analytical expression N = 25 T_end = 50 Tgrid= np.linspace(0,T_end,N) Exact = np.zeros([N,1]) Proxy= np.zeros ([N,1]) for i,Ti in enumerate(Tgrid): Proxy[i] = HW_ZCB(lambd,eta,P0T,0.0,Ti,r0) Exact[i] = P0T(Ti) plt.figure(1) plt.grid() plt.plot(Tgrid,Exact,'-k') plt.plot(Tgrid,Proxy,'--r') plt.legend(["Analytcal ZCB","Monte Carlo ZCB"]) plt.title('P(0,T) from Monte Carlo vs. Analytical expression') # In this experiment we compare Monte Carlo results for T1 = 4.0 T2 = 8.0 paths= GeneratePathsHWEuler(NoOfPaths,NoOfSteps,T1 ,P0T, lambd, eta) r = paths["R"] timeGrid = paths["time"] dt = timeGrid[1]-timeGrid[0] # Here we compare the price of an option on a ZCB from Monte Carlo and Analytical expression M_t = np.zeros([NoOfPaths,NoOfSteps]) for i in range(0,NoOfPaths): M_t[i,:] = np.exp(np.cumsum(r[i,:-1])*dt) KVec = np.linspace(0.01,1.7,50) Price_MC_V = np.zeros([len(KVec),1]) Price_Th_V =np.zeros([len(KVec),1]) P_T1_T2 = HW_ZCB(lambd,eta,P0T,T1,T2,r[:,-1]) for i,K in enumerate(KVec): if CP==OptionType.CALL: Price_MC_V[i] =np.mean( 1.0/M_t[:,-1] * np.maximum(P_T1_T2-K,0.0)) elif CP==OptionType.PUT: Price_MC_V[i] =np.mean( 1.0/M_t[:,-1] * np.maximum(K-P_T1_T2,0.0)) Price_Th_V[i] =HW_ZCB_CallPutPrice(CP,K,lambd,eta,P0T,T1,T2)#HW_ZCB_CallPrice(K,lambd,eta,P0T,T1,T2) plt.figure(2) plt.grid() plt.plot(KVec,Price_MC_V) plt.plot(KVec,Price_Th_V,'--r') plt.legend(['Monte Carlo','Theoretical']) plt.title('Option on ZCB') # Effect of the HW model parameters on Implied Volatilities # define a forward rate between T1 and T2 frwd = 1.0/(T2-T1) *(P0T(T1)/P0T(T2)-1.0) K = np.linspace(frwd/2.0,3.0*frwd,25) Notional = 1.0 capletPrice = np.zeros(len(K)) for idx in range(0,len(K)): capletPrice[idx] = HW_CapletFloorletPrice(CP,Notional,K[idx],lambd,eta,P0T,T1,T2) plt.figure(3) plt.title('Caplet Price') plt.plot(K,capletPrice) plt.xlabel('strike') plt.ylabel('Caplet Price') plt.grid() mainCalculation() ================================================ FILE: Lecture 05-Interest Rate Products/Materials/HW_OptionsOnZCBs.py ================================================ #%% """ Created on July 12 2021 Options on ZCBs under the Hull-White model This code is purely educational and comes from "Financial Engineering" course by L.A. Grzelak The course is based on the book “Mathematical Modeling and Computation in Finance: With Exercises and Python and MATLAB Computer Codes”, by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing Europe Ltd, 2019. @author: Lech A. Grzelak """ import numpy as np import enum import matplotlib.pyplot as plt import scipy.stats as st import scipy.integrate as integrate from scipy import interpolate # This class defines puts and calls class OptionType(enum.Enum): CALL = 1.0 PUT = -1.0 def GeneratePathsHWEuler(NoOfPaths,NoOfSteps,T,P0T, lambd, eta): # time-step needed for differentiation dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) # Initial interest rate is a forward rate at time t->0 r0 = f0T(0.00001) theta = lambda t: 1.0/lambd * (f0T(t+dt)-f0T(t-dt))/(2.0*dt) + f0T(t) + eta*eta/(2.0*lambd*lambd)*(1.0-np.exp(-2.0*lambd*t)) #theta = lambda t: 0.1 +t -t #print("changed theta") Z = np.random.normal(0.0,1.0,[NoOfPaths,NoOfSteps]) W = np.zeros([NoOfPaths, NoOfSteps+1]) R = np.zeros([NoOfPaths, NoOfSteps+1]) R[:,0]=r0 time = np.zeros([NoOfSteps+1]) dt = T / float(NoOfSteps) for i in range(0,NoOfSteps): # making sure that samples from normal have mean 0 and variance 1 if NoOfPaths > 1: Z[:,i] = (Z[:,i] - np.mean(Z[:,i])) / np.std(Z[:,i]) W[:,i+1] = W[:,i] + np.power(dt, 0.5)*Z[:,i] R[:,i+1] = R[:,i] + lambd*(theta(time[i]) - R[:,i]) * dt + eta* (W[:,i+1]-W[:,i]) time[i+1] = time[i] +dt # Outputs paths = {"time":time,"R":R} return paths def HW_theta(lambd,eta,P0T): dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) theta = lambda t: 1.0/lambd * (f0T(t+dt)-f0T(t-dt))/(2.0*dt) + f0T(t) + eta*eta/(2.0*lambd*lambd)*(1.0-np.exp(-2.0*lambd*t)) #print("CHANGED THETA") return theta#lambda t: 0.1+t-t def HW_A(lambd,eta,P0T,T1,T2): tau = T2-T1 zGrid = np.linspace(0.0,tau,250) B_r = lambda tau: 1.0/lambd * (np.exp(-lambd *tau)-1.0) theta = HW_theta(lambd,eta,P0T) temp1 = lambd * integrate.trapz(theta(T2-zGrid)*B_r(zGrid),zGrid) temp2 = eta*eta/(4.0*np.power(lambd,3.0)) * (np.exp(-2.0*lambd*tau)*(4*np.exp(lambd*tau)-1.0) -3.0) + eta*eta*tau/(2.0*lambd*lambd) return temp1 + temp2 def HW_B(lambd,eta,T1,T2): return 1.0/lambd *(np.exp(-lambd*(T2-T1))-1.0) def HW_ZCB(lambd,eta,P0T,T1,T2,rT1): B_r = HW_B(lambd,eta,T1,T2) A_r = HW_A(lambd,eta,P0T,T1,T2) return np.exp(A_r + B_r *rT1) def HWMean_r(P0T,lambd,eta,T): # time-step needed for differentiation dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2.0*dt) # Initial interest rate is a forward rate at time t->0 r0 = f0T(0.00001) theta = HW_theta(lambd,eta,P0T) zGrid = np.linspace(0.0,T,2500) temp =lambda z: theta(z) * np.exp(-lambd*(T-z)) r_mean = r0*np.exp(-lambd*T) + lambd * integrate.trapz(temp(zGrid),zGrid) return r_mean def HW_r_0(P0T,lambd,eta): # time-step needed for differentiation dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) # Initial interest rate is a forward rate at time t->0 r0 = f0T(0.00001) return r0 def HW_Mu_FrwdMeasure(P0T,lambd,eta,T): # time-step needed for differentiation dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) # Initial interest rate is a forward rate at time t->0 r0 = f0T(0.00001) theta = HW_theta(lambd,eta,P0T) zGrid = np.linspace(0.0,T,500) theta_hat =lambda t,T: theta(t) + eta*eta / lambd *1.0/lambd * (np.exp(-lambd*(T-t))-1.0) temp =lambda z: theta_hat(z,T) * np.exp(-lambd*(T-z)) r_mean = r0*np.exp(-lambd*T) + lambd * integrate.trapz(temp(zGrid),zGrid) return r_mean def HWVar_r(lambd,eta,T): return eta*eta/(2.0*lambd) *( 1.0-np.exp(-2.0*lambd *T)) def HWDensity(P0T,lambd,eta,T): r_mean = HWMean_r(P0T,lambd,eta,T) r_var = HWVar_r(lambd,eta,T) return lambda x: st.norm.pdf(x,r_mean,np.sqrt(r_var)) def HW_ZCB_CallPutPrice(CP,K,lambd,eta,P0T,T1,T2): B_r = HW_B(lambd,eta,T1,T2) A_r = HW_A(lambd,eta,P0T,T1,T2) mu_r = HW_Mu_FrwdMeasure(P0T,lambd,eta,T1) v_r = np.sqrt(HWVar_r(lambd,eta,T1)) K_hat = K * np.exp(-A_r) a = (np.log(K_hat) - B_r*mu_r)/(B_r*v_r) d1 = a - B_r*v_r d2 = d1 + B_r*v_r term1 = np.exp(0.5* B_r*B_r*v_r*v_r + B_r*mu_r)*st.norm.cdf(d1) - K_hat * st.norm.cdf(d2) value =P0T(T1) * np.exp(A_r) * term1 if CP == OptionType.CALL: return value elif CP==OptionType.PUT: return value - P0T(T2) + K*P0T(T1) def mainCalculation(): CP= OptionType.CALL NoOfPaths = 20000 NoOfSteps = 1000 lambd = 0.02 eta = 0.02 # We define a ZCB curve (obtained from the market) # We define a ZCB curve (obtained from the market), 1-D points with a B-spline curve. ti = [0.0,0.00273972600000000,0.0876712330000000,0.172602740000000,0.257534247000000,0.342465753000000,0.427397260000000,0.512328767000000,0.597260274000000,0.682191781000000,0.767123288000000,0.852054795000000,0.936986301000000,1.02191780800000,1.10684931500000,1.19178082200000,1.27671232900000,1.36164383600000,1.44657534200000,1.53150684900000,1.61643835600000,1.70136986300000,1.78630137000000,1.87123287700000,1.95616438400000,2.04109589000000,2.12602739700000,2.21095890400000,2.29589041100000,2.38082191800000,2.46575342500000,2.55068493200000,2.63561643800000,2.72054794500000,2.80547945200000,2.89041095900000,2.97534246600000,3.06027397300000,3.14520547900000,3.23013698600000,3.31506849300000,3.40000000000000,3.48493150700000,3.56986301400000,3.65479452100000,3.73972602700000,3.82465753400000,3.90958904100000,3.99452054800000,4.07945205500000,4.16438356200000,4.24931506800000,4.33424657500000,4.41917808200000,4.50410958900000,4.58904109600000,4.67397260300000,4.75890411000000,4.84383561600000,4.92876712300000,5.01369863000000,5.09863013700000,5.18356164400000,5.26849315100000,5.35342465800000,5.43835616400000,5.52328767100000,5.60821917800000,5.69315068500000,5.77808219200000,5.86301369900000,5.94794520500000,6.03287671200000,6.11780821900000,6.20273972600000,6.28767123300000,6.37260274000000,6.45753424700000,6.54246575300000,6.62739726000000,6.71232876700000,6.79726027400000,6.88219178100000,6.96712328800000,7.05205479500000,7.13698630100000,7.22191780800000,7.30684931500000,7.39178082200000,7.47671232900000,7.56164383600000,7.64657534200000,7.73150684900000,7.81643835600000,7.90136986300000,7.98630137000000,8.07123287700000,8.15616438400000,8.24109589000000,8.32602739700000,8.41095890400000,8.49589041100000,8.58082191800000,8.66575342500000,8.75068493200000,8.83561643800000,8.92054794500000,9.00547945200000,9.09041095900000,9.17534246600000,9.26027397300000,9.34520547900000,9.43013698600000,9.51506849300000,9.60000000000000,9.68493150700000,9.76986301400000,9.85479452100000,9.93972602700000,10.0246575300000,10.1095890400000,10.1945205500000,10.2794520500000,10.3643835600000,10.4493150700000,10.5342465800000,10.6191780800000,10.7041095900000,10.7890411000000,10.8739726000000,10.9589041100000,11.0438356200000,11.1287671200000,11.2136986300000,11.2986301400000,11.3835616400000,11.4684931500000,11.5534246600000,11.6383561600000,11.7232876700000,11.8082191800000,11.8931506800000,11.9780821900000,12.0630137000000,12.1479452100000,12.2328767100000,12.3178082200000,12.4027397300000,12.4876712300000,12.5726027400000,12.6575342500000,12.7424657500000,12.8273972600000,12.9123287700000,12.9972602700000,13.0821917800000,13.1671232900000,13.2520547900000,13.3369863000000,13.4219178100000,13.5068493200000,13.5917808200000,13.6767123300000,13.7616438400000,13.8465753400000,13.9315068500000,14.0164383600000,14.1013698600000,14.1863013700000,14.2712328800000,14.3561643800000,14.4410958900000,14.5260274000000,14.6109589000000,14.6958904100000,14.7808219200000,14.8657534200000,14.9506849300000,15.0356164400000,15.1205479500000,15.2054794500000,15.2904109600000,15.3753424700000,15.4602739700000,15.5452054800000,15.6301369900000,15.7150684900000,15.8000000000000,15.8849315100000,15.9698630100000,16.0547945200000,16.1397260300000,16.2246575300000,16.3095890400000,16.3945205500000,16.4794520500000,16.5643835600000,16.6493150700000,16.7342465800000,16.8191780800000,16.9041095900000,16.9890411000000,17.0739726000000,17.1589041100000,17.2438356200000,17.3287671200000,17.4136986300000,17.4986301400000,17.5835616400000,17.6684931500000,17.7534246600000,17.8383561600000,17.9232876700000,18.0082191800000,18.0931506800000,18.1780821900000,18.2630137000000,18.3479452100000,18.4328767100000,18.5178082200000,18.6027397300000,18.6876712300000,18.7726027400000,18.8575342500000,18.9424657500000,19.0273972600000,19.1123287700000,19.1972602700000,19.2821917800000,19.3671232900000,19.4520547900000,19.5369863000000,19.6219178100000,19.7068493200000,19.7917808200000,19.8767123300000,19.9616438400000,20.0465753400000,20.1315068500000,20.2164383600000,20.3013698600000,20.3863013700000,20.4712328800000,20.5561643800000,20.6410958900000,20.7260274000000,20.8109589000000,20.8958904100000,20.9808219200000,21.0657534200000,21.1506849300000,21.2356164400000,21.3205479500000,21.4054794500000,21.4904109600000,21.5753424700000,21.6602739700000,21.7452054800000,21.8301369900000,21.9150684900000,22,22.0849315100000,22.1698630100000,22.2547945200000,22.3397260300000,22.4246575300000,22.5095890400000,22.5945205500000,22.6794520500000,22.7643835600000,22.8493150700000,22.9342465800000,23.0191780800000,23.1041095900000,23.1890411000000,23.2739726000000,23.3589041100000,23.4438356200000,23.5287671200000,23.6136986300000,23.6986301400000,23.7835616400000,23.8684931500000,23.9534246600000,24.0383561600000,24.1232876700000,24.2082191800000,24.2931506800000,24.3780821900000,24.4630137000000,24.5479452100000,24.6328767100000,24.7178082200000,24.8027397300000,24.8876712300000,24.9726027400000,25.0575342500000,25.1424657500000,25.2273972600000,25.3123287700000,25.3972602700000,25.4821917800000,25.5671232900000,25.6520547900000,25.7369863000000,25.8219178100000,25.9068493200000,25.9917808200000,26.0767123300000,26.1616438400000,26.2465753400000,26.3315068500000,26.4164383600000,26.5013698600000,26.5863013700000,26.6712328800000,26.7561643800000,26.8410958900000,26.9260274000000,27.0109589000000,27.0958904100000,27.1808219200000,27.2657534200000,27.3506849300000,27.4356164400000,27.5205479500000,27.6054794500000,27.6904109600000,27.7753424700000,27.8602739700000,27.9452054800000,28.0301369900000,28.1150684900000,28.2000000000000,28.2849315100000,28.3698630100000,28.4547945200000,28.5397260300000,28.6246575300000,28.7095890400000,28.7945205500000,28.8794520500000,28.9643835600000,29.0493150700000,29.1342465800000,29.2191780800000,29.3041095900000,29.3890411000000,29.4739726000000,29.5589041100000,29.6438356200000,29.7287671200000,29.8136986300000,29.8986301400000,29.9835616400000,30.0684931500000,30.1534246600000,30.2383561600000,30.3232876700000,30.4082191800000,30.4931506800000,30.5780821900000,30.6630137000000,30.7479452100000,30.8328767100000,30.9178082200000,31.0027397300000,31.0876712300000,31.1726027400000,31.2575342500000,31.3424657500000,31.4273972600000,31.5123287700000,31.5972602700000,31.6821917800000,31.7671232900000,31.8520547900000,31.9369863000000,32.0219178100000,32.1068493200000,32.1917808200000,32.2767123300000,32.3616438400000,32.4465753400000,32.5315068500000,32.6164383600000,32.7013698600000,32.7863013700000,32.8712328800000,32.9561643800000,33.0410958900000,33.1260274000000,33.2109589000000,33.2958904100000,33.3808219200000,33.4657534200000,33.5506849300000,33.6356164400000,33.7205479500000,33.8054794500000,33.8904109600000,33.9753424700000,34.0602739700000,34.1452054800000,34.2301369900000,34.3150684900000,34.4000000000000,34.4849315100000,34.5698630100000,34.6547945200000,34.7397260300000,34.8246575300000,34.9095890400000,34.9945205500000,35.0794520500000,35.1643835600000,35.2493150700000,35.3342465800000,35.4191780800000,35.5041095900000,35.5890411000000,35.6739726000000,35.7589041100000,35.8438356200000,35.9287671200000,36.0136986300000,36.0986301400000,36.1835616400000,36.2684931500000,36.3534246600000,36.4383561600000,36.5232876700000,36.6082191800000,36.6931506800000,36.7780821900000,36.8630137000000,36.9479452100000,37.0328767100000,37.1178082200000,37.2027397300000,37.2876712300000,37.3726027400000,37.4575342500000,37.5424657500000,37.6273972600000,37.7123287700000,37.7972602700000,37.8821917800000,37.9671232900000,38.0520547900000,38.1369863000000,38.2219178100000,38.3068493200000,38.3917808200000,38.4767123300000,38.5616438400000,38.6465753400000,38.7315068500000,38.8164383600000,38.9013698600000,38.9863013700000,39.0712328800000,39.1561643800000,39.2410958900000,39.3260274000000,39.4109589000000,39.4958904100000,39.5808219200000,39.6657534200000,39.7506849300000,39.8356164400000,39.9205479500000,40.0054794500000] Pi = [1.0,0.999966573000000,0.998930882000000,0.997824062000000,0.996511145000000,0.995199956000000,0.993821602000000,0.992277014000000,0.990734827000000,0.989164324000000,0.987428762000000,0.985704346000000,0.983946708000000,0.982068207000000,0.980193293000000,0.978281187000000,0.976255832000000,0.974234670000000,0.972174514000000,0.970028236000000,0.967886697000000,0.965693800000000,0.963440984000000,0.961193424000000,0.958903753000000,0.956575247000000,0.954252397000000,0.951842433000000,0.949228406000000,0.946482248000000,0.943632525000000,0.940707509000000,0.937735160000000,0.934743101000000,0.931758611000000,0.928808623000000,0.925919731000000,0.923110403000000,0.920338655000000,0.917589739000000,0.914858924000000,0.912141532000000,0.909432941000000,0.906728583000000,0.904023944000000,0.901314567000000,0.898596044000000,0.895864025000000,0.893114214000000,0.890346348000000,0.887569841000000,0.884786140000000,0.881996090000000,0.879200528000000,0.876400278000000,0.873596157000000,0.870788975000000,0.867979528000000,0.865168605000000,0.862356987000000,0.859545442000000,0.856732322000000,0.853915005000000,0.851094371000000,0.848271318000000,0.845446732000000,0.842621487000000,0.839796449000000,0.836972470000000,0.834150393000000,0.831331050000000,0.828515261000000,0.825703790000000,0.822893949000000,0.820084325000000,0.817275636000000,0.814468590000000,0.811663887000000,0.808862219000000,0.806064266000000,0.803270701000000,0.800482187000000,0.797699379000000,0.794922921000000,0.792153338000000,0.789389717000000,0.786631547000000,0.783878633000000,0.781130782000000,0.778387801000000,0.775649505000000,0.772915708000000,0.770186227000000,0.767460882000000,0.764739497000000,0.762021898000000,0.759308200000000,0.756599781000000,0.753896558000000,0.751198194000000,0.748504356000000,0.745814715000000,0.743128950000000,0.740446741000000,0.737767776000000,0.735091745000000,0.732418345000000,0.729747277000000,0.727078670000000,0.724413443000000,0.721751645000000,0.719093279000000,0.716438346000000,0.713786850000000,0.711138792000000,0.708494177000000,0.705853006000000,0.703215284000000,0.700581015000000,0.697950202000000,0.695320544000000,0.692690217000000,0.690059929000000,0.687430378000000,0.684802253000000,0.682176235000000,0.679552992000000,0.676933185000000,0.674317462000000,0.671706465000000,0.669100824000000,0.666501159000000,0.663908082000000,0.661322194000000,0.658744089000000,0.656174348000000,0.653613545000000,0.651062245000000,0.648521002000000,0.645990363000000,0.643470865000000,0.640963036000000,0.638467395000000,0.635983411000000,0.633503959000000,0.631028021000000,0.628556105000000,0.626088714000000,0.623626343000000,0.621169478000000,0.618718599000000,0.616274178000000,0.613836679000000,0.611406558000000,0.608984265000000,0.606570242000000,0.604164924000000,0.601768738000000,0.599382105000000,0.597005438000000,0.594639144000000,0.592283621000000,0.589939262000000,0.587606453000000,0.585285574000000,0.582976996000000,0.580681085000000,0.578398202000000,0.576128698000000,0.573872922000000,0.571631214000000,0.569403908000000,0.567191334000000,0.564993814000000,0.562811666000000,0.560645202000000,0.558494727000000,0.556360543000000,0.554242913000000,0.552138115000000,0.550043470000000,0.547959012000000,0.545884776000000,0.543820795000000,0.541767101000000,0.539723726000000,0.537690699000000,0.535668051000000,0.533655809000000,0.531654001000000,0.529662652000000,0.527681790000000,0.525711438000000,0.523751620000000,0.521802360000000,0.519863678000000,0.517935598000000,0.516018137000000,0.514111318000000,0.512215157000000,0.510329674000000,0.508454885000000,0.506590807000000,0.504737456000000,0.502894847000000,0.501062994000000,0.499241910000000,0.497431609000000,0.495632103000000,0.493843403000000,0.492065521000000,0.490298466000000,0.488542249000000,0.486796879000000,0.485062364000000,0.483338711000000,0.481625929000000,0.479924024000000,0.478233003000000,0.476552870000000,0.474883632000000,0.473225293000000,0.471577858000000,0.469941330000000,0.468315712000000,0.466701007000000,0.465097218000000,0.463504347000000,0.461922395000000,0.460351364000000,0.458791253000000,0.457242065000000,0.455703798000000,0.454176452000000,0.452660027000000,0.451154522000000,0.449659935000000,0.448176257000000,0.446703083000000,0.445240105000000,0.443787209000000,0.442344286000000,0.440911226000000,0.439487920000000,0.438074261000000,0.436670143000000,0.435275460000000,0.433890109000000,0.432513986000000,0.431146989000000,0.429789017000000,0.428439971000000,0.427099751000000,0.425768260000000,0.424445399000000,0.423131074000000,0.421825189000000,0.420527650000000,0.419238363000000,0.417957237000000,0.416684180000000,0.415419101000000,0.414161910000000,0.412912520000000,0.411670842000000,0.410436789000000,0.409210275000000,0.407991213000000,0.406779521000000,0.405575114000000,0.404377909000000,0.403187824000000,0.402004777000000,0.400828688000000,0.399659478000000,0.398497066000000,0.397341375000000,0.396192326000000,0.395049844000000,0.393913852000000,0.392784274000000,0.391661036000000,0.390544064000000,0.389433283000000,0.388328623000000,0.387230010000000,0.386137373000000,0.385050641000000,0.383969745000000,0.382894615000000,0.381825182000000,0.380761378000000,0.379703135000000,0.378650387000000,0.377603067000000,0.376561110000000,0.375524516000000,0.374495509000000,0.373475055000000,0.372462955000000,0.371459016000000,0.370463043000000,0.369474847000000,0.368494238000000,0.367521029000000,0.366555034000000,0.365596072000000,0.364643959000000,0.363698516000000,0.362759566000000,0.361826932000000,0.360900441000000,0.359979918000000,0.359065194000000,0.358156099000000,0.357252465000000,0.356354126000000,0.355460918000000,0.354572678000000,0.353689243000000,0.352810455000000,0.351936156000000,0.351066187000000,0.350200393000000,0.349338622000000,0.348480719000000,0.347626535000000,0.346775919000000,0.345928722000000,0.345084799000000,0.344244003000000,0.343406190000000,0.342571217000000,0.341738943000000,0.340909228000000,0.340081932000000,0.339256917000000,0.338434048000000,0.337613190000000,0.336794207000000,0.335976969000000,0.335161343000000,0.334347200000000,0.333534411000000,0.332722847000000,0.331912384000000,0.331102895000000,0.330294257000000,0.329486347000000,0.328679044000000,0.327872227000000,0.327065778000000,0.326259578000000,0.325453511000000,0.324647462000000,0.323841447000000,0.323037558000000,0.322236538000000,0.321438361000000,0.320642997000000,0.319850421000000,0.319060604000000,0.318273519000000,0.317489140000000,0.316707441000000,0.315928394000000,0.315151974000000,0.314378155000000,0.313606911000000,0.312838216000000,0.312072045000000,0.311308373000000,0.310547175000000,0.309788426000000,0.309032102000000,0.308278178000000,0.307526629000000,0.306777432000000,0.306030564000000,0.305285999000000,0.304543716000000,0.303803690000000,0.303065898000000,0.302330318000000,0.301596926000000,0.300865701000000,0.300136619000000,0.299409659000000,0.298684798000000,0.297962014000000,0.297241286000000,0.296522593000000,0.295805912000000,0.295091222000000,0.294378503000000,0.293667734000000,0.292958894000000,0.292251962000000,0.291546917000000,0.290843741000000,0.290142412000000,0.289442910000000,0.288745217000000,0.288049311000000,0.287355175000000,0.286662788000000,0.285972131000000,0.285283185000000,0.284595932000000,0.283910353000000,0.283226429000000,0.282544142000000,0.281863473000000,0.281184405000000,0.280506920000000,0.279830999000000,0.279156625000000,0.278483781000000,0.277812449000000,0.277142612000000,0.276474253000000,0.275807354000000,0.275141900000000,0.274477873000000,0.273815257000000,0.273154036000000,0.272494193000000,0.271835712000000,0.271178577000000,0.270522772000000,0.269868283000000,0.269215092000000,0.268563184000000,0.267912545000000,0.267263159000000,0.266615011000000,0.265968086000000,0.265322370000000,0.264677847000000,0.264034503000000,0.263392324000000,0.262751295000000,0.262111403000000,0.261472633000000,0.260834972000000,0.260198405000000,0.259562919000000,0.258928501000000,0.258295136000000,0.257662813000000,0.257031517000000,0.256401236000000,0.255771957000000,0.255143667000000,0.254516353000000,0.253890002000000,0.253264603000000,0.252640144000000,0.252016611000000,0.251393992000000,0.250772277000000,0.250151453000000,0.249531508000000,0.248912431000000,0.248294210000000,0.247676834000000,0.247060291000000,0.246444571000000,0.245829663000000,0.245215555000000,0.244602237000000,0.243989698000000,0.243377928000000] interpolator = interpolate.splrep(ti, Pi, s=0.0001) P0T = lambda T: interpolate.splev(T, interpolator, der=0) r0 = HW_r_0(P0T,lambd,eta) # In this experiment we compare ZCB from the Market and Analytical expression N = 25 T_end = 50 Tgrid= np.linspace(0,T_end,N) Exact = np.zeros([N,1]) Proxy= np.zeros ([N,1]) for i,Ti in enumerate(Tgrid): Proxy[i] = HW_ZCB(lambd,eta,P0T,0.0,Ti,r0) Exact[i] = P0T(Ti) plt.figure(1) plt.grid() plt.plot(Tgrid,Exact,'-k') plt.plot(Tgrid,Proxy,'--r') plt.legend(["Analytcal ZCB","Monte Carlo ZCB"]) plt.title('P(0,T) from Monte Carlo vs. Analytical expression') # In this experiment we compare Monte Carlo results for T1 = 4.0 T2 = 8.0 paths= GeneratePathsHWEuler(NoOfPaths,NoOfSteps,T1 ,P0T, lambd, eta) r = paths["R"] timeGrid = paths["time"] dt = timeGrid[1]-timeGrid[0] # Here we compare the price of an option on a ZCB from Monte Carlo and Analytical expression M_t = np.zeros([NoOfPaths,NoOfSteps]) for i in range(0,NoOfPaths): M_t[i,:] = np.exp(np.cumsum(r[i,:-1])*dt) KVec = np.linspace(0.01,1.7,50) Price_MC_V = np.zeros([len(KVec),1]) Price_Th_V =np.zeros([len(KVec),1]) P_T1_T2 = HW_ZCB(lambd,eta,P0T,T1,T2,r[:,-1]) for i,K in enumerate(KVec): if CP==OptionType.CALL: Price_MC_V[i] =np.mean( 1.0/M_t[:,-1] * np.maximum(P_T1_T2-K,0.0)) elif CP==OptionType.PUT: Price_MC_V[i] =np.mean( 1.0/M_t[:,-1] * np.maximum(K-P_T1_T2,0.0)) Price_Th_V[i] =HW_ZCB_CallPutPrice(CP,K,lambd,eta,P0T,T1,T2) plt.figure(2) plt.grid() plt.plot(KVec,Price_MC_V) plt.plot(KVec,Price_Th_V,'--r') plt.legend(['Monte Carlo','Theoretical']) plt.title('Call Option on ZCB') plt.xlabel('Strike') plt.ylabel('Option value') mainCalculation() ================================================ FILE: Lecture 05-Interest Rate Products/Materials/Swaps_HW.py ================================================ #%% """ Created on July 12 2021 Swaps computed from a yield curve and from the Hull-White Model This code is purely educational and comes from "Financial Engineering" course by L.A. Grzelak The course is based on the book “Mathematical Modeling and Computation in Finance: With Exercises and Python and MATLAB Computer Codes”, by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing Europe Ltd, 2019. @author: Lech A. Grzelak """ import numpy as np import enum import matplotlib.pyplot as plt import scipy.integrate as integrate from scipy import interpolate from scipy.optimize import newton # This class defines puts and calls class OptionTypeSwap(enum.Enum): RECEIVER = 1.0 PAYER = -1.0 def GeneratePathsHWEuler(NoOfPaths,NoOfSteps,T,P0T, lambd, eta): # time-step needed for differentiation dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) # Initial interest rate is a forward rate at time t->0 r0 = f0T(0.00001) theta = lambda t: 1.0/lambd * (f0T(t+dt)-f0T(t-dt))/(2.0*dt) + f0T(t) + eta*eta/(2.0*lambd*lambd)*(1.0-np.exp(-2.0*lambd*t)) #theta = lambda t: 0.1 +t -t #print("changed theta") Z = np.random.normal(0.0,1.0,[NoOfPaths,NoOfSteps]) W = np.zeros([NoOfPaths, NoOfSteps+1]) R = np.zeros([NoOfPaths, NoOfSteps+1]) R[:,0]=r0 time = np.zeros([NoOfSteps+1]) dt = T / float(NoOfSteps) for i in range(0,NoOfSteps): # making sure that samples from normal have mean 0 and variance 1 if NoOfPaths > 1: Z[:,i] = (Z[:,i] - np.mean(Z[:,i])) / np.std(Z[:,i]) W[:,i+1] = W[:,i] + np.power(dt, 0.5)*Z[:,i] R[:,i+1] = R[:,i] + lambd*(theta(time[i]) - R[:,i]) * dt + eta* (W[:,i+1]-W[:,i]) time[i+1] = time[i] +dt # Outputs paths = {"time":time,"R":R} return paths def HW_theta(lambd,eta,P0T): dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) theta = lambda t: 1.0/lambd * (f0T(t+dt)-f0T(t-dt))/(2.0*dt) + f0T(t) + eta*eta/(2.0*lambd*lambd)*(1.0-np.exp(-2.0*lambd*t)) #print("CHANGED THETA") return theta#lambda t: 0.1+t-t def HW_A(lambd,eta,P0T,T1,T2): tau = T2-T1 zGrid = np.linspace(0.0,tau,250) B_r = lambda tau: 1.0/lambd * (np.exp(-lambd *tau)-1.0) theta = HW_theta(lambd,eta,P0T) temp1 = lambd * integrate.trapz(theta(T2-zGrid)*B_r(zGrid),zGrid) temp2 = eta*eta/(4.0*np.power(lambd,3.0)) * (np.exp(-2.0*lambd*tau)*(4*np.exp(lambd*tau)-1.0) -3.0) + eta*eta*tau/(2.0*lambd*lambd) return temp1 + temp2 def HW_B(lambd,eta,T1,T2): return 1.0/lambd *(np.exp(-lambd*(T2-T1))-1.0) def HW_ZCB(lambd,eta,P0T,T1,T2,rT1): n = np.size(rT1) if T10 r0 = f0T(0.00001) return r0 def SwapPrice(CP,notional,K,t,Ti,Tm,n,P0T): # CP- payer of receiver # n- notional # K- strike # t- today's date # Ti- beginning of the swap # Tm- end of Swap # n- number of dates payments between Ti and Tm # r_t -interest rate at time t if n == 1: ti_grid =np.array([Ti,Tm]) else: ti_grid = np.linspace(Ti,Tm,n) tau = ti_grid[1]- ti_grid[0] # overwrite Ti if t>Ti prevTi = ti_grid[np.where(ti_grid 0: #prevTi != []: Ti = prevTi[-1] # Now we need to handle the case when some payments are already done ti_grid = ti_grid[np.where(ti_grid>t)] temp= 0.0 for (idx,ti) in enumerate(ti_grid): if ti>Ti: temp = temp + tau * P0T(ti) if CP==OptionTypeSwap.PAYER: swap = (P0T(Ti) - P0T(Tm)) - K * temp elif CP==OptionTypeSwap.RECEIVER: swap = K * temp- (P0T(Ti) - P0T(Tm)) return swap*notional def HW_SwapPrice(CP,notional,K,t,Ti,Tm,n,r_t,P0T,lambd,eta): # CP- payer of receiver # n- notional # K- strike # t- today's date # Ti- beginning of the swap # Tm- end of Swap # n- number of dates payments between Ti and Tm # r_t -interest rate at time t if n == 1: ti_grid =np.array([Ti,Tm]) else: ti_grid = np.linspace(Ti,Tm,n) tau = ti_grid[1]- ti_grid[0] # overwrite Ti if t>Ti prevTi = ti_grid[np.where(ti_grid 0: #prevTi != []: Ti = prevTi[-1] # Now we need to handle the case when some payments are already done ti_grid = ti_grid[np.where(ti_grid>t)] temp= np.zeros(np.size(r_t)); P_t_TiLambda = lambda Ti : HW_ZCB(lambd,eta,P0T,t,Ti,r_t) for (idx,ti) in enumerate(ti_grid): if ti>Ti: temp = temp + tau * P_t_TiLambda(ti) P_t_Ti = P_t_TiLambda(Ti) P_t_Tm = P_t_TiLambda(Tm) if CP==OptionTypeSwap.PAYER: swap = (P_t_Ti - P_t_Tm) - K * temp elif CP==OptionTypeSwap.RECEIVER: swap = K * temp- (P_t_Ti - P_t_Tm) return swap*notional def mainCalculation(): NoOfPaths = 2000 NoOfSteps = 1000 CP = OptionTypeSwap.PAYER lambd = 0.5 eta = 0.03 notional = 10000.0 # We define a ZCB curve (obtained from the market), 1-D points with a B-spline curve. ti = [0.0,0.00273972600000000,0.0876712330000000,0.172602740000000,0.257534247000000,0.342465753000000,0.427397260000000,0.512328767000000,0.597260274000000,0.682191781000000,0.767123288000000,0.852054795000000,0.936986301000000,1.02191780800000,1.10684931500000,1.19178082200000,1.27671232900000,1.36164383600000,1.44657534200000,1.53150684900000,1.61643835600000,1.70136986300000,1.78630137000000,1.87123287700000,1.95616438400000,2.04109589000000,2.12602739700000,2.21095890400000,2.29589041100000,2.38082191800000,2.46575342500000,2.55068493200000,2.63561643800000,2.72054794500000,2.80547945200000,2.89041095900000,2.97534246600000,3.06027397300000,3.14520547900000,3.23013698600000,3.31506849300000,3.40000000000000,3.48493150700000,3.56986301400000,3.65479452100000,3.73972602700000,3.82465753400000,3.90958904100000,3.99452054800000,4.07945205500000,4.16438356200000,4.24931506800000,4.33424657500000,4.41917808200000,4.50410958900000,4.58904109600000,4.67397260300000,4.75890411000000,4.84383561600000,4.92876712300000,5.01369863000000,5.09863013700000,5.18356164400000,5.26849315100000,5.35342465800000,5.43835616400000,5.52328767100000,5.60821917800000,5.69315068500000,5.77808219200000,5.86301369900000,5.94794520500000,6.03287671200000,6.11780821900000,6.20273972600000,6.28767123300000,6.37260274000000,6.45753424700000,6.54246575300000,6.62739726000000,6.71232876700000,6.79726027400000,6.88219178100000,6.96712328800000,7.05205479500000,7.13698630100000,7.22191780800000,7.30684931500000,7.39178082200000,7.47671232900000,7.56164383600000,7.64657534200000,7.73150684900000,7.81643835600000,7.90136986300000,7.98630137000000,8.07123287700000,8.15616438400000,8.24109589000000,8.32602739700000,8.41095890400000,8.49589041100000,8.58082191800000,8.66575342500000,8.75068493200000,8.83561643800000,8.92054794500000,9.00547945200000,9.09041095900000,9.17534246600000,9.26027397300000,9.34520547900000,9.43013698600000,9.51506849300000,9.60000000000000,9.68493150700000,9.76986301400000,9.85479452100000,9.93972602700000,10.0246575300000,10.1095890400000,10.1945205500000,10.2794520500000,10.3643835600000,10.4493150700000,10.5342465800000,10.6191780800000,10.7041095900000,10.7890411000000,10.8739726000000,10.9589041100000,11.0438356200000,11.1287671200000,11.2136986300000,11.2986301400000,11.3835616400000,11.4684931500000,11.5534246600000,11.6383561600000,11.7232876700000,11.8082191800000,11.8931506800000,11.9780821900000,12.0630137000000,12.1479452100000,12.2328767100000,12.3178082200000,12.4027397300000,12.4876712300000,12.5726027400000,12.6575342500000,12.7424657500000,12.8273972600000,12.9123287700000,12.9972602700000,13.0821917800000,13.1671232900000,13.2520547900000,13.3369863000000,13.4219178100000,13.5068493200000,13.5917808200000,13.6767123300000,13.7616438400000,13.8465753400000,13.9315068500000,14.0164383600000,14.1013698600000,14.1863013700000,14.2712328800000,14.3561643800000,14.4410958900000,14.5260274000000,14.6109589000000,14.6958904100000,14.7808219200000,14.8657534200000,14.9506849300000,15.0356164400000,15.1205479500000,15.2054794500000,15.2904109600000,15.3753424700000,15.4602739700000,15.5452054800000,15.6301369900000,15.7150684900000,15.8000000000000,15.8849315100000,15.9698630100000,16.0547945200000,16.1397260300000,16.2246575300000,16.3095890400000,16.3945205500000,16.4794520500000,16.5643835600000,16.6493150700000,16.7342465800000,16.8191780800000,16.9041095900000,16.9890411000000,17.0739726000000,17.1589041100000,17.2438356200000,17.3287671200000,17.4136986300000,17.4986301400000,17.5835616400000,17.6684931500000,17.7534246600000,17.8383561600000,17.9232876700000,18.0082191800000,18.0931506800000,18.1780821900000,18.2630137000000,18.3479452100000,18.4328767100000,18.5178082200000,18.6027397300000,18.6876712300000,18.7726027400000,18.8575342500000,18.9424657500000,19.0273972600000,19.1123287700000,19.1972602700000,19.2821917800000,19.3671232900000,19.4520547900000,19.5369863000000,19.6219178100000,19.7068493200000,19.7917808200000,19.8767123300000,19.9616438400000,20.0465753400000,20.1315068500000,20.2164383600000,20.3013698600000,20.3863013700000,20.4712328800000,20.5561643800000,20.6410958900000,20.7260274000000,20.8109589000000,20.8958904100000,20.9808219200000,21.0657534200000,21.1506849300000,21.2356164400000,21.3205479500000,21.4054794500000,21.4904109600000,21.5753424700000,21.6602739700000,21.7452054800000,21.8301369900000,21.9150684900000,22,22.0849315100000,22.1698630100000,22.2547945200000,22.3397260300000,22.4246575300000,22.5095890400000,22.5945205500000,22.6794520500000,22.7643835600000,22.8493150700000,22.9342465800000,23.0191780800000,23.1041095900000,23.1890411000000,23.2739726000000,23.3589041100000,23.4438356200000,23.5287671200000,23.6136986300000,23.6986301400000,23.7835616400000,23.8684931500000,23.9534246600000,24.0383561600000,24.1232876700000,24.2082191800000,24.2931506800000,24.3780821900000,24.4630137000000,24.5479452100000,24.6328767100000,24.7178082200000,24.8027397300000,24.8876712300000,24.9726027400000,25.0575342500000,25.1424657500000,25.2273972600000,25.3123287700000,25.3972602700000,25.4821917800000,25.5671232900000,25.6520547900000,25.7369863000000,25.8219178100000,25.9068493200000,25.9917808200000,26.0767123300000,26.1616438400000,26.2465753400000,26.3315068500000,26.4164383600000,26.5013698600000,26.5863013700000,26.6712328800000,26.7561643800000,26.8410958900000,26.9260274000000,27.0109589000000,27.0958904100000,27.1808219200000,27.2657534200000,27.3506849300000,27.4356164400000,27.5205479500000,27.6054794500000,27.6904109600000,27.7753424700000,27.8602739700000,27.9452054800000,28.0301369900000,28.1150684900000,28.2000000000000,28.2849315100000,28.3698630100000,28.4547945200000,28.5397260300000,28.6246575300000,28.7095890400000,28.7945205500000,28.8794520500000,28.9643835600000,29.0493150700000,29.1342465800000,29.2191780800000,29.3041095900000,29.3890411000000,29.4739726000000,29.5589041100000,29.6438356200000,29.7287671200000,29.8136986300000,29.8986301400000,29.9835616400000,30.0684931500000,30.1534246600000,30.2383561600000,30.3232876700000,30.4082191800000,30.4931506800000,30.5780821900000,30.6630137000000,30.7479452100000,30.8328767100000,30.9178082200000,31.0027397300000,31.0876712300000,31.1726027400000,31.2575342500000,31.3424657500000,31.4273972600000,31.5123287700000,31.5972602700000,31.6821917800000,31.7671232900000,31.8520547900000,31.9369863000000,32.0219178100000,32.1068493200000,32.1917808200000,32.2767123300000,32.3616438400000,32.4465753400000,32.5315068500000,32.6164383600000,32.7013698600000,32.7863013700000,32.8712328800000,32.9561643800000,33.0410958900000,33.1260274000000,33.2109589000000,33.2958904100000,33.3808219200000,33.4657534200000,33.5506849300000,33.6356164400000,33.7205479500000,33.8054794500000,33.8904109600000,33.9753424700000,34.0602739700000,34.1452054800000,34.2301369900000,34.3150684900000,34.4000000000000,34.4849315100000,34.5698630100000,34.6547945200000,34.7397260300000,34.8246575300000,34.9095890400000,34.9945205500000,35.0794520500000,35.1643835600000,35.2493150700000,35.3342465800000,35.4191780800000,35.5041095900000,35.5890411000000,35.6739726000000,35.7589041100000,35.8438356200000,35.9287671200000,36.0136986300000,36.0986301400000,36.1835616400000,36.2684931500000,36.3534246600000,36.4383561600000,36.5232876700000,36.6082191800000,36.6931506800000,36.7780821900000,36.8630137000000,36.9479452100000,37.0328767100000,37.1178082200000,37.2027397300000,37.2876712300000,37.3726027400000,37.4575342500000,37.5424657500000,37.6273972600000,37.7123287700000,37.7972602700000,37.8821917800000,37.9671232900000,38.0520547900000,38.1369863000000,38.2219178100000,38.3068493200000,38.3917808200000,38.4767123300000,38.5616438400000,38.6465753400000,38.7315068500000,38.8164383600000,38.9013698600000,38.9863013700000,39.0712328800000,39.1561643800000,39.2410958900000,39.3260274000000,39.4109589000000,39.4958904100000,39.5808219200000,39.6657534200000,39.7506849300000,39.8356164400000,39.9205479500000,40.0054794500000] Pi = [1.0,0.999966573000000,0.998930882000000,0.997824062000000,0.996511145000000,0.995199956000000,0.993821602000000,0.992277014000000,0.990734827000000,0.989164324000000,0.987428762000000,0.985704346000000,0.983946708000000,0.982068207000000,0.980193293000000,0.978281187000000,0.976255832000000,0.974234670000000,0.972174514000000,0.970028236000000,0.967886697000000,0.965693800000000,0.963440984000000,0.961193424000000,0.958903753000000,0.956575247000000,0.954252397000000,0.951842433000000,0.949228406000000,0.946482248000000,0.943632525000000,0.940707509000000,0.937735160000000,0.934743101000000,0.931758611000000,0.928808623000000,0.925919731000000,0.923110403000000,0.920338655000000,0.917589739000000,0.914858924000000,0.912141532000000,0.909432941000000,0.906728583000000,0.904023944000000,0.901314567000000,0.898596044000000,0.895864025000000,0.893114214000000,0.890346348000000,0.887569841000000,0.884786140000000,0.881996090000000,0.879200528000000,0.876400278000000,0.873596157000000,0.870788975000000,0.867979528000000,0.865168605000000,0.862356987000000,0.859545442000000,0.856732322000000,0.853915005000000,0.851094371000000,0.848271318000000,0.845446732000000,0.842621487000000,0.839796449000000,0.836972470000000,0.834150393000000,0.831331050000000,0.828515261000000,0.825703790000000,0.822893949000000,0.820084325000000,0.817275636000000,0.814468590000000,0.811663887000000,0.808862219000000,0.806064266000000,0.803270701000000,0.800482187000000,0.797699379000000,0.794922921000000,0.792153338000000,0.789389717000000,0.786631547000000,0.783878633000000,0.781130782000000,0.778387801000000,0.775649505000000,0.772915708000000,0.770186227000000,0.767460882000000,0.764739497000000,0.762021898000000,0.759308200000000,0.756599781000000,0.753896558000000,0.751198194000000,0.748504356000000,0.745814715000000,0.743128950000000,0.740446741000000,0.737767776000000,0.735091745000000,0.732418345000000,0.729747277000000,0.727078670000000,0.724413443000000,0.721751645000000,0.719093279000000,0.716438346000000,0.713786850000000,0.711138792000000,0.708494177000000,0.705853006000000,0.703215284000000,0.700581015000000,0.697950202000000,0.695320544000000,0.692690217000000,0.690059929000000,0.687430378000000,0.684802253000000,0.682176235000000,0.679552992000000,0.676933185000000,0.674317462000000,0.671706465000000,0.669100824000000,0.666501159000000,0.663908082000000,0.661322194000000,0.658744089000000,0.656174348000000,0.653613545000000,0.651062245000000,0.648521002000000,0.645990363000000,0.643470865000000,0.640963036000000,0.638467395000000,0.635983411000000,0.633503959000000,0.631028021000000,0.628556105000000,0.626088714000000,0.623626343000000,0.621169478000000,0.618718599000000,0.616274178000000,0.613836679000000,0.611406558000000,0.608984265000000,0.606570242000000,0.604164924000000,0.601768738000000,0.599382105000000,0.597005438000000,0.594639144000000,0.592283621000000,0.589939262000000,0.587606453000000,0.585285574000000,0.582976996000000,0.580681085000000,0.578398202000000,0.576128698000000,0.573872922000000,0.571631214000000,0.569403908000000,0.567191334000000,0.564993814000000,0.562811666000000,0.560645202000000,0.558494727000000,0.556360543000000,0.554242913000000,0.552138115000000,0.550043470000000,0.547959012000000,0.545884776000000,0.543820795000000,0.541767101000000,0.539723726000000,0.537690699000000,0.535668051000000,0.533655809000000,0.531654001000000,0.529662652000000,0.527681790000000,0.525711438000000,0.523751620000000,0.521802360000000,0.519863678000000,0.517935598000000,0.516018137000000,0.514111318000000,0.512215157000000,0.510329674000000,0.508454885000000,0.506590807000000,0.504737456000000,0.502894847000000,0.501062994000000,0.499241910000000,0.497431609000000,0.495632103000000,0.493843403000000,0.492065521000000,0.490298466000000,0.488542249000000,0.486796879000000,0.485062364000000,0.483338711000000,0.481625929000000,0.479924024000000,0.478233003000000,0.476552870000000,0.474883632000000,0.473225293000000,0.471577858000000,0.469941330000000,0.468315712000000,0.466701007000000,0.465097218000000,0.463504347000000,0.461922395000000,0.460351364000000,0.458791253000000,0.457242065000000,0.455703798000000,0.454176452000000,0.452660027000000,0.451154522000000,0.449659935000000,0.448176257000000,0.446703083000000,0.445240105000000,0.443787209000000,0.442344286000000,0.440911226000000,0.439487920000000,0.438074261000000,0.436670143000000,0.435275460000000,0.433890109000000,0.432513986000000,0.431146989000000,0.429789017000000,0.428439971000000,0.427099751000000,0.425768260000000,0.424445399000000,0.423131074000000,0.421825189000000,0.420527650000000,0.419238363000000,0.417957237000000,0.416684180000000,0.415419101000000,0.414161910000000,0.412912520000000,0.411670842000000,0.410436789000000,0.409210275000000,0.407991213000000,0.406779521000000,0.405575114000000,0.404377909000000,0.403187824000000,0.402004777000000,0.400828688000000,0.399659478000000,0.398497066000000,0.397341375000000,0.396192326000000,0.395049844000000,0.393913852000000,0.392784274000000,0.391661036000000,0.390544064000000,0.389433283000000,0.388328623000000,0.387230010000000,0.386137373000000,0.385050641000000,0.383969745000000,0.382894615000000,0.381825182000000,0.380761378000000,0.379703135000000,0.378650387000000,0.377603067000000,0.376561110000000,0.375524516000000,0.374495509000000,0.373475055000000,0.372462955000000,0.371459016000000,0.370463043000000,0.369474847000000,0.368494238000000,0.367521029000000,0.366555034000000,0.365596072000000,0.364643959000000,0.363698516000000,0.362759566000000,0.361826932000000,0.360900441000000,0.359979918000000,0.359065194000000,0.358156099000000,0.357252465000000,0.356354126000000,0.355460918000000,0.354572678000000,0.353689243000000,0.352810455000000,0.351936156000000,0.351066187000000,0.350200393000000,0.349338622000000,0.348480719000000,0.347626535000000,0.346775919000000,0.345928722000000,0.345084799000000,0.344244003000000,0.343406190000000,0.342571217000000,0.341738943000000,0.340909228000000,0.340081932000000,0.339256917000000,0.338434048000000,0.337613190000000,0.336794207000000,0.335976969000000,0.335161343000000,0.334347200000000,0.333534411000000,0.332722847000000,0.331912384000000,0.331102895000000,0.330294257000000,0.329486347000000,0.328679044000000,0.327872227000000,0.327065778000000,0.326259578000000,0.325453511000000,0.324647462000000,0.323841447000000,0.323037558000000,0.322236538000000,0.321438361000000,0.320642997000000,0.319850421000000,0.319060604000000,0.318273519000000,0.317489140000000,0.316707441000000,0.315928394000000,0.315151974000000,0.314378155000000,0.313606911000000,0.312838216000000,0.312072045000000,0.311308373000000,0.310547175000000,0.309788426000000,0.309032102000000,0.308278178000000,0.307526629000000,0.306777432000000,0.306030564000000,0.305285999000000,0.304543716000000,0.303803690000000,0.303065898000000,0.302330318000000,0.301596926000000,0.300865701000000,0.300136619000000,0.299409659000000,0.298684798000000,0.297962014000000,0.297241286000000,0.296522593000000,0.295805912000000,0.295091222000000,0.294378503000000,0.293667734000000,0.292958894000000,0.292251962000000,0.291546917000000,0.290843741000000,0.290142412000000,0.289442910000000,0.288745217000000,0.288049311000000,0.287355175000000,0.286662788000000,0.285972131000000,0.285283185000000,0.284595932000000,0.283910353000000,0.283226429000000,0.282544142000000,0.281863473000000,0.281184405000000,0.280506920000000,0.279830999000000,0.279156625000000,0.278483781000000,0.277812449000000,0.277142612000000,0.276474253000000,0.275807354000000,0.275141900000000,0.274477873000000,0.273815257000000,0.273154036000000,0.272494193000000,0.271835712000000,0.271178577000000,0.270522772000000,0.269868283000000,0.269215092000000,0.268563184000000,0.267912545000000,0.267263159000000,0.266615011000000,0.265968086000000,0.265322370000000,0.264677847000000,0.264034503000000,0.263392324000000,0.262751295000000,0.262111403000000,0.261472633000000,0.260834972000000,0.260198405000000,0.259562919000000,0.258928501000000,0.258295136000000,0.257662813000000,0.257031517000000,0.256401236000000,0.255771957000000,0.255143667000000,0.254516353000000,0.253890002000000,0.253264603000000,0.252640144000000,0.252016611000000,0.251393992000000,0.250772277000000,0.250151453000000,0.249531508000000,0.248912431000000,0.248294210000000,0.247676834000000,0.247060291000000,0.246444571000000,0.245829663000000,0.245215555000000,0.244602237000000,0.243989698000000,0.243377928000000] interpolator = interpolate.splrep(ti, Pi, s=0.0001) P0T = lambda T: interpolate.splev(T, interpolator, der=0) r0 = HW_r_0(P0T,lambd,eta) # In this experiment we compare ZCB from the Market and Analytical expression N = 25 T_end = 50 Tgrid= np.linspace(0,T_end,N) Exact = np.zeros([N,1]) Proxy= np.zeros ([N,1]) for i,Ti in enumerate(Tgrid): Proxy[i] = HW_ZCB(lambd,eta,P0T,0.0,Ti,r0) Exact[i] = P0T(Ti) plt.figure(1) plt.grid() plt.plot(Tgrid,Exact,'-k') plt.plot(Tgrid,Proxy,'--r') plt.legend(["Analytcal ZCB","Monte Carlo ZCB"]) plt.title('P(0,T) from Monte Carlo vs. Analytical expression') # Here we simulate the exposure profiles for a swap, using the HW model # Swap settings K = np.linspace(-0.1,0.1,25) # strike Ti = 1.0 # begining of the swap Tm = 10.0 # end date of the swap n = 10 # number of payments between Ti and Tm paths= GeneratePathsHWEuler(NoOfPaths, NoOfSteps,Tm + 1.0, P0T, lambd, eta) r = paths["R"] timeGrid = paths["time"] dt = timeGrid[1]-timeGrid[0] # Here we compare the price of an option on a ZCB from Monte Carlo and Analytical expression M_t = np.zeros([NoOfPaths,NoOfSteps]) for i in range(0,NoOfPaths): M_t[i,:] = np.exp(np.cumsum(r[i,0:-1])*dt) # Portfolio without netting VSwapHW = np.zeros(len(K)) VSwap = np.zeros(len(K)) t0= 0 for (idx,Ki) in enumerate(K): VHW = HW_SwapPrice(CP,notional,Ki,t0,Ti,Tm,n,r0,P0T,lambd,eta) V = SwapPrice(CP,notional,Ki,t0,Ti,Tm,n,P0T) VSwap[idx] = V VSwapHW[idx] = VHW[0] plt.figure(2) plt.plot(K,VSwap) plt.plot(K,VSwapHW,'--r') plt.grid() plt.xlabel('strike,K') plt.ylabel('Swap value') plt.title('Swap pricing') # Computation for Swap Par K=0.0 print('Swap price for K = 0 is {0}'.format(SwapPrice(CP,notional,K,t0,Ti,Tm,n,P0T))) # Determine a part swap func = lambda k: SwapPrice(CP,notional,k,t0,Ti,Tm,n,P0T) K_par = newton(func, 0.0) print('Swap price for K_par = {0} is {1}'.format(K_par,SwapPrice(CP,notional,K_par,t0,Ti,Tm,n,P0T))) mainCalculation() ================================================ FILE: Lecture 06-Construction of Yield Curve and Multi-Curve/Materials/MultiCurveBuild.py ================================================ #%% """ Created on June 27 2021 Construction of a multi- curve for a given set of swap instruments This code is purely educational and comes from "Financial Engineering" course by L.A. Grzelak The course is based on the book “Mathematical Modeling and Computation in Finance: With Exercises and Python and MATLAB Computer Codes”, by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing Europe Ltd, 2019. @author: Lech A. Grzelak """ import numpy as np import enum from copy import deepcopy import matplotlib.pyplot as plt from scipy.interpolate import splrep, splev, interp1d # This class defines puts and calls class OptionTypeSwap(enum.Enum): RECEIVER = 1.0 PAYER = -1.0 def IRSwap(CP,notional,K,t,Ti,Tm,n,P0T): # CP- payer of receiver # n- notional # K- strike # t- today's date # Ti- beginning of the swap # Tm- end of Swap # n- number of dates payments between Ti and Tm # r_t -interest rate at time t ti_grid = np.linspace(Ti,Tm,int(n)) tau = ti_grid[1]- ti_grid[0] temp= 0.0 for (idx,ti) in enumerate(ti_grid): if idx>0: temp = temp + tau * P0T(ti) P_t_Ti = P0T(Ti) P_t_Tm = P0T(Tm) if CP==OptionTypeSwap.PAYER: swap = (P_t_Ti - P_t_Tm) - K * temp elif CP==OptionTypeSwap.RECEIVER: swap = K * temp - (P_t_Ti - P_t_Tm) return swap * notional def IRSwapMultiCurve(CP,notional,K,t,Ti,Tm,n,P0T,P0TFrd): # CP- payer of receiver # n- notional # K- strike # t- today's date # Ti- beginning of the swap # Tm- end of Swap # n- number of dates payments between Ti and Tm # r_t -interest rate at time t ti_grid = np.linspace(Ti,Tm,int(n)) tau = ti_grid[1]- ti_grid[0] swap = 0.0 for (idx,ti) in enumerate(ti_grid): #L(t_0,t_{k-1},t_{k}) from the forward curve if idx>0: L_frwd = 1.0/tau * (P0TFrd(ti_grid[idx-1])-P0TFrd(ti_grid[idx])) / P0TFrd(ti_grid[idx]) swap = swap + tau * P0T(ti_grid[idx]) * (L_frwd - K) return swap * notional def P0TModel(t,ti,ri,method): rInterp = method(ti,ri) r = rInterp(t) return np.exp(-r*t) def YieldCurve(instruments, maturities, r0, method, tol): r0 = deepcopy(r0) ri = MultivariateNewtonRaphson(r0, maturities, instruments, method, tol=tol) return ri def MultivariateNewtonRaphson(ri, ti, instruments, method, tol): err = 10e10 idx = 0 while err > tol: idx = idx +1 values = EvaluateInstruments(ti,ri,instruments,method) J = Jacobian(ti,ri, instruments, method) J_inv = np.linalg.inv(J) err = - np.dot(J_inv, values) ri[0:] = ri[0:] + err err = np.linalg.norm(err) print('index in the loop is',idx,' Error is ', err) return ri def Jacobian(ti, ri, instruments, method): eps = 1e-05 swap_num = len(ti) J = np.zeros([swap_num, swap_num]) val = EvaluateInstruments(ti,ri,instruments,method) ri_up = deepcopy(ri) for j in range(0, len(ri)): ri_up[j] = ri[j] + eps val_up = EvaluateInstruments(ti,ri_up,instruments,method) ri_up[j] = ri[j] dv = (val_up - val) / eps J[:, j] = dv[:] return J def EvaluateInstruments(ti,ri,instruments,method): P0Ttemp = lambda t: P0TModel(t,ti,ri,method) val = np.zeros(len(instruments)) for i in range(0,len(instruments)): val[i] = instruments[i](P0Ttemp) return val def linear_interpolation(ti,ri): interpolator = lambda t: np.interp(t, ti, ri) return interpolator def spline_interpolate(ti,ri): interpolator = splrep(ti, ri, s=0.01) interp = lambda t: splev(t,interpolator) return interp def scipy_1d_interpolate(ti, ri): interpolator = lambda t: interp1d(ti, ri, kind='quadratic')(t) return interpolator #def ComputeDelta(instrument,) def mainCode(): # Convergence tolerance tol = 1.0e-15 # Initial guess for the spine points r0 = np.array([0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01]) # Interpolation method method = linear_interpolation # Construct swaps that are used for building of the yield curve K = np.array([0.04/100.0, 0.16/100.0, 0.31/100.0, 0.81/100.0, 1.28/100.0, 1.62/100.0, 2.22/100.0, 2.30/100.0]) mat = np.array([1.0,2.0,3.0,5.0,7.0,10.0,20.0,30.0]) # IRSwap(CP, notional,K, t, Ti,Tm, n,P0T) swap1 = lambda P0T: IRSwap(OptionTypeSwap.PAYER,1,K[0],0.0,0.0,mat[0],4*mat[0],P0T) swap2 = lambda P0T: IRSwap(OptionTypeSwap.PAYER,1,K[1],0.0,0.0,mat[1],5*mat[1],P0T) swap3 = lambda P0T: IRSwap(OptionTypeSwap.PAYER,1,K[2],0.0,0.0,mat[2],6*mat[2],P0T) swap4 = lambda P0T: IRSwap(OptionTypeSwap.PAYER,1,K[3],0.0,0.0,mat[3],7*mat[3],P0T) swap5 = lambda P0T: IRSwap(OptionTypeSwap.PAYER,1,K[4],0.0,0.0,mat[4],8*mat[4],P0T) swap6 = lambda P0T: IRSwap(OptionTypeSwap.PAYER,1,K[5],0.0,0.0,mat[5],9*mat[5],P0T) swap7 = lambda P0T: IRSwap(OptionTypeSwap.PAYER,1,K[6],0.0,0.0,mat[6],10*mat[6],P0T) swap8 = lambda P0T: IRSwap(OptionTypeSwap.PAYER,1,K[7],0.0,0.0,mat[7],11*mat[7],P0T) instruments = [swap1,swap2,swap3,swap4,swap5,swap6,swap7,swap8] # determine optimal spine points ri = YieldCurve(instruments, mat, r0, method, tol) print('\n Spine points are',ri,'\n') # Build a ZCB-curve/yield curve from the spine points P0T_Initial = lambda t: P0TModel(t,mat,r0,method) P0T = lambda t: P0TModel(t,mat,ri,method) # price back the swaps swapsModel = np.zeros(len(instruments)) swapsInitial = np.zeros(len(instruments)) for i in range(0,len(instruments)): swapsModel[i] = instruments[i](P0T) swapsInitial[i] = instruments[i](P0T_Initial) print('Prices for Pas Swaps (initial) = ',swapsInitial,'\n') print('Prices for Par Swaps = ',swapsModel,'\n') # Multi Curve extension- simple sanity check P0TFrd = deepcopy(P0T) Ktest = 0.2 swap1 = lambda P0T: IRSwap(OptionTypeSwap.PAYER,1,Ktest,0.0,0.0,mat[0],4*mat[0],P0T) swap1MC = lambda P0T: IRSwapMultiCurve(OptionTypeSwap.PAYER,1,Ktest,0.0,0.0,mat[0],4*mat[0],P0T,P0TFrd) print('Sanity check: swap1 = {0}, swap2 = {1}'.format(swap1(P0T),swap1MC(P0T))) # Forward curve settings r0Frwd = np.array([0.01, 0.01, 0.01, 0.01]) KFrwd = np.array([0.09/100.0, 0.26/100.0, 0.37/100.0, 1.91/100.0]) matFrwd = np.array([1.0, 2.0, 3.0, 5.0]) # At this point we already know P(0,T) for the discount curve P0TDiscount = lambda t: P0TModel(t,mat,ri,method) swap1Frwd = lambda P0TFrwd: IRSwapMultiCurve(OptionTypeSwap.PAYER,1,KFrwd[0],0.0,0.0,matFrwd[0],4*matFrwd[0],P0TDiscount,P0TFrwd) swap2Frwd = lambda P0TFrwd: IRSwapMultiCurve(OptionTypeSwap.PAYER,1,KFrwd[1],0.0,0.0,matFrwd[1],5*matFrwd[1],P0TDiscount,P0TFrwd) swap3Frwd = lambda P0TFrwd: IRSwapMultiCurve(OptionTypeSwap.PAYER,1,KFrwd[2],0.0,0.0,matFrwd[2],6*matFrwd[2],P0TDiscount,P0TFrwd) swap4Frwd = lambda P0TFrwd: IRSwapMultiCurve(OptionTypeSwap.PAYER,1,KFrwd[3],0.0,0.0,matFrwd[3],7*matFrwd[3],P0TDiscount,P0TFrwd) instrumentsFrwd = [swap1Frwd,swap2Frwd,swap3Frwd,swap4Frwd] # determine optimal spine points for the forward curve riFrwd = YieldCurve(instrumentsFrwd, matFrwd, r0Frwd, method, tol) print('\n Frwd Spine points are',riFrwd,'\n') # Build a ZCB-curve/yield curve from the spine points P0TFrwd_Initial = lambda t: P0TModel(t,matFrwd,r0Frwd,method) P0TFrwd = lambda t: P0TModel(t,matFrwd,riFrwd,method) # price back the swaps swapsModelFrwd = np.zeros(len(instrumentsFrwd)) swapsInitialFrwd = np.zeros(len(instrumentsFrwd)) for i in range(0,len(instrumentsFrwd)): swapsModelFrwd[i] = instrumentsFrwd[i](P0TFrwd) swapsInitialFrwd[i] = instrumentsFrwd[i](P0TFrwd_Initial) print('Prices for Pas Swaps (initial) = ',swapsInitialFrwd,'\n') print('Prices for Par Swaps = ',swapsModelFrwd,'\n') print(swap1Frwd(P0TFrwd)) t = np.linspace(0,10,100) plt.figure() plt.plot(t,P0TDiscount(t),'--r') plt.plot(t,P0TFrwd(t),'-b') plt.legend(['discount','forecast']) return 0.0 mainCode() ================================================ FILE: Lecture 06-Construction of Yield Curve and Multi-Curve/Materials/YieldCurveBuildGreeks.py ================================================ #%% """ Created on June 27 2021 Construction of a yield curve for a given set of swap instruments This code is purely educational and comes from "Financial Engineering" course by L.A. Grzelak The course is based on the book “Mathematical Modeling and Computation in Finance: With Exercises and Python and MATLAB Computer Codes”, by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing Europe Ltd, 2019. @author: Lech A. Grzelak """ import numpy as np import enum from copy import deepcopy from scipy.interpolate import splrep, splev, interp1d # This class defines puts and calls class OptionTypeSwap(enum.Enum): RECEIVER = 1.0 PAYER = -1.0 def IRSwap(CP,notional,K,t,Ti,Tm,n,P0T): # CP- payer of receiver # n- notional # K- strike # t- today's date # Ti- beginning of the swap # Tm- end of Swap # n- number of dates payments between Ti and Tm # r_t -interest rate at time t ti_grid = np.linspace(Ti,Tm,int(n)) tau = ti_grid[1]- ti_grid[0] # overwrite Ti if t>Ti prevTi = ti_grid[np.where(ti_grid 0: Ti = prevTi[-1] # Now we need to handle the case when some payments are already done ti_grid = ti_grid[np.where(ti_grid>t)] temp= 0.0 for (idx,ti) in enumerate(ti_grid): if ti>Ti: temp = temp + tau * P0T(ti) P_t_Ti = P0T(Ti) P_t_Tm = P0T(Tm) if CP==OptionTypeSwap.PAYER: swap = (P_t_Ti - P_t_Tm) - K * temp elif CP==OptionTypeSwap.RECEIVER: swap = K * temp - (P_t_Ti - P_t_Tm) return swap * notional def P0TModel(t,ti,ri,method): rInterp = method(ti,ri) if t>=ti[-1]: r = ri[-1] if t<=ti[0]: r= ri[0] if t>ti[0] and t tol: idx = idx +1 values = EvaluateInstruments(ti,ri,instruments,method) J = Jacobian(ti,ri, instruments, method) J_inv = np.linalg.inv(J) err = - np.dot(J_inv, values) ri[0:] = ri[0:] + err err = np.linalg.norm(err) print('index in the loop is',idx,' Error is ', err) return ri def Jacobian(ti, ri, instruments, method): eps = 1e-05 swap_num = len(ti) J = np.zeros([swap_num, swap_num]) val = EvaluateInstruments(ti,ri,instruments,method) ri_up = deepcopy(ri) for j in range(0, len(ri)): ri_up[j] = ri[j] + eps val_up = EvaluateInstruments(ti,ri_up,instruments,method) ri_up[j] = ri[j] dv = (val_up - val) / eps J[:, j] = dv[:] return J def EvaluateInstruments(ti,ri,instruments,method): P0Ttemp = lambda t: P0TModel(t,ti,ri,method) val = np.zeros(len(instruments)) for i in range(0,len(instruments)): val[i] = instruments[i](P0Ttemp) return val def linear_interpolation(ti,ri): interpolator = lambda t: np.interp(t, ti, ri) return interpolator def quadratic_interpolation(ti, ri): interpolator = interp1d(ti, ri, kind='quadratic') return interpolator def cubic_interpolation(ti, ri): interpolator = interp1d(ti, ri, kind='cubic') return interpolator #def ComputeDelta(instrument,) def BuildInstruments(K,mat): swap1 = lambda P0T: IRSwap(OptionTypeSwap.PAYER,1,K[0],0.0,0.0,mat[0],4*mat[0],P0T) swap2 = lambda P0T: IRSwap(OptionTypeSwap.PAYER,1,K[1],0.0,0.0,mat[1],4*mat[1],P0T) swap3 = lambda P0T: IRSwap(OptionTypeSwap.RECEIVER,1,K[2],0.0,0.0,mat[2],4*mat[2],P0T) swap4 = lambda P0T: IRSwap(OptionTypeSwap.PAYER,1,K[3],0.0,0.0,mat[3],4*mat[3],P0T) swap5 = lambda P0T: IRSwap(OptionTypeSwap.PAYER,1,K[4],0.0,0.0,mat[4],4*mat[4],P0T) swap6 = lambda P0T: IRSwap(OptionTypeSwap.RECEIVER,1,K[5],0.0,0.0,mat[5],4*mat[5],P0T) swap7 = lambda P0T: IRSwap(OptionTypeSwap.PAYER,1,K[6],0.0,0.0,mat[6],4*mat[6],P0T) swap8 = lambda P0T: IRSwap(OptionTypeSwap.PAYER,1,K[7],0.0,0.0,mat[7],4*mat[7],P0T) instruments = [swap1,swap2,swap3,swap4,swap5,swap6,swap7,swap8] return instruments def mainCode(): # Convergence tolerance tol = 1.0e-8 # Initial guess for the spine points r0 = np.array([0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01]) # Interpolation method method = cubic_interpolation # Construct swaps that are used for building of the yield curve K = np.array([0.02, 0.03, 0.04, 0.05, 0.06, 0.07,0.08,0.09]) mat = np.array([1.0,2.0,3.0,5.0,7.0,10.0,20.0,30.0]) instruments = BuildInstruments(K,mat) ri = YieldCurve(instruments, mat, r0, method, tol) P0T = lambda t: P0TModel(t,mat,ri,method) # Define off market Swap SwapLambda = lambda P0T: IRSwap(OptionTypeSwap.PAYER,1,0.03,0.0,0.0,4,6*mat[0],P0T) swap= SwapLambda(P0T) print('Swap price = ',swap) dK = 0.0001 delta = np.zeros(len(K)) K_new = K for i in range(0,len(K)): K_new[i] = K_new[i] + dK instruments = BuildInstruments(K_new,mat) ri = YieldCurve(instruments, mat, r0, method, tol) P0T_new = lambda t: P0TModel(t,mat,ri,method) swap_shock = SwapLambda(P0T_new) delta[i] = (swap_shock-swap)/dK K_new[i] = K_new[i] - dK print(delta) return 0.0 mainCode() ================================================ FILE: Lecture 06-Construction of Yield Curve and Multi-Curve/Materials/YieldCurveBuild_Treasury.py ================================================ #%% """ Created on June 27 2021 Construction of a yield curve for a given set of swap instruments This code is purely educational and comes from "Financial Engineering" course by L.A. Grzelak The course is based on the book “Mathematical Modeling and Computation in Finance: With Exercises and Python and MATLAB Computer Codes”, by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing Europe Ltd, 2019. @author: Lech A. Grzelak """ import numpy as np import enum from copy import deepcopy from scipy.interpolate import splrep, splev, interp1d # This class defines puts and calls class OptionTypeSwap(enum.Enum): RECEIVER = 1.0 PAYER = -1.0 def IRSwap(CP,notional,K,t,Ti,Tm,n,P0T): # CP- payer of receiver # n- notional # K- strike # t- today's date # Ti- beginning of the swap # Tm- end of Swap # n- number of dates payments between Ti and Tm # r_t -interest rate at time t ti_grid = np.linspace(Ti,Tm,int(n)) tau = ti_grid[1]- ti_grid[0] # overwrite Ti if t>Ti prevTi = ti_grid[np.where(ti_grid 0: #prevTi != []: Ti = prevTi[-1] # Now we need to handle the case when some payments are already done ti_grid = ti_grid[np.where(ti_grid>t)] temp= 0.0 for (idx,ti) in enumerate(ti_grid): if ti>Ti: temp = temp + tau * P0T(ti) P_t_Ti = P0T(Ti) P_t_Tm = P0T(Tm) if CP==OptionTypeSwap.PAYER: swap = (P_t_Ti - P_t_Tm) - K * temp elif CP==OptionTypeSwap.RECEIVER: swap = K * temp - (P_t_Ti - P_t_Tm) return swap * notional def P0TModel(t,ti,ri,method): rInterp = method(ti,ri) r = rInterp(t) return np.exp(-r*t) def YieldCurve(instruments, maturities, r0, method, tol): r0 = deepcopy(r0) ri = MultivariateNewtonRaphson(r0, maturities, instruments, method, tol=tol) return ri def MultivariateNewtonRaphson(ri, ti, instruments, method, tol): err = 10e10 idx = 0 while err > tol: idx = idx +1 values = EvaluateInstruments(ti,ri,instruments,method) J = Jacobian(ti,ri, instruments, method) J_inv = np.linalg.inv(J) err = - np.dot(J_inv, values) ri[0:] = ri[0:] + err err = np.linalg.norm(err) print('index in the loop is',idx,' Error is ', err) return ri def Jacobian(ti, ri, instruments, method): eps = 1e-05 swap_num = len(ti) J = np.zeros([swap_num, swap_num]) val = EvaluateInstruments(ti,ri,instruments,method) ri_up = deepcopy(ri) for j in range(0, len(ri)): ri_up[j] = ri[j] + eps val_up = EvaluateInstruments(ti,ri_up,instruments,method) ri_up[j] = ri[j] dv = (val_up - val) / eps J[:, j] = dv[:] return J def EvaluateInstruments(ti,ri,instruments,method): P0Ttemp = lambda t: P0TModel(t,ti,ri,method) val = np.zeros(len(instruments)) for i in range(0,len(instruments)): val[i] = instruments[i](P0Ttemp) return val def linear_interpolation(ti,ri): interpolator = lambda t: np.interp(t, ti, ri) return interpolator def mainCode(): # Convergence tolerance tol = 1.0e-15 # Initial guess for the spine points r0 = np.array([0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01]) # Interpolation method method = linear_interpolation # Construct swaps that are used for building of the yield curve K = np.array([0.04/100.0, 0.16/100.0, 0.31/100.0, 0.81/100.0, 1.28/100.0, 1.62/100.0, 2.22/100.0, 2.30/100.0]) mat = np.array([1.0,2.0,3.0,5.0,7.0,10.0,20.0,30.0]) swap1 = lambda P0T: IRSwap(OptionTypeSwap.PAYER,1,K[0],0.0,0.0,mat[0],4*mat[0],P0T) swap2 = lambda P0T: IRSwap(OptionTypeSwap.PAYER,1,K[1],0.0,0.0,mat[1],4*mat[1],P0T) swap3 = lambda P0T: IRSwap(OptionTypeSwap.PAYER,1,K[2],0.0,0.0,mat[2],4*mat[2],P0T) swap4 = lambda P0T: IRSwap(OptionTypeSwap.PAYER,1,K[3],0.0,0.0,mat[3],4*mat[3],P0T) swap5 = lambda P0T: IRSwap(OptionTypeSwap.PAYER,1,K[4],0.0,0.0,mat[4],4*mat[4],P0T) swap6 = lambda P0T: IRSwap(OptionTypeSwap.PAYER,1,K[5],0.0,0.0,mat[5],4*mat[5],P0T) swap7 = lambda P0T: IRSwap(OptionTypeSwap.PAYER,1,K[6],0.0,0.0,mat[6],4*mat[6],P0T) swap8 = lambda P0T: IRSwap(OptionTypeSwap.PAYER,1,K[7],0.0,0.0,mat[7],4*mat[7],P0T) instruments = [swap1,swap2,swap3,swap4,swap5,swap6,swap7,swap8] # determine optimal spine points ri = YieldCurve(instruments, mat, r0, method, tol) print('\n Spine points are',ri,'\n') # Build a ZCB-curve/yield curve from the spine points P0T_Initial = lambda t: P0TModel(t,mat,r0,method) P0T = lambda t: P0TModel(t,mat,ri,method) # price back the swaps swapsModel = np.zeros(len(instruments)) swapsInitial = np.zeros(len(instruments)) for i in range(0,len(instruments)): swapsInitial[i] = instruments[i](P0T_Initial) swapsModel[i] = instruments[i](P0T) print('Prices for Pas Swaps (initial) = ',swapsInitial,'\n') print('Prices for Par Swaps = ',swapsModel,'\n') return 0.0 mainCode() ================================================ FILE: Lecture 07-Pricing of Swaptions and Negative Interest Rates/Materials/HW_CapletsAndFloorlets.py ================================================ #%% """ Created on July 12 2021 Caplets and floorlets under the Hull-White Model This code is purely educational and comes from "Financial Engineering" course by L.A. Grzelak The course is based on the book “Mathematical Modeling and Computation in Finance: With Exercises and Python and MATLAB Computer Codes”, by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing Europe Ltd, 2019. @author: Lech A. Grzelak """ import numpy as np import enum import matplotlib.pyplot as plt import scipy.stats as st import scipy.integrate as integrate import scipy.optimize as optimize # This class defines puts and calls class OptionType(enum.Enum): CALL = 1.0 PUT = -1.0 def GeneratePathsHWEuler(NoOfPaths,NoOfSteps,T,P0T, lambd, eta): # time-step needed for differentiation dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) # Initial interest rate is a forward rate at time t->0 r0 = f0T(0.00001) theta = lambda t: 1.0/lambd * (f0T(t+dt)-f0T(t-dt))/(2.0*dt) + f0T(t) + eta*eta/(2.0*lambd*lambd)*(1.0-np.exp(-2.0*lambd*t)) #theta = lambda t: 0.1 +t -t #print("changed theta") Z = np.random.normal(0.0,1.0,[NoOfPaths,NoOfSteps]) W = np.zeros([NoOfPaths, NoOfSteps+1]) R = np.zeros([NoOfPaths, NoOfSteps+1]) R[:,0]=r0 time = np.zeros([NoOfSteps+1]) dt = T / float(NoOfSteps) for i in range(0,NoOfSteps): # making sure that samples from normal have mean 0 and variance 1 if NoOfPaths > 1: Z[:,i] = (Z[:,i] - np.mean(Z[:,i])) / np.std(Z[:,i]) W[:,i+1] = W[:,i] + np.power(dt, 0.5)*Z[:,i] R[:,i+1] = R[:,i] + lambd*(theta(time[i]) - R[:,i]) * dt + eta* (W[:,i+1]-W[:,i]) time[i+1] = time[i] +dt # Outputs paths = {"time":time,"R":R} return paths def HW_theta(lambd,eta,P0T): dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) theta = lambda t: 1.0/lambd * (f0T(t+dt)-f0T(t-dt))/(2.0*dt) + f0T(t) + eta*eta/(2.0*lambd*lambd)*(1.0-np.exp(-2.0*lambd*t)) #print("CHANGED THETA") return theta#lambda t: 0.1+t-t def HW_A(lambd,eta,P0T,T1,T2): tau = T2-T1 zGrid = np.linspace(0.0,tau,250) B_r = lambda tau: 1.0/lambd * (np.exp(-lambd *tau)-1.0) theta = HW_theta(lambd,eta,P0T) temp1 = lambd * integrate.trapz(theta(T2-zGrid)*B_r(zGrid),zGrid) temp2 = eta*eta/(4.0*np.power(lambd,3.0)) * (np.exp(-2.0*lambd*tau)*(4*np.exp(lambd*tau)-1.0) -3.0) + eta*eta*tau/(2.0*lambd*lambd) return temp1 + temp2 def HW_B(lambd,eta,T1,T2): return 1.0/lambd *(np.exp(-lambd*(T2-T1))-1.0) def HW_ZCB(lambd,eta,P0T,T1,T2,rT1): B_r = HW_B(lambd,eta,T1,T2) A_r = HW_A(lambd,eta,P0T,T1,T2) return np.exp(A_r + B_r *rT1) def HWMean_r(P0T,lambd,eta,T): # time-step needed for differentiation dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2.0*dt) # Initial interest rate is a forward rate at time t->0 r0 = f0T(0.00001) theta = HW_theta(lambd,eta,P0T) zGrid = np.linspace(0.0,T,2500) temp =lambda z: theta(z) * np.exp(-lambd*(T-z)) r_mean = r0*np.exp(-lambd*T) + lambd * integrate.trapz(temp(zGrid),zGrid) return r_mean def HW_r_0(P0T,lambd,eta): # time-step needed for differentiation dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) # Initial interest rate is a forward rate at time t->0 r0 = f0T(0.00001) return r0 def HW_Mu_FrwdMeasure(P0T,lambd,eta,T): # time-step needed for differentiation dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) # Initial interest rate is a forward rate at time t->0 r0 = f0T(0.00001) theta = HW_theta(lambd,eta,P0T) zGrid = np.linspace(0.0,T,500) theta_hat =lambda t,T: theta(t) + eta*eta / lambd *1.0/lambd * (np.exp(-lambd*(T-t))-1.0) temp =lambda z: theta_hat(z,T) * np.exp(-lambd*(T-z)) r_mean = r0*np.exp(-lambd*T) + lambd * integrate.trapz(temp(zGrid),zGrid) return r_mean def HWVar_r(lambd,eta,T): return eta*eta/(2.0*lambd) *( 1.0-np.exp(-2.0*lambd *T)) def HWDensity(P0T,lambd,eta,T): r_mean = HWMean_r(P0T,lambd,eta,T) r_var = HWVar_r(lambd,eta,T) return lambda x: st.norm.pdf(x,r_mean,np.sqrt(r_var)) def HW_CapletFloorletPrice(CP,N,K,lambd,eta,P0T,T1,T2): if CP == OptionType.CALL: N_new = N * (1.0+(T2-T1)*K) K_new = 1.0 + (T2-T1)*K caplet = N_new*HW_ZCB_CallPutPrice(OptionType.PUT,1.0/K_new,lambd,eta,P0T,T1,T2) value= caplet elif CP==OptionType.PUT: N_new = N * (1.0+ (T2-T1)*K) K_new = 1.0 + (T2-T1)*K floorlet = N_new*HW_ZCB_CallPutPrice(OptionType.CALL,1.0/K_new,lambd,eta,P0T,T1,T2) value = floorlet return value def HW_ZCB_CallPutPrice(CP,K,lambd,eta,P0T,T1,T2): B_r = HW_B(lambd,eta,T1,T2) A_r = HW_A(lambd,eta,P0T,T1,T2) mu_r = HW_Mu_FrwdMeasure(P0T,lambd,eta,T1) v_r = np.sqrt(HWVar_r(lambd,eta,T1)) K_hat = K * np.exp(-A_r) a = (np.log(K_hat) - B_r*mu_r)/(B_r*v_r) d1 = a - B_r*v_r d2 = d1 + B_r*v_r term1 = np.exp(0.5* B_r*B_r*v_r*v_r + B_r*mu_r)*st.norm.cdf(d1) - K_hat * st.norm.cdf(d2) value =P0T(T1) * np.exp(A_r) * term1 if CP == OptionType.CALL: return value elif CP==OptionType.PUT: return value - P0T(T2) + K*P0T(T1) # Black-Scholes Call option price def BS_Call_Put_Option_Price(CP,S_0,K,sigma,tau,r): if K is list: K = np.array(K).reshape([len(K),1]) d1 = (np.log(S_0 / K) + (r + 0.5 * np.power(sigma,2.0)) * tau) / (sigma * np.sqrt(tau)) d2 = d1 - sigma * np.sqrt(tau) if CP == OptionType.CALL: value = st.norm.cdf(d1) * S_0 - st.norm.cdf(d2) * K * np.exp(-r * tau) elif CP == OptionType.PUT: value = st.norm.cdf(-d2) * K * np.exp(-r * tau) - st.norm.cdf(-d1)*S_0 return value # Implied volatility method def ImpliedVolatilityBlack76(CP,marketPrice,K,T,S_0): # To determine initial volatility we interpolate define a grid for sigma # and interpolate on the inverse sigmaGrid = np.linspace(0.0,5.0,5000) optPriceGrid = BS_Call_Put_Option_Price(CP,S_0,K,sigmaGrid,T,0.0) sigmaInitial = np.interp(marketPrice,optPriceGrid,sigmaGrid) print("Strike = {0}".format(K)) print("Initial volatility = {0}".format(sigmaInitial)) # Use determined input for the local-search (final tuning) func = lambda sigma: np.power(BS_Call_Put_Option_Price(CP,S_0,K,sigma,T,0.0) - marketPrice, 1.0) impliedVol = optimize.newton(func, sigmaInitial, tol=1e-15) print("Final volatility = {0}".format(impliedVol)) if impliedVol > 2.0: impliedVol = 0.0 return impliedVol def mainCalculation(): CP= OptionType.CALL NoOfPaths = 20000 NoOfSteps = 1000 lambd = 0.02 eta = 0.02 # We define a ZCB curve (obtained from the market) P0T = lambda T: np.exp(-0.1*T)#np.exp(-0.03*T*T-0.1*T) r0 = HW_r_0(P0T,lambd,eta) # In this experiment we compare ZCB from the Market and Analytical expression N = 25 T_end = 50 Tgrid= np.linspace(0,T_end,N) Exact = np.zeros([N,1]) Proxy= np.zeros ([N,1]) for i,Ti in enumerate(Tgrid): Proxy[i] = HW_ZCB(lambd,eta,P0T,0.0,Ti,r0) Exact[i] = P0T(Ti) plt.figure(1) plt.grid() plt.plot(Tgrid,Exact,'-k') plt.plot(Tgrid,Proxy,'--r') plt.legend(["Analytcal ZCB","Monte Carlo ZCB"]) plt.title('P(0,T) from Monte Carlo vs. Analytical expression') # In this experiment we compare Monte Carlo results for T1 = 4.0 T2 = 8.0 paths= GeneratePathsHWEuler(NoOfPaths,NoOfSteps,T1 ,P0T, lambd, eta) r = paths["R"] timeGrid = paths["time"] dt = timeGrid[1]-timeGrid[0] # Here we compare the price of an option on a ZCB from Monte Carlo and Analytical expression M_t = np.zeros([NoOfPaths,NoOfSteps]) for i in range(0,NoOfPaths): M_t[i,:] = np.exp(np.cumsum(r[i,:-1])*dt) KVec = np.linspace(0.01,1.7,50) Price_MC_V = np.zeros([len(KVec),1]) Price_Th_V =np.zeros([len(KVec),1]) P_T1_T2 = HW_ZCB(lambd,eta,P0T,T1,T2,r[:,-1]) for i,K in enumerate(KVec): if CP==OptionType.CALL: Price_MC_V[i] =np.mean( 1.0/M_t[:,-1] * np.maximum(P_T1_T2-K,0.0)) elif CP==OptionType.PUT: Price_MC_V[i] =np.mean( 1.0/M_t[:,-1] * np.maximum(K-P_T1_T2,0.0)) Price_Th_V[i] =HW_ZCB_CallPutPrice(CP,K,lambd,eta,P0T,T1,T2)#HW_ZCB_CallPrice(K,lambd,eta,P0T,T1,T2) plt.figure(2) plt.grid() plt.plot(KVec,Price_MC_V) plt.plot(KVec,Price_Th_V,'--r') plt.legend(['Monte Carlo','Theoretical']) plt.title('Option on ZCB') # Effect of the HW model parameters on Implied Volatilities # define a forward rate between T1 and T2 frwd = 1.0/(T2-T1) *(P0T(T1)/P0T(T2)-1.0) K = np.linspace(frwd/2.0,3.0*frwd,25) # Effect of eta plt.figure(3) plt.grid() plt.xlabel('strike, K') plt.ylabel('implied volatility') etaV =[0.01, 0.02, 0.03, 0.04] legend = [] Notional = 1.0 for etaTemp in etaV: optPrice = HW_CapletFloorletPrice(CP,Notional,K,lambd,etaTemp,P0T,T1,T2) # Implied volatilities IV =np.zeros([len(K),1]) for idx in range(0,len(K)): valFrwd = optPrice[idx]/P0T(T2)/(T2-T1) IV[idx] = ImpliedVolatilityBlack76(CP,valFrwd,K[idx],T2,frwd) plt.plot(K,IV*100.0) #plt.plot(K,optPrice) legend.append('eta={0}'.format(etaTemp)) plt.legend(legend) # Effect of lambda plt.figure(4) plt.grid() plt.xlabel('strike, K') plt.ylabel('implied volatility') lambdaV = [0.01, 0.03, 0.05, 0.09] legend = [] Notional = 1.0 for lambdTemp in lambdaV: optPrice = HW_CapletFloorletPrice(CP,Notional,K,lambdTemp,eta,P0T,T1,T2) # Implied volatilities IV =np.zeros([len(K),1]) for idx in range(0,len(K)): valFrwd = optPrice[idx]/P0T(T2)/(T2-T1) IV[idx] = ImpliedVolatilityBlack76(CP,valFrwd,K[idx],T2,frwd) #plt.plot(K,optPrice) plt.plot(K,IV*100.0) legend.append('lambda={0}'.format(lambdTemp)) plt.legend(legend) print('frwd={0}'.format(frwd*P0T(T2))) mainCalculation() ================================================ FILE: Lecture 07-Pricing of Swaptions and Negative Interest Rates/Materials/JamshidianTrick.py ================================================ #%% """ Created on July 18 2021 Jamshidian's trick for handling E max sum -> sum E max This code is purely educational and comes from "Financial Engineering" course by L.A. Grzelak The course is based on the book “Mathematical Modeling and Computation in Finance: With Exercises and Python and MATLAB Computer Codes”, by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing Europe Ltd, 2019. @author: Lech A. Grzelak """ import numpy as np import matplotlib.pyplot as plt import scipy.optimize as optimize def PsiSum(psi,N,x): temp = 0 for i in range(0,N): temp = temp + psi(i,x) return temp def JamshidianTrick(psi,N,K): A = lambda x: PsiSum(psi,N,x) - K result = optimize.newton(A,0.1) return result def Main(): NoOfSamples = 1000 X = np.random.normal(0.0,1.0,[NoOfSamples,1]) psi_i = lambda i,X: np.exp(-i*np.abs(X)) # Number of terms N = 15 A = 0 for i in range(0,N): A = A + psi_i(i,X) K = np.linspace(2,10,10) resultMC = np.zeros(len(K)) for (i,Ki) in enumerate(K): resultMC[i] = np.mean(np.maximum(A-Ki,0)) # Jamshidians trick resultJams = np.zeros(len(K)) for i,Ki in enumerate(K): # Compute optimal K* optX = JamshidianTrick(psi_i,N,Ki) A = 0 for j in range(0,N): A = A + np.mean(np.maximum(psi_i(j,X)-psi_i(j,optX),0)) resultJams[i] = A plt.figure() plt.plot(K,resultMC) plt.plot(K,resultJams,'--r') plt.grid() plt.xlabel('K') plt.ylabel('expectation') plt.legend(['Monte Carlo','Jamshidians trick']) Main() ================================================ FILE: Lecture 07-Pricing of Swaptions and Negative Interest Rates/Materials/ShiftedLognormal.py ================================================ #%% """ Created on July 12 2021 Shifted GBM and pricing of caplets/floorlets This code is purely educational and comes from "Financial Engineering" course by L.A. Grzelak The course is based on the book “Mathematical Modeling and Computation in Finance: With Exercises and Python and MATLAB Computer Codes”, by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing Europe Ltd, 2019. @author: Lech A. Grzelak """ import numpy as np import matplotlib.pyplot as plt import scipy.stats as st import enum import scipy.optimize as optimize # This class defines puts and calls class OptionType(enum.Enum): CALL = 1.0 PUT = -1.0 def GeneratePathsGBMShifted(NoOfPaths,NoOfSteps,T,r,sigma,S_0,shift): S_0shift = S_0 + shift if (S_0shift < 0.0): raise('Shift is too small!') paths =GeneratePathsGBM(NoOfPaths,NoOfSteps,T,r,sigma,S_0shift) Sshifted = paths["S"] - shift time = paths["time"] return {"time":time,"S":Sshifted} def GeneratePathsGBM(NoOfPaths,NoOfSteps,T,r,sigma,S_0): Z = np.random.normal(0.0,1.0,[NoOfPaths,NoOfSteps]) X = np.zeros([NoOfPaths, NoOfSteps+1]) W = np.zeros([NoOfPaths, NoOfSteps+1]) time = np.zeros([NoOfSteps+1]) X[:,0] = np.log(S_0) dt = T / float(NoOfSteps) for i in range(0,NoOfSteps): # making sure that samples from normal have mean 0 and variance 1 if NoOfPaths > 1: Z[:,i] = (Z[:,i] - np.mean(Z[:,i])) / np.std(Z[:,i]) W[:,i+1] = W[:,i] + np.power(dt, 0.5)*Z[:,i] X[:,i+1] = X[:,i] + (r - 0.5 * sigma * sigma) * dt + sigma * (W[:,i+1]-W[:,i]) time[i+1] = time[i] +dt #Compute exponent of ABM S = np.exp(X) paths = {"time":time,"S":S} return paths # Shifted Black-Scholes Call option price def BS_Call_Put_Option_Price_Shifted(CP,S_0,K,sigma,tau,r,shift): K_new = K + shift S_0_new = S_0 + shift return BS_Call_Put_Option_Price(CP,S_0_new,K_new,sigma,tau,r) # Black-Scholes Call option price def BS_Call_Put_Option_Price(CP,S_0,K,sigma,tau,r): if K is list: K = np.array(K).reshape([len(K),1]) d1 = (np.log(S_0 / K) + (r + 0.5 * np.power(sigma,2.0)) * tau) / (sigma * np.sqrt(tau)) d2 = d1 - sigma * np.sqrt(tau) if CP == OptionType.CALL: value = st.norm.cdf(d1) * S_0 - st.norm.cdf(d2) * K * np.exp(-r * tau) elif CP == OptionType.PUT: value = st.norm.cdf(-d2) * K * np.exp(-r * tau) - st.norm.cdf(-d1)*S_0 return value # Implied volatility method def ImpliedVolatilityBlack76(CP,marketPrice,K,T,S_0): # To determine initial volatility we interpolate define a grid for sigma # and interpolate on the inverse sigmaGrid = np.linspace(0.0,2.0,5000) optPriceGrid = BS_Call_Put_Option_Price(CP,S_0,K,sigmaGrid,T,0.0) sigmaInitial = np.interp(marketPrice,optPriceGrid,sigmaGrid) print("Initial volatility = {0}".format(sigmaInitial)) # Use determined input for the local-search (final tuning) func = lambda sigma: np.power(BS_Call_Put_Option_Price(CP,S_0,K,sigma,T,0.0) - marketPrice, 1.0) impliedVol = optimize.newton(func, sigmaInitial, tol=1e-15) print("Final volatility = {0}".format(impliedVol)) return impliedVol # Implied volatility method def ImpliedVolatilityBlack76Shifted(CP,marketPrice,K,T,S_0,shift): # To determine initial volatility we interpolate define a grid for sigma # and interpolate on the inverse sigmaGrid = np.linspace(0.0,2.0,5000) optPriceGrid = BS_Call_Put_Option_Price_Shifted(CP,S_0,K,sigmaGrid,T,0.0,shift) sigmaInitial = np.interp(marketPrice,optPriceGrid,sigmaGrid) print("Initial volatility = {0}".format(sigmaInitial)) # Use determined input for the local-search (final tuning) func = lambda sigma: np.power(BS_Call_Put_Option_Price_Shifted(CP,S_0,K,sigma,T,0.0,shift) - marketPrice, 1.0) impliedVol = optimize.newton(func, sigmaInitial, tol=1e-15) print("Final volatility = {0}".format(impliedVol)) return impliedVol def mainCalculation(): NoOfPaths = 10000 NoOfSteps = 500 T = 3.0 sigma = 0.2 L0 = -0.05 shift = 0.1 K = [0.95] CP = OptionType.CALL P0T =lambda T: np.exp(-0.1*T) np.random.seed(4) Paths = GeneratePathsGBMShifted(NoOfPaths,NoOfSteps,T,0.0,sigma,L0,shift) time = Paths["time"] L = Paths["S"] print(np.mean(L[:,-1])) # Plot first few paths plt.figure(1) plt.plot(time,np.transpose(L[0:20,:])) plt.grid() # Shifted lognormal for different shift parameters plt.figure(2) shiftV = [1.0,2.0,3.0,4.0,5.0] legend = [] for shiftTemp in shiftV: x = np.linspace(-shiftTemp,10,1000) lognnormPDF = lambda x,t : st.lognorm.pdf(x+shiftTemp, scale = np.exp(np.log(L0+shiftTemp) + (- 0.5 * sigma * sigma)*t), s= np.sqrt(t) * sigma) pdf_x= lognnormPDF(x,T) plt.plot(x,pdf_x) legend.append('shift={0}'.format(shiftTemp)) plt.legend(legend) plt.xlabel('x') plt.ylabel('pdf') plt.title('shifted lognormal density') plt.grid() # Call/Put option prices, MC vs. Analytical plt.figure(3) K = np.linspace(-shift,np.abs(L0)*3,25) optPriceMCV=np.zeros([len(K),1]) for idx in range(0,len(K)): optPriceMCV[idx] =0.0 if CP == OptionType.CALL: optPriceMCV[idx] = P0T(T)*np.mean(np.maximum(L[:,-1]-K[idx],0.0)) elif CP == OptionType.PUT: optPriceMCV[idx] = P0T(T)*np.mean(np.maximum(K[idx]-L[:,-1],0.0)) optPriceExact = P0T(T)*BS_Call_Put_Option_Price_Shifted(CP,L0,K,sigma,T,0.0,shift) plt.plot(K,optPriceMCV) plt.plot(K,optPriceExact,'--r') plt.grid() plt.xlabel('strike,K') plt.ylabel('option price') plt.legend(['Monte Carlo','Exact']) # Shift Effect on Option prices plt.figure(4) legend = [] for shiftTemp in [0.2,0.3,0.4,0.5]: K = np.linspace(-shiftTemp,np.abs(L0)*6.0,25) optPriceExact = P0T(T)*BS_Call_Put_Option_Price_Shifted(CP,L0,K,sigma,T,0.0,shiftTemp) plt.plot(K,optPriceExact) legend.append('shift={0}'.format(shiftTemp)) plt.grid() plt.xlabel('strike,K') plt.ylabel('option price') plt.legend(legend) mainCalculation() ================================================ FILE: Lecture 08-Mortgages and Prepayments/Materials/AnnuityMortgage.py ================================================ #%% """ Created on July 05 2021 Annuity mortgage- payment profile This code is purely educational and comes from "Financial Engineering" course by L.A. Grzelak The course is based on the book “Mathematical Modeling and Computation in Finance: With Exercises and Python and MATLAB Computer Codes”, by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing Europe Ltd, 2019. @author: Lech A. Grzelak and Emanuele Casamassima """ import numpy as np import matplotlib.pyplot as plt def Annuity(rate,notional,periods,CPR): # it returns a matrix M such that # M = [t notional(t) prepayment(t) notional_quote(t) interest_quote(t) installment(t)] # WARNING! here "rate" and "periods" are quite general, the choice of getting year/month/day.. steps, depends on the rate # that the function receives. So, it is necessary to pass the correct rate to the function M = np.zeros((periods + 1,6)) M[:,0] = np.arange(periods + 1) # we define the times M[0,1] = notional for t in range(1,periods + 1): # we are computing the installment at time t knowing the oustanding at time (t-1) remaining_periods = periods - (t - 1) # Installment, C(t_i) M[t,5] = rate * M[t-1,1]/(1 - 1/(1 + rate)**remaining_periods) # Interest rate payment, I(t_i) = K * N(t_{i}) M[t,4] = rate * M[t-1,1] # Notional payment, Q(t_i) = C(t_i) - I(t_i) M[t,3] = M[t,5] - M[t,4] # Prepayment, P(t_i)= Lambda * (N(t_i) -Q(t_i)) M[t,2] = CPR * (M[t-1,1] - M[t,3]) # notional, N(t_{i+1}) = N(t_{i}) - lambda * (Q(t_{i} + P(t_i))) M[t,1] = M[t-1,1] - M[t,3] - M[t,2] return M def mainCode(): # Initial notional N0 = 1000000 # Interest rates from a bank r = 0.05 # Prepayment rate, 0.1 = 10% Lambda = 0.1 # For simplicity we assume 1 as unit (yearly payments of mortgage) T_end = 30 M = Annuity(r,N0,T_end,Lambda) for i in range(0,T_end+1): print("Ti={0}, Notional={1:.0f}, Prepayment={2:.0f}, Notional Repayment={3:.0f}, Interest Rate={4:.0f}, Installment={5:.0f} ".format(M[i,0],M[i,1],M[i,2],M[i,3],M[i,4],M[i,5])) plt.figure(1) plt.plot(M[:,0],M[:,1],'.r') plt.grid() plt.xlabel('time') plt.ylabel('notional') return 0.0 mainCode() ================================================ FILE: Lecture 08-Mortgages and Prepayments/Materials/BulletMortgage.py ================================================ #%% """ Created on July 05 2021 Bullet mortgage- payment profile This code is purely educational and comes from "Financial Engineering" course by L.A. Grzelak The course is based on the book “Mathematical Modeling and Computation in Finance: With Exercises and Python and MATLAB Computer Codes”, by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing Europe Ltd, 2019. @author: Lech A. Grzelak and Emanuele Casamassima """ import numpy as np import matplotlib.pyplot as plt def Bullet(rate,notional,periods,CPR): # it returns a matrix M such that # M = [t notional(t) prepayment(t) notional_quote(t) interest_(t) installment(t)] # WARNING! here "rate" and "periods" are quite general, the choice of getting year/month/day.. steps, depends on the rate # that the function receives. So, it is necessary to pass the correct rate to the function M = np.zeros((periods + 1,6)) M[:,0] = np.arange(periods + 1) # we define the times M[0,1] = notional for t in range(1,periods): M[t,4] = rate*M[t-1,1] # interest quote M[t,3] = 0 # repayment, 0 for bullet mortgage scheduled_oustanding = M[t-1,1] - M[t,3] M[t,2] = scheduled_oustanding * CPR # prepayment M[t,1] = scheduled_oustanding - M[t,2] # notional(t) = notional(t-1) - (repayment + prepayment) M[t,5] = M[t,4] + M[t,2] + M[t,3] M[periods,4] = rate*M[periods-1,1] # interest quote M[periods,3] = M[periods-1,1] # notional quote M[periods,5] = M[periods,4] + M[periods,2] + M[periods,3] return M def mainCode(): # Initial notional N0 = 1000000 # Interest rates from a bank r = 0.05 # Prepayment rate, 0.1 = 10% Lambda = 0.01 # For simplicity we assume 1 as unit (yearly payments of mortgage) T_end = 30 M = Bullet(r,N0,T_end,Lambda) for i in range(0,T_end+1): print("Ti={0}, Notional={1:.0f}, Prepayment={2:.0f}, Notional Repayment={3:.0f}, Interest Rate={4:.0f}, Installment={5:.0f} ".format(M[i,0],M[i,1],M[i,2],M[i,3],M[i,4],M[i,5])) plt.figure(1) plt.plot(M[:,0],M[:,1],'-r') plt.grid() plt.xlabel('time') plt.ylabel('notional') return 0.0 mainCode() ================================================ FILE: Lecture 08-Mortgages and Prepayments/Materials/Incentives.py ================================================ #%% """ Created on July 05 2021 Incentive function as a function of a swap rate or the differential w.r.t. "old" mortgage rate This code is purely educational and comes from "Financial Engineering" course by L.A. Grzelak The course is based on the book “Mathematical Modeling and Computation in Finance: With Exercises and Python and MATLAB Computer Codes”, by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing Europe Ltd, 2019. @author: Lech A. Grzelak and Emanuele Cassamassima """ import numpy as np import matplotlib.pyplot as plt def Annuity(rate,notional,periods,CPR): # it returns a matrix M such that # M = [t notional(t) prepayment(t) notional_quote(t) interest_quote(t) installment(t)] # WARNING! here "rate" and "periods" are quite general, the choice of getting year/month/day.. steps, depends on the rate # that the function receives. So, it is necessary to pass the correct rate to the function M = np.zeros((periods + 1,6)) M[:,0] = np.arange(periods + 1) # we define the times M[0,1] = notional for t in range(1,periods + 1): # we are computing the installment at time t knowing the oustanding at time (t-1) remaining_periods = periods - (t - 1) # Installment, C(t_i) M[t,5] = rate * M[t-1,1]/(1 - 1/(1 + rate)**remaining_periods) # Interest rate payment, I(t_i) = r * N(t_{i}) M[t,4] = rate * M[t-1,1] # Notional payment, Q(t_i) = C(t_i) - I(t_i) M[t,3] = M[t,5] - M[t,4] # Prepayment, P(t_i)= Lambda * (N(t_i) -Q(t_i)) M[t,2] = CPR * (M[t-1,1] - M[t,3]) # notional, N(t_{i+1}) = N(t_{i}) - lambda * (Q(t_{i} + P(t_i))) M[t,1] = M[t-1,1] - M[t,3] - M[t,2] return M def mainCode(): IncentiveFunction = lambda x : 0.04 + 0.1/(1 + np.exp(115 * (0.02-x))) oldRate = 0.05 newRate = np.linspace(-0.05,0.15,25) epsilon = oldRate-newRate incentive = IncentiveFunction(epsilon) plt.figure(1) plt.plot(newRate,incentive) plt.xlabel('S(t)') plt.ylabel('Incentive') plt.grid() plt.figure(2) plt.plot(epsilon,incentive) plt.xlabel('epsilon= K - S(t)') plt.ylabel('Incentive') plt.grid() return 0.0 mainCode() ================================================ FILE: Lecture 08-Mortgages and Prepayments/Materials/StochasticAmortizingSwap.py ================================================ #%% """ Created on July 05 2021 Stochastic amortization given the incentive function and irrational/rational behavior profile This code is purely educational and comes from "Financial Engineering" course by L.A. Grzelak The course is based on the book “Mathematical Modeling and Computation in Finance: With Exercises and Python and MATLAB Computer Codes”, by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing Europe Ltd, 2019. @author: Lech A. Grzelak and Emanuele Cassamassima """ import numpy as np import matplotlib.pyplot as plt import scipy.optimize as optimize import scipy.integrate as integrate def GeneratePathsHWEuler(NoOfPaths,NoOfSteps,T,P0T, lambd, eta): # time-step needed for differentiation dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) # Initial interest rate is a forward rate at time t->0 r0 = f0T(0.00001) theta = lambda t: 1.0/lambd * (f0T(t+dt)-f0T(t-dt))/(2.0*dt) + f0T(t) + eta*eta/(2.0*lambd*lambd)*(1.0-np.exp(-2.0*lambd*t)) #theta = lambda t: 0.1 +t -t #print("changed theta") Z = np.random.normal(0.0,1.0,[NoOfPaths,NoOfSteps]) W = np.zeros([NoOfPaths, NoOfSteps+1]) R = np.zeros([NoOfPaths, NoOfSteps+1]) R[:,0]=r0 time = np.zeros([NoOfSteps+1]) dt = T / float(NoOfSteps) for i in range(0,NoOfSteps): # making sure that samples from normal have mean 0 and variance 1 if NoOfPaths > 1: Z[:,i] = (Z[:,i] - np.mean(Z[:,i])) / np.std(Z[:,i]) W[:,i+1] = W[:,i] + np.power(dt, 0.5)*Z[:,i] R[:,i+1] = R[:,i] + lambd*(theta(time[i]) - R[:,i]) * dt + eta* (W[:,i+1]-W[:,i]) time[i+1] = time[i] +dt # Outputs paths = {"time":time,"R":R} return paths def HW_theta(lambd,eta,P0T): dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) theta = lambda t: 1.0/lambd * (f0T(t+dt)-f0T(t-dt))/(2.0*dt) + f0T(t) + eta*eta/(2.0*lambd*lambd)*(1.0-np.exp(-2.0*lambd*t)) return theta def HW_A(lambd,eta,P0T,T1,T2): tau = T2-T1 zGrid = np.linspace(0.0,tau,250) B_r = lambda tau: 1.0/lambd * (np.exp(-lambd *tau)-1.0) theta = HW_theta(lambd,eta,P0T) temp1 = lambd * integrate.trapz(theta(T2-zGrid)*B_r(zGrid),zGrid) temp2 = eta*eta/(4.0*np.power(lambd,3.0)) * (np.exp(-2.0*lambd*tau)*(4*np.exp(lambd*tau)-1.0) -3.0) + eta*eta*tau/(2.0*lambd*lambd) return temp1 + temp2 def HW_B(lambd,eta,T1,T2): return 1.0/lambd *(np.exp(-lambd*(T2-T1))-1.0) def HW_ZCB(lambd,eta,P0T,T1,T2,rT1): n = np.size(rT1) if T1Ti prevTi = ti_grid[np.where(ti_grid 0: #prevTi != []: Ti = prevTi[-1] # Now we need to handle the case when some payments are already done ti_grid = ti_grid[np.where(ti_grid>t)] temp= np.zeros(np.size(r_t)); P_t_TiLambda = lambda Ti : HW_ZCB(lambd,eta,P0T,t,Ti,r_t) for (idx,ti) in enumerate(ti_grid): if ti>Ti: temp = temp + tau * P_t_TiLambda(ti) P_t_Ti = P_t_TiLambda(Ti) P_t_Tm = P_t_TiLambda(Tm) swapRate = (P_t_Ti - P_t_Tm) / temp return swapRate def Bullet(rate,notional,periods,CPR): # it returns a matrix M such that # M = [t notional(t) prepayment(t) notional_quote(t) interest_(t) installment(t)] # WARNING! here "rate" and "periods" are quite general, the choice of getting year/month/day.. steps, depends on the rate # that the function receives. So, it is necessary to pass the correct rate to the function M = np.zeros((periods + 1,6)) M[:,0] = np.arange(periods + 1) # we define the times M[0,1] = notional for t in range(1,periods): M[t,4] = rate*M[t-1,1] # interest quote M[t,3] = 0 # notional quote, 0 for bullet mortgage scheduled_oustanding = M[t-1,1] - M[t,3] M[t,2] = scheduled_oustanding * CPR[t] # prepayment M[t,1] = scheduled_oustanding - M[t,2] # notional(t) = notional(t-1) - (notional quote + prepayment) M[t,5] = M[t,4] + M[t,2] + M[t,3] M[periods,4] = rate*M[periods-1,1] # interest quote M[periods,3] = M[periods-1,1] # notional quote M[periods,5] = M[periods,4] + M[periods,2] + M[periods,3] return M def Annuity(rate,notional,periods,CPR): # it returns a matrix M such that # M = [t notional(t) prepayment(t) notional_quote(t) interest_quote(t) installment(t)] # WARNING! here "rate" and "periods" are quite general, the choice of getting year/month/day.. steps, depends on the rate # that the function receives. So, it is necessary to pass the correct rate to the function M = np.zeros((periods + 1,6)) M[:,0] = np.arange(periods + 1) # we define the times M[0,1] = notional for t in range(1,periods + 1): # we are computing the installment at time t knowing the oustanding at time (t-1) remaining_periods = periods - (t - 1) # Installment, C(t_i) M[t,5] = rate * M[t-1,1]/(1 - 1/(1 + rate)**remaining_periods) # Interest rate payment, I(t_i) = r * N(t_{i}) M[t,4] = rate * M[t-1,1] # Notional payment, Q(t_i) = C(t_i) - I(t_i) M[t,3] = M[t,5] - M[t,4] # Prepayment, P(t_i)= Lambda * (N(t_i) -Q(t_i)) M[t,2] = CPR[t] * (M[t-1,1] - M[t,3]) # notional, N(t_{i+1}) = N(t_{i}) - lambda * (Q(t_{i} + P(t_i))) M[t,1] = M[t-1,1] - M[t,3] - M[t,2] return M def mainCode(): Irrational = lambda x : 0.04 + 0.1/(1 + np.exp(200 * (-x))) Rational = lambda x : 0.04*(x>0.0) IncentiveFunction = Irrational K = 0.05 newRate = np.linspace(-0.1,0.1,150) epsilon = K - newRate incentive = IncentiveFunction(epsilon) plt.figure(1) plt.plot(newRate,incentive) plt.xlabel('S(t)') plt.ylabel('Incentive') plt.grid() plt.figure(2) plt.plot(epsilon,incentive) plt.xlabel('epsilon= K - S(t)') plt.ylabel('Incentive') plt.grid() # Stochastic interest rates NoOfPaths = 2000 NoOfSteps = 30 lambd = 0.05 eta = 0.01 # End date of the underlying swap / mortgage Tend = 30 # Market ZCB P0T = lambda T: np.exp(-0.05*T) paths = GeneratePathsHWEuler(NoOfPaths, NoOfSteps,Tend, P0T, lambd, eta) R = paths["R"] tiGrid = paths["time"] # Compute swap rates, at this point we assume that the incentive is driven by the CMS rate S = np.zeros([NoOfPaths,NoOfSteps+1]) for (i,ti) in enumerate(tiGrid): S[:,i] = SwapRateHW(ti,ti,Tend+ti,30,R[:,i],P0T,lambd,eta) # Incentive for the new swap rate epsilon = K - S[:,-1] incentive = IncentiveFunction(epsilon) plt.figure(3) plt.plot(epsilon,incentive,'.r') plt.xlabel('epsilon= K - S(t)') plt.ylabel('Incentive') plt.grid() plt.title('Incentive for prepayment given stochastis S(t)') plt.figure(4) plt.hist(S[:,-1],bins=50) plt.grid() plt.title('Swap distribution at Tend') # Building up of stochastic notional N(ti) for every ti MortgageProfile = Annuity notional = 1000000 N = np.zeros([NoOfPaths,NoOfSteps+1]) for i in range(0,NoOfPaths): epsilon = K - S[i,:] Lambda = IncentiveFunction(epsilon) NotionalProfile = MortgageProfile(K,notional,NoOfSteps,Lambda) N[i,:] = NotionalProfile[:,1] plt.figure(6) plt.grid() plt.xlabel('time') plt.ylabel('notional') n = 100 for k in range(0,n): plt.plot(tiGrid,N[k,:],'-b') AnnuityProfile_NoPrepayment = MortgageProfile(K,notional,NoOfSteps,np.zeros(NoOfSteps+1)) plt.plot(tiGrid,AnnuityProfile_NoPrepayment[:,1],'--r') plt.title('Notional profile') return 0.0 mainCode() ================================================ FILE: Lecture 09-Hybrid Models and Stochastic Interest Rates/Materials/BSHW_Comparison.py ================================================ #%% """ Created on Thu Jan 04 2019 The BSHW model and implied volatilities obtained with the COS method and comparisons against a factorized 1D case where the implied volatilities are known analytically This code is purely educational and comes from "Financial Engineering" course by L.A. Grzelak The course is based on the book “Mathematical Modeling and Computation in Finance: With Exercises and Python and MATLAB Computer Codes”, by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing Europe Ltd, 2019. @author: Lech A. Grzelak """ import numpy as np import matplotlib.pyplot as plt import scipy.integrate as integrate import scipy.stats as st import enum import scipy.optimize as optimize # set i= imaginary number i = np.complex(0.0,1.0) # time-step needed for differentiation dt = 0.0001 # This class defines puts and calls class OptionType(enum.Enum): CALL = 1.0 PUT = -1.0 def CallPutOptionPriceCOSMthd_StochIR(cf,CP,S0,tau,K,N,L,P0T): # cf - characteristic function as a functon, in the book denoted as \varphi # CP - C for call and P for put # S0 - Initial stock price # tau - time to maturity # K - list of strikes # N - Number of expansion terms # L - size of truncation domain (typ.:L=8 or L=10) # P0T - zero-coupon bond for maturity T. # reshape K to a column vector if K is not np.array: K = np.array(K).reshape([len(K),1]) #assigning i=sqrt(-1) i = np.complex(0.0,1.0) x0 = np.log(S0 / K) # truncation domain a = 0.0 - L * np.sqrt(tau) b = 0.0 + L * np.sqrt(tau) # sumation from k = 0 to k=N-1 k = np.linspace(0,N-1,N).reshape([N,1]) u = k * np.pi / (b - a); # Determine coefficients for Put Prices H_k = CallPutCoefficients(OptionType.PUT,a,b,k) mat = np.exp(i * np.outer((x0 - a) , u)) temp = cf(u) * H_k temp[0] = 0.5 * temp[0] value = K * np.real(mat.dot(temp)) # we use call-put parity for call options if CP == OptionType.CALL: value = value + S0 - K * P0T return value # Determine coefficients for Put Prices def CallPutCoefficients(CP,a,b,k): if CP==OptionType.CALL: c = 0.0 d = b coef = Chi_Psi(a,b,c,d,k) Chi_k = coef["chi"] Psi_k = coef["psi"] if a < b and b < 0.0: H_k = np.zeros([len(k),1]) else: H_k = 2.0 / (b - a) * (Chi_k - Psi_k) elif CP==OptionType.PUT: c = a d = 0.0 coef = Chi_Psi(a,b,c,d,k) Chi_k = coef["chi"] Psi_k = coef["psi"] H_k = 2.0 / (b - a) * (- Chi_k + Psi_k) return H_k def Chi_Psi(a,b,c,d,k): psi = np.sin(k * np.pi * (d - a) / (b - a)) - np.sin(k * np.pi * (c - a)/(b - a)) psi[1:] = psi[1:] * (b - a) / (k[1:] * np.pi) psi[0] = d - c chi = 1.0 / (1.0 + np.power((k * np.pi / (b - a)) , 2.0)) expr1 = np.cos(k * np.pi * (d - a)/(b - a)) * np.exp(d) - np.cos(k * np.pi * (c - a) / (b - a)) * np.exp(c) expr2 = k * np.pi / (b - a) * np.sin(k * np.pi * (d - a) / (b - a)) - k * np.pi / (b - a) * np.sin(k * np.pi * (c - a) / (b - a)) * np.exp(c) chi = chi * (expr1 + expr2) value = {"chi":chi,"psi":psi } return value # Black-Scholes Call option price def BS_Call_Option_Price(CP,S_0,K,sigma,tau,r): if K is list: K = np.array(K).reshape([len(K),1]) d1 = (np.log(S_0 / K) + (r + 0.5 * np.power(sigma,2.0)) * tau) / float(sigma * np.sqrt(tau)) d2 = d1 - sigma * np.sqrt(tau) if CP == OptionType.CALL: value = st.norm.cdf(d1) * S_0 - st.norm.cdf(d2) * K * np.exp(-r * tau) elif CP == OptionType.PUT: value = st.norm.cdf(-d2) * K * np.exp(-r * tau) - st.norm.cdf(-d1)*S_0 return value # Implied volatility method def ImpliedVolatilityBlack76(CP,marketPrice,K,T,S_0): func = lambda sigma: np.power(BS_Call_Option_Price(CP,S_0,K,sigma,T,0.0) - marketPrice, 1.0) impliedVol = optimize.newton(func, 0.2, tol=1e-9) #impliedVol = optimize.brent(func, brack= (0.05, 2)) return impliedVol def ChFBSHW(u, T, P0T, lambd, eta, rho, sigma): i = np.complex(0.0,1.0) f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) # Initial interest rate is a forward rate at time t->0 r0 = f0T(0.00001) theta = lambda t: 1.0/lambd * (f0T(t+dt)-f0T(t-dt))/(2.0*dt) + f0T(t) \ + eta*eta/(2.0*lambd*lambd)*(1.0-np.exp(-2.0*lambd*t)) C = lambda u,tau: 1.0/lambd*(i*u-1.0)*(1.0-np.exp(-lambd*tau)) # define a grid for the numerical integration of function theta zGrid = np.linspace(0.0,T,2500) term1 = lambda u: 0.5*sigma*sigma *i*u*(i*u-1.0)*T term2 = lambda u: i*u*rho*sigma*eta/lambd*(i*u-1.0)*(T+1.0/lambd \ *(np.exp(-lambd*T)-1.0)) term3 = lambda u: eta*eta/(4.0*np.power(lambd,3.0))*np.power(i+u,2.0)*\ (3.0+np.exp(-2.0*lambd*T)-4.0*np.exp(-lambd*T)-2.0*lambd*T) term4 = lambda u: lambd*integrate.trapz(theta(T-zGrid)*C(u,zGrid), zGrid) A= lambda u: term1(u) + term2(u) + term3(u) + term4(u) # Note that we don't include B(u)*x0 term as it is included in the COS method cf = lambda u : np.exp(A(u) + C(u,T)*r0 ) # Iterate over all u and collect the ChF, iteration is necessary due to the integration over in term4 cfV = [] for ui in u: cfV.append(cf(ui)) return cfV def BSHWVolatility(T,eta,sigma,rho,lambd): Br= lambda t,T: 1/lambd * (np.exp(-lambd*(T-t))-1.0) sigmaF = lambda t: np.sqrt(sigma * sigma + eta * eta * Br(t,T) * Br(t,T)\ - 2.0 * rho * sigma * eta * Br(t,T)) zGrid = np.linspace(0.0,T,2500) sigmaC = np.sqrt(1/T*integrate.trapz(sigmaF(zGrid)*sigmaF(zGrid), zGrid)) return sigmaC def BSHWOptionPrice(CP,S0,K,P0T,T,eta,sigma,rho,lambd): frwdS0 = S0 / P0T vol = BSHWVolatility(T,eta,sigma,rho,lambd) # As we deal with the forward prices we evaluate Black's 76 prices r = 0.0 BlackPrice = BS_Call_Option_Price(CP,frwdS0,K,vol,T,r) return P0T * BlackPrice def mainCalculation(): CP = OptionType.CALL K = np.linspace(40.0,220.0,100) K = np.array(K).reshape([len(K),1]) # HW model settings lambd = 0.1 eta = 0.05 sigma = 0.2 rho = 0.3 S0 = 100 T = 5.0 # We define a ZCB curve (obtained from the market) P0T = lambda T: np.exp(-0.05*T) N = 500 L = 8 # Characteristic function of the BSHW model + the COS method cf = lambda u: ChFBSHW(u, T, P0T, lambd, eta, rho, sigma) valCOS = CallPutOptionPriceCOSMthd_StochIR(cf, CP, S0, T, K, N, L,P0T(T)) exactBSHW = BSHWOptionPrice(CP,S0,K,P0T(T),T,eta,sigma,rho,lambd) IV = np.zeros([len(K),1]) for idx in range(0,len(K)): frwdStock = S0 / P0T(T) valCOSFrwd = valCOS[idx] / P0T(T) IV[idx] = ImpliedVolatilityBlack76(CP,valCOSFrwd, K[idx], T, frwdStock) IVExact = BSHWVolatility(T,eta,sigma,rho,lambd) print(IVExact) # Plot option prices plt.figure(1) plt.plot(K,valCOS) plt.plot(K,exactBSHW,'--r') plt.grid() plt.xlabel("strike") plt.ylabel("option price") plt.legend(["BSHW, COS method","BSHW, exact solution"]) # Plot implied volatilities plt.figure(2) plt.plot(K,IV*100.0) plt.plot(K,np.ones([len(K),1])*IVExact*100.0,'--r') plt.grid() plt.xlabel("strike") plt.ylabel("Implied Volatility [%]") plt.legend(["BSHW, COS method","BSHW, exact solution"]) plt.axis([np.min(K),np.max(K),0,100]) mainCalculation() ================================================ FILE: Lecture 09-Hybrid Models and Stochastic Interest Rates/Materials/BSHW_ImpliedVolatility.py ================================================ #%% """ Created on July 05 2021 The BSHW model and implied volatilities term structure computerion This code is purely educational and comes from "Financial Engineering" course by L.A. Grzelak The course is based on the book “Mathematical Modeling and Computation in Finance: With Exercises and Python and MATLAB Computer Codes”, by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing Europe Ltd, 2019. @author: Lech A. Grzelak """ import numpy as np import matplotlib.pyplot as plt import scipy.integrate as integrate import scipy.stats as st import enum import scipy.optimize as optimize # set i= imaginary number i = np.complex(0.0,1.0) # time-step needed for differentiation dt = 0.0001 # This class defines puts and calls class OptionType(enum.Enum): CALL = 1.0 PUT = -1.0 # Black-Scholes Call option price def BS_Call_Option_Price(CP,S_0,K,sigma,tau,r): if K is list: K = np.array(K).reshape([len(K),1]) d1 = (np.log(S_0 / K) + (r + 0.5 * np.power(sigma,2.0)) * tau) / float(sigma * np.sqrt(tau)) d2 = d1 - sigma * np.sqrt(tau) if CP == OptionType.CALL: value = st.norm.cdf(d1) * S_0 - st.norm.cdf(d2) * K * np.exp(-r * tau) elif CP == OptionType.PUT: value = st.norm.cdf(-d2) * K * np.exp(-r * tau) - st.norm.cdf(-d1)*S_0 return value # Implied volatility method def ImpliedVolatilityBlack76(CP,frwdMarketPrice,K,T,frwdStock): func = lambda sigma: np.power(BS_Call_Option_Price(CP,frwdStock,K,sigma,T,0.0) - frwdMarketPrice, 1.0) impliedVol = optimize.newton(func, 0.2, tol=1e-9) #impliedVol = optimize.brent(func, brack= (0.05, 2)) return impliedVol def BSHWVolatility(T,eta,sigma,rho,lambd): Br= lambda t,T: 1/lambd * (np.exp(-lambd*(T-t))-1.0) sigmaF = lambda t: np.sqrt(sigma * sigma + eta * eta * Br(t,T) * Br(t,T)\ - 2.0 * rho * sigma * eta * Br(t,T)) zGrid = np.linspace(0.0,T,2500) sigmaC = np.sqrt(1/T*integrate.trapz(sigmaF(zGrid)*sigmaF(zGrid), zGrid)) return sigmaC def BSHWOptionPrice(CP,S0,K,P0T,T,eta,sigma,rho,lambd): frwdS0 = S0 / P0T vol = BSHWVolatility(T,eta,sigma,rho,lambd) # As we deal with the forward prices we evaluate Black's 76 prices r = 0.0 BlackPrice = BS_Call_Option_Price(CP,frwdS0,K,vol,T,r) return P0T * BlackPrice def mainCalculation(): CP = OptionType.CALL # HW model settings lambd = 0.1 eta = 0.01 sigma = 0.2 rho = 0.3 S0 = 100 # Strike equals stock value, thus ATM K = [100] K = np.array(K).reshape([len(K),1]) # We define a ZCB curve (obtained from the market) P0T = lambda T: np.exp(-0.05*T) # Maturitires at which we compute implied volatility TMat = np.linspace(0.1,5.0,20) # Effect of lambda plt.figure(2) plt.grid() plt.xlabel('maturity, T') plt.ylabel('implied volatility') lambdV = [0.001,0.1,0.5,1.5] legend = [] for lambdaTemp in lambdV: IV =np.zeros([len(TMat),1]) for idx in range(0,len(TMat)): T = TMat[idx] val = BSHWOptionPrice(CP,S0,K,P0T(T),T,eta,sigma,rho,lambdaTemp) frwdStock = S0 / P0T(T) valFrwd = val / P0T(T) IV[idx] = ImpliedVolatilityBlack76(CP,valFrwd,K,T,frwdStock) plt.plot(TMat,IV*100.0) legend.append('lambda={0}'.format(lambdaTemp)) plt.legend(legend) # Effect of eta plt.figure(3) plt.grid() plt.xlabel('maturity, T') plt.ylabel('implied volatility') etaV = [0.001,0.05,0.1,0.15] legend = [] for etaTemp in etaV: IV =np.zeros([len(TMat),1]) for idx in range(0,len(TMat)): T = TMat[idx] val = BSHWOptionPrice(CP,S0,K,P0T(T),T,etaTemp,sigma,rho,lambd) frwdStock = S0 / P0T(T) valFrwd = val/P0T(T) IV[idx] = ImpliedVolatilityBlack76(CP,valFrwd,K,T,frwdStock) plt.plot(TMat,IV*100.0) legend.append('eta={0}'.format(etaTemp)) plt.legend(legend) # Effect of sigma plt.figure(4) plt.grid() plt.xlabel('maturity, T') plt.ylabel('implied volatility') sigmaV = [0.1,0.2,0.3,0.4] legend = [] for sigmaTemp in sigmaV: IV =np.zeros([len(TMat),1]) for idx in range(0,len(TMat)): T = TMat[idx] val = BSHWOptionPrice(CP,S0,K,P0T(T),T,eta,sigmaTemp,rho,lambd) frwdStock = S0 / P0T(T) valFrwd = val / P0T(T) IV[idx] = ImpliedVolatilityBlack76(CP,valFrwd,K,T,frwdStock) plt.plot(TMat,IV*100.0) legend.append('sigma={0}'.format(sigmaTemp)) plt.legend(legend) # Effect of rho plt.figure(5) plt.grid() plt.xlabel('maturity, T') plt.ylabel('implied volatility') rhoV = [-0.7, -0.3, 0.3, 0.7] legend = [] for rhoTemp in rhoV: IV =np.zeros([len(TMat),1]) for idx in range(0,len(TMat)): T = TMat[idx] val = BSHWOptionPrice(CP,S0,K,P0T(T),T,eta,sigma,rhoTemp,lambd) frwdStock = S0 / P0T(T) valFrwd = val / P0T(T) IV[idx] = ImpliedVolatilityBlack76(CP,valFrwd,K,T,frwdStock) plt.plot(TMat,IV*100.0) legend.append('rho={0}'.format(rhoTemp)) plt.legend(legend) mainCalculation() ================================================ FILE: Lecture 09-Hybrid Models and Stochastic Interest Rates/Materials/H1_HW_COS_vs_MC.py ================================================ #%% """ Created on July 05 2021 The Heston-Hull-White model and pricing European Options with Monte Carlo and the COS method This code is purely educational and comes from "Financial Engineering" course by L.A. Grzelak The course is based on the book “Mathematical Modeling and Computation in Finance: With Exercises and Python and MATLAB Computer Codes”, by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing Europe Ltd, 2019. @author: Lech A. Grzelak """ import numpy as np import matplotlib.pyplot as plt import scipy.special as sp import scipy.integrate as integrate import enum # This class defines puts and calls class OptionType(enum.Enum): CALL = 1.0 PUT = -1.0 def CallPutOptionPriceCOSMthd_StochIR(cf,CP,S0,tau,K,N,L,P0T): # cf - characteristic function as a functon, in the book denoted as \varphi # CP - C for call and P for put # S0 - Initial stock price # tau - time to maturity # K - list of strikes # N - Number of expansion terms # L - size of truncation domain (typ.:L=8 or L=10) # P0T - zero-coupon bond for maturity T. # reshape K to a column vector if K is not np.array: K = np.array(K).reshape([len(K),1]) #assigning i=sqrt(-1) i = np.complex(0.0,1.0) x0 = np.log(S0 / K) # truncation domain a = 0.0 - L * np.sqrt(tau) b = 0.0 + L * np.sqrt(tau) # sumation from k = 0 to k=N-1 k = np.linspace(0,N-1,N).reshape([N,1]) u = k * np.pi / (b - a) # Determine coefficients for Put Prices H_k = CallPutCoefficients(OptionType.PUT,a,b,k) mat = np.exp(i * np.outer((x0 - a) , u)) temp = cf(u) * H_k temp[0] = 0.5 * temp[0] value = K * np.real(mat.dot(temp)) # we use call-put parity for call options if CP == OptionType.CALL: value = value + S0 - K * P0T return value # Determine coefficients for Put Prices def CallPutCoefficients(CP,a,b,k): if CP==OptionType.CALL: c = 0.0 d = b coef = Chi_Psi(a,b,c,d,k) Chi_k = coef["chi"] Psi_k = coef["psi"] if a < b and b < 0.0: H_k = np.zeros([len(k),1]) else: H_k = 2.0 / (b - a) * (Chi_k - Psi_k) elif CP==OptionType.PUT: c = a d = 0.0 coef = Chi_Psi(a,b,c,d,k) Chi_k = coef["chi"] Psi_k = coef["psi"] H_k = 2.0 / (b - a) * (- Chi_k + Psi_k) return H_k def Chi_Psi(a,b,c,d,k): psi = np.sin(k * np.pi * (d - a) / (b - a)) - np.sin(k * np.pi * (c - a)/(b - a)) psi[1:] = psi[1:] * (b - a) / (k[1:] * np.pi) psi[0] = d - c chi = 1.0 / (1.0 + np.power((k * np.pi / (b - a)) , 2.0)) expr1 = np.cos(k * np.pi * (d - a)/(b - a)) * np.exp(d) - np.cos(k * np.pi * (c - a) / (b - a)) * np.exp(c) expr2 = k * np.pi / (b - a) * np.sin(k * np.pi * (d - a) / (b - a)) - k * np.pi / (b - a) * np.sin(k * np.pi * (c - a) / (b - a)) * np.exp(c) chi = chi * (expr1 + expr2) value = {"chi":chi,"psi":psi } return value def EUOptionPriceFromMCPathsGeneralizedStochIR(CP,S,K,T,M): # S is a vector of Monte Carlo samples at T result = np.zeros([len(K),1]) if CP == OptionType.CALL: for (idx,k) in enumerate(K): result[idx] = np.mean(1.0/M*np.maximum(S-k,0.0)) elif CP == OptionType.PUT: for (idx,k) in enumerate(K): result[idx] = np.mean(1.0/M*np.maximum(k-S,0.0)) return result def CIR_Sample(NoOfPaths,kappa,gamma,vbar,s,t,v_s): delta = 4.0 *kappa*vbar/gamma/gamma c= 1.0/(4.0*kappa)*gamma*gamma*(1.0-np.exp(-kappa*(t-s))) kappaBar = 4.0*kappa*v_s*np.exp(-kappa*(t-s))/(gamma*gamma*(1.0-np.exp(-kappa*(t-s)))) sample = c* np.random.noncentral_chisquare(delta,kappaBar,NoOfPaths) return sample def GeneratePathsHestonHW_AES(NoOfPaths,NoOfSteps,P0T,T,S_0,kappa,gamma,rhoxr,rhoxv,vbar,v0,lambd,eta): # time-step needed for differentiation dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) # Initial interest rate is a forward rate at time t->0 r0 = f0T(0.00001) theta = lambda t: 1.0/lambd * (f0T(t+dt)-f0T(t-dt))/(2.0*dt) + f0T(t) + eta*eta/(2.0*lambd*lambd)*(1.0-np.exp(-2.0*lambd*t)) Z1 = np.random.normal(0.0,1.0,[NoOfPaths,NoOfSteps]) Z2 = np.random.normal(0.0,1.0,[NoOfPaths,NoOfSteps]) Z3 = np.random.normal(0.0,1.0,[NoOfPaths,NoOfSteps]) W1 = np.zeros([NoOfPaths, NoOfSteps+1]) W2 = np.zeros([NoOfPaths, NoOfSteps+1]) W3 = np.zeros([NoOfPaths, NoOfSteps+1]) V = np.zeros([NoOfPaths, NoOfSteps+1]) X = np.zeros([NoOfPaths, NoOfSteps+1]) R = np.zeros([NoOfPaths, NoOfSteps+1]) M_t = np.ones([NoOfPaths,NoOfSteps+1]) R[:,0]=r0 V[:,0]=v0 X[:,0]=np.log(S_0) time = np.zeros([NoOfSteps+1]) dt = T / float(NoOfSteps) for i in range(0,NoOfSteps): # making sure that samples from normal have mean 0 and variance 1 if NoOfPaths > 1: Z1[:,i] = (Z1[:,i] - np.mean(Z1[:,i])) / np.std(Z1[:,i]) Z2[:,i] = (Z2[:,i] - np.mean(Z2[:,i])) / np.std(Z2[:,i]) Z3[:,i] = (Z3[:,i] - np.mean(Z3[:,i])) / np.std(Z3[:,i]) W1[:,i+1] = W1[:,i] + np.power(dt, 0.5)*Z1[:,i] W2[:,i+1] = W2[:,i] + np.power(dt, 0.5)*Z2[:,i] W3[:,i+1] = W3[:,i] + np.power(dt, 0.5)*Z3[:,i] R[:,i+1] = R[:,i] + lambd*(theta(time[i]) - R[:,i]) * dt + eta* (W1[:,i+1]-W1[:,i]) M_t[:,i+1] = M_t[:,i] * np.exp(0.5*(R[:,i+1] + R[:,i])*dt) # Exact samles for the variance process V[:,i+1] = CIR_Sample(NoOfPaths,kappa,gamma,vbar,0,dt,V[:,i]) k0 = -rhoxv /gamma * kappa*vbar*dt k2 = rhoxv/gamma k1 = kappa*k2 -0.5 k3 = np.sqrt(1.0-rhoxr*rhoxr - rhoxv*rhoxv) X[:,i+1] = X[:,i] + k0 + (k1*dt - k2)*V[:,i] + R[:,i]*dt + k2*V[:,i+1]+\ + np.sqrt(V[:,i]*dt)*(rhoxr*Z1[:,i] + k3 * Z3[:,i]) # Moment matching component, i.e.: ensure that E(S(T)/M(T))= S0 a = S_0 / np.mean(np.exp(X[:,i+1])/M_t[:,i+1]) X[:,i+1] = X[:,i+1] + np.log(a) time[i+1] = time[i] +dt #Compute exponent S = np.exp(X) paths = {"time":time,"S":S,"R":R,"M_t":M_t} return paths def GeneratePathsHestonHWEuler(NoOfPaths,NoOfSteps,P0T,T,S_0,kappa,gamma,rhoxr,rhoxv,vbar,v0,lambd,eta): # time-step needed for differentiation dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) # Initial interest rate is a forward rate at time t->0 r0 = f0T(0.00001) theta = lambda t: 1.0/lambd * (f0T(t+dt)-f0T(t-dt))/(2.0*dt) + f0T(t) + eta*eta/(2.0*lambd*lambd)*(1.0-np.exp(-2.0*lambd*t)) Z1 = np.random.normal(0.0,1.0,[NoOfPaths,NoOfSteps]) Z2 = np.random.normal(0.0,1.0,[NoOfPaths,NoOfSteps]) Z3 = np.random.normal(0.0,1.0,[NoOfPaths,NoOfSteps]) W1 = np.zeros([NoOfPaths, NoOfSteps+1]) W2 = np.zeros([NoOfPaths, NoOfSteps+1]) W3 = np.zeros([NoOfPaths, NoOfSteps+1]) V = np.zeros([NoOfPaths, NoOfSteps+1]) X = np.zeros([NoOfPaths, NoOfSteps+1]) R = np.zeros([NoOfPaths, NoOfSteps+1]) M_t = np.ones([NoOfPaths,NoOfSteps+1]) R[:,0]=r0 V[:,0]=v0 X[:,0]=np.log(S_0) time = np.zeros([NoOfSteps+1]) dt = T / float(NoOfSteps) for i in range(0,NoOfSteps): # making sure that samples from normal have mean 0 and variance 1 if NoOfPaths > 1: Z1[:,i] = (Z1[:,i] - np.mean(Z1[:,i])) / np.std(Z1[:,i]) Z2[:,i] = (Z2[:,i] - np.mean(Z2[:,i])) / np.std(Z2[:,i]) Z3[:,i] = (Z3[:,i] - np.mean(Z3[:,i])) / np.std(Z3[:,i]) W1[:,i+1] = W1[:,i] + np.power(dt, 0.5)*Z1[:,i] W2[:,i+1] = W2[:,i] + np.power(dt, 0.5)*Z2[:,i] W3[:,i+1] = W3[:,i] + np.power(dt, 0.5)*Z3[:,i] # Truncated boundary condition R[:,i+1] = R[:,i] + lambd*(theta(time[i]) - R[:,i]) * dt + eta* (W1[:,i+1]-W1[:,i]) M_t[:,i+1] = M_t[:,i] * np.exp(0.5*(R[:,i+1] + R[:,i])*dt) V[:,i+1] = V[:,i] + kappa*(vbar - V[:,i]) * dt + gamma* np.sqrt(V[:,i]) * (W2[:,i+1]-W2[:,i]) V[:,i+1] = np.maximum(V[:,i+1],0.0) term1 = rhoxr * (W1[:,i+1]-W1[:,i]) + rhoxv * (W2[:,i+1]-W2[:,i]) \ + np.sqrt(1.0-rhoxr*rhoxr-rhoxv*rhoxv)* (W3[:,i+1]-W3[:,i]) X[:,i+1] = X[:,i] + (R[:,i] - 0.5*V[:,i])*dt + np.sqrt(V[:,i])*term1 time[i+1] = time[i] +dt # Moment matching component, i.e.: ensure that E(S(T)/M(T))= S0 a = S_0 / np.mean(np.exp(X[:,i+1])/M_t[:,i+1]) X[:,i+1] = X[:,i+1] + np.log(a) #Compute exponent S = np.exp(X) paths = {"time":time,"S":S,"R":R,"M_t":M_t} return paths # Exact expectation E(sqrt(V(t))) def meanSqrtV_3(kappa,v0,vbar,gamma): delta = 4.0 *kappa*vbar/gamma/gamma c= lambda t: 1.0/(4.0*kappa)*gamma*gamma*(1.0-np.exp(-kappa*(t))) kappaBar = lambda t: 4.0*kappa*v0*np.exp(-kappa*t)/(gamma*gamma*(1.0-np.exp(-kappa*t))) temp1 = lambda t: np.sqrt(2.0*c(t))* sp.gamma((1.0+delta)/2.0)/sp.gamma(delta/2.0)*sp.hyp1f1(-0.5,delta/2.0,-kappaBar(t)/2.0) return temp1 def C_H1HW(u,tau,lambd): i = np.complex(0.0,1.0) C = (i*u - 1.0)/lambd * (1-np.exp(-lambd*tau)) return C def D_H1HW(u,tau,kappa,gamma,rhoxv): i = np.complex(0.0,1.0) D1 = np.sqrt(np.power(kappa-gamma*rhoxv*i*u,2)+(u*u+i*u)*gamma*gamma) g = (kappa-gamma*rhoxv*i*u-D1)/(kappa-gamma*rhoxv*i*u+D1) C = (1.0-np.exp(-D1*tau))/(gamma*gamma*(1.0-g*np.exp(-D1*tau)))\ *(kappa-gamma*rhoxv*i*u-D1) return C def A_H1HW(u,tau,P0T,lambd,eta,kappa,gamma,vbar,v0,rhoxv,rhoxr): i = np.complex(0.0,1.0) D1 = np.sqrt(np.power(kappa-gamma*rhoxv*i*u,2)+(u*u+i*u)*gamma*gamma) g = (kappa-gamma*rhoxv*i*u-D1)/(kappa-gamma*rhoxv*i*u+D1) # function theta(t) dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2.0*dt) theta = lambda t: 1.0/lambd * (f0T(t+dt)-f0T(t-dt))/(2.0*dt) + f0T(t) + eta*eta/(2.0*lambd*lambd)*(1.0-np.exp(-2.0*lambd*t)) # Integration in the function I_1 N = 500 z = np.linspace(0,tau-1e-10,N) f1 = (1.0-np.exp(-lambd*z))*theta(tau-z) value1 = integrate.trapz(f1,z) # Note that I_1_adj also allows for theta to be time-dependent # therefore it is not exactly the same as given in the book I_1_adj = (i*u-1.0) * value1 I_2 = tau/(gamma**2.0) *(kappa-gamma*rhoxv*i*u-D1) - 2.0/(gamma**2.0)*np.log((1.0-g*np.exp(-D1*tau))/(1.0-g)) I_3 = 1.0/(2.0*np.power(lambd,3.0))* np.power(i+u,2.0)*(3.0+np.exp(-2.0*lambd*tau)-4.0*np.exp(-lambd*tau)-2.0*lambd*tau) meanSqrtV = meanSqrtV_3(kappa,v0,vbar,gamma) f2 = meanSqrtV(tau-z)*(1.0-np.exp(-lambd*z)) value2 = integrate.trapz(f2,z) I_4 = -1.0/lambd * (i*u+u**2.0)*value2 return I_1_adj + kappa*vbar*I_2 + 0.5*eta**2.0*I_3+eta*rhoxr*I_4 def ChFH1HWModel(P0T,lambd,eta,tau,kappa,gamma,vbar,v0,rhoxv, rhoxr): # Determine initial interest rate r(0) dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2.0*dt) r0 =f0T(0.00001) C = lambda u: C_H1HW(u,tau,lambd) D = lambda u: D_H1HW(u,tau,kappa,gamma,rhoxv) A = lambda u: A_H1HW(u,tau,P0T,lambd,eta,kappa,gamma,vbar,v0,rhoxv,rhoxr) cf = lambda u: np.exp(A(u) + C(u)*r0 + D(u)*v0 ) return cf def mainCalculation(): CP = OptionType.CALL NoOfPaths = 10000 NoOfSteps = 500 # HW model settings lambd = 1.12 eta = 0.01 S0 = 100.0 T = 15.0 r = 0.1 # Strike range K = np.linspace(.01,1.8*S0*np.exp(r*T),20) K = np.array(K).reshape([len(K),1]) # We define a ZCB curve (obtained from the market) P0T = lambda T: np.exp(-r*T) # Settings for the COS method N = 2000 L = 15 gamma = 0.3 vbar = 0.05 v0 = 0.02 rhoxr = 0.5 rhoxv =-0.8 kappa = 0.5 np.random.seed(1) paths = GeneratePathsHestonHWEuler(NoOfPaths,NoOfSteps,P0T,T,S0,kappa,gamma,rhoxr,rhoxv,vbar,v0,lambd,eta) S = paths["S"] M_t = paths["M_t"] print(np.mean(S[:,-1]/M_t[:,-1])) valueOptMC= EUOptionPriceFromMCPathsGeneralizedStochIR(CP,S[:,-1],K,T,M_t[:,-1]) np.random.seed(1) pathsExact = GeneratePathsHestonHW_AES(NoOfPaths,NoOfSteps,P0T,T,S0,kappa,gamma,rhoxr,rhoxv,vbar,v0,lambd,eta) S_ex = pathsExact["S"] M_t_ex = pathsExact["M_t"] valueOptMC_ex= EUOptionPriceFromMCPathsGeneralizedStochIR(CP,S_ex[:,-1],K,T,M_t_ex[:,-1]) print(np.mean(S_ex[:,-1]/M_t_ex[:,-1])) plt.figure(1) plt.plot(K,valueOptMC) plt.plot(K,valueOptMC_ex,'.k') plt.ylim([0.0,110.0]) # The COS method cf2 = ChFH1HWModel(P0T,lambd,eta,T,kappa,gamma,vbar,v0,rhoxv, rhoxr) u=np.array([1.0,2.0,3.0]) print(cf2(u)) valCOS = CallPutOptionPriceCOSMthd_StochIR(cf2, CP, S0, T, K, N, L,P0T(T)) plt.plot(K,valCOS,'--r') plt.legend(['Euler','AES','COS']) plt.grid() plt.xlabel('strike, K') plt.ylabel('EU Option Value, K') print("Value from the COS method:") print(valCOS) mainCalculation() ================================================ FILE: Lecture 09-Hybrid Models and Stochastic Interest Rates/Materials/SZHW_ImpliedVolatilities.py ================================================ #%% """ Created on July 05 2021 The SZHW model and implied volatilities This code is purely educational and comes from "Financial Engineering" course by L.A. Grzelak The course is based on the book “Mathematical Modeling and Computation in Finance: With Exercises and Python and MATLAB Computer Codes”, by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing Europe Ltd, 2019. @author: Lech A. Grzelak """ import numpy as np import matplotlib.pyplot as plt import scipy.integrate as integrate import scipy.stats as st import enum import scipy.optimize as optimize # set i= imaginary number i = np.complex(0.0,1.0) # time-step needed for differentiation dt = 0.0001 # This class defines puts and calls class OptionType(enum.Enum): CALL = 1.0 PUT = -1.0 def CallPutOptionPriceCOSMthd_StochIR(cf,CP,S0,tau,K,N,L,P0T): # cf - characteristic function as a functon, in the book denoted as \varphi # CP - C for call and P for put # S0 - Initial stock price # tau - time to maturity # K - list of strikes # N - Number of expansion terms # L - size of truncation domain (typ.:L=8 or L=10) # P0T - zero-coupon bond for maturity T. # reshape K to a column vector if K is not np.array: K = np.array(K).reshape([len(K),1]) #assigning i=sqrt(-1) i = np.complex(0.0,1.0) x0 = np.log(S0 / K) # truncation domain a = 0.0 - L * np.sqrt(tau) b = 0.0 + L * np.sqrt(tau) # sumation from k = 0 to k=N-1 k = np.linspace(0,N-1,N).reshape([N,1]) u = k * np.pi / (b - a) # Determine coefficients for Put Prices H_k = CallPutCoefficients(OptionType.PUT,a,b,k) mat = np.exp(i * np.outer((x0 - a) , u)) temp = cf(u) * H_k temp[0] = 0.5 * temp[0] value = K * np.real(mat.dot(temp)) # we use call-put parity for call options if CP == OptionType.CALL: value = value + S0 - K * P0T return value # Determine coefficients for Put Prices def CallPutCoefficients(CP,a,b,k): if CP==OptionType.CALL: c = 0.0 d = b coef = Chi_Psi(a,b,c,d,k) Chi_k = coef["chi"] Psi_k = coef["psi"] if a < b and b < 0.0: H_k = np.zeros([len(k),1]) else: H_k = 2.0 / (b - a) * (Chi_k - Psi_k) elif CP==OptionType.PUT: c = a d = 0.0 coef = Chi_Psi(a,b,c,d,k) Chi_k = coef["chi"] Psi_k = coef["psi"] H_k = 2.0 / (b - a) * (- Chi_k + Psi_k) return H_k def Chi_Psi(a,b,c,d,k): psi = np.sin(k * np.pi * (d - a) / (b - a)) - np.sin(k * np.pi * (c - a)/(b - a)) psi[1:] = psi[1:] * (b - a) / (k[1:] * np.pi) psi[0] = d - c chi = 1.0 / (1.0 + np.power((k * np.pi / (b - a)) , 2.0)) expr1 = np.cos(k * np.pi * (d - a)/(b - a)) * np.exp(d) - np.cos(k * np.pi * (c - a) / (b - a)) * np.exp(c) expr2 = k * np.pi / (b - a) * np.sin(k * np.pi * (d - a) / (b - a)) - k * np.pi / (b - a) * np.sin(k * np.pi * (c - a) / (b - a)) * np.exp(c) chi = chi * (expr1 + expr2) value = {"chi":chi,"psi":psi } return value # Black-Scholes Call option price def BS_Call_Put_Option_Price(CP,S_0,K,sigma,tau,r): if K is list: K = np.array(K).reshape([len(K),1]) d1 = (np.log(S_0 / K) + (r + 0.5 * np.power(sigma,2.0)) * tau) / (sigma * np.sqrt(tau)) d2 = d1 - sigma * np.sqrt(tau) if CP == OptionType.CALL: value = st.norm.cdf(d1) * S_0 - st.norm.cdf(d2) * K * np.exp(-r * tau) elif CP == OptionType.PUT: value = st.norm.cdf(-d2) * K * np.exp(-r * tau) - st.norm.cdf(-d1)*S_0 return value # Implied volatility method def ImpliedVolatilityBlack76(CP,marketPrice,K,T,S_0): # To determine initial volatility we interpolate define a grid for sigma # and interpolate on the inverse sigmaGrid = np.linspace(0.0,5.0,5000) optPriceGrid = BS_Call_Put_Option_Price(CP,S_0,K,sigmaGrid,T,0.0) sigmaInitial = np.interp(marketPrice,optPriceGrid,sigmaGrid) print("Strike = {0}".format(K)) print("Initial volatility = {0}".format(sigmaInitial)) # Use determined input for the local-search (final tuning) func = lambda sigma: np.power(BS_Call_Put_Option_Price(CP,S_0,K,sigma,T,0.0) - marketPrice, 1.0) impliedVol = optimize.newton(func, sigmaInitial, tol=1e-11) print("Final volatility = {0}".format(impliedVol)) if impliedVol > 2.0: impliedVol = 0.0 return impliedVol def C(u,tau,lambd): i = complex(0,1) return 1.0/lambd*(i*u-1.0)*(1.0-np.exp(-lambd*tau)) def D(u,tau,kappa,Rxsigma,gamma): i=np.complex(0.0,1.0) a_0=-1.0/2.0*u*(i+u) a_1=2.0*(gamma*Rxsigma*i*u-kappa) a_2=2.0*gamma*gamma d=np.sqrt(a_1*a_1-4.0*a_0*a_2) g=(-a_1-d)/(-a_1+d) return (-a_1-d)/(2.0*a_2*(1.0-g*np.exp(-d*tau)))*(1.0-np.exp(-d*tau)) def E(u,tau,lambd,gamma,Rxsigma,Rrsigma,Rxr,eta,kappa,sigmabar): i=np.complex(0.0,1.0) a_0=-1.0/2.0*u*(i+u) a_1=2.0*(gamma*Rxsigma*i*u-kappa) a_2=2*gamma*gamma d =np.sqrt(a_1*a_1-4.0*a_0*a_2) g =(-a_1-d)/(-a_1+d) c_1=gamma*Rxsigma*i*u-kappa-1.0/2.0*(a_1+d) f_1=1.0/c_1*(1.0-np.exp(-c_1*tau))+1.0/(c_1+d)*(np.exp(-(c_1+d)*tau)-1.0) f_2=1.0/c_1*(1.0-np.exp(-c_1*tau))+1.0/(c_1+lambd)*(np.exp(-(c_1+lambd)*tau)-1.0) f_3=(np.exp(-(c_1+d)*tau)-1.0)/(c_1+d)+(1.0-np.exp(-(c_1+d+lambd)*tau))/(c_1+d+lambd) f_4=1.0/c_1-1.0/(c_1+d)-1.0/(c_1+lambd)+1.0/(c_1+d+lambd) f_5=np.exp(-(c_1+d+lambd)*tau)*(np.exp(lambd*tau)*(1.0/(c_1+d)-np.exp(d*tau)/c_1)+np.exp(d*tau)/(c_1+lambd)-1.0/(c_1+d+lambd)) I_1=kappa*sigmabar/a_2*(-a_1-d)*f_1 I_2=eta*Rxr*i*u*(i*u-1.0)/lambd*(f_2+g*f_3) I_3=-Rrsigma*eta*gamma/(lambd*a_2)*(a_1+d)*(i*u-1)*(f_4+f_5) return np.exp(c_1*tau)*1.0/(1.0-g*np.exp(-d*tau))*(I_1+I_2+I_3) def A(u,tau,eta,lambd,Rxsigma,Rrsigma,Rxr,gamma,kappa,sigmabar): i=np.complex(0.0,1.0) a_0=-1.0/2.0*u*(i+u) a_1=2.0*(gamma*Rxsigma*i*u-kappa) a_2=2.0*gamma*gamma d =np.sqrt(a_1*a_1-4.0*a_0*a_2) g =(-a_1-d)/(-a_1+d) f_6=eta*eta/(4.0*np.power(lambd,3.0))*np.power(i+u,2.0)*(3.0+np.exp(-2.0*lambd*tau)-4.0*np.exp(-lambd*tau)-2.0*lambd*tau) A_1=1.0/4.0*((-a_1-d)*tau-2.0*np.log((1-g*np.exp(-d*tau))/(1.0-g)))+f_6 # Integration in the function A(u,tau) value=np.zeros([len(u),1],dtype=np.complex_) N = 500 arg=np.linspace(0,tau,N) for k in range(0,len(u)): E_val=E(u[k],arg,lambd,gamma,Rxsigma,Rrsigma,Rxr,eta,kappa,sigmabar) C_val=C(u[k],arg,lambd) f=(kappa*sigmabar+1.0/2.0*gamma*gamma*E_val+gamma*eta*Rrsigma*C_val)*E_val value1 =integrate.trapz(np.real(f),arg) value2 =integrate.trapz(np.imag(f),arg) value[k]=(value1 + value2*i)#np.complex(value1,value2) return value + A_1 def ChFSZHW(u,P0T,sigma0,tau,lambd,gamma, Rxsigma,Rrsigma,Rxr,eta,kappa,sigmabar): v_D = D(u,tau,kappa,Rxsigma,gamma) v_E = E(u,tau,lambd,gamma,Rxsigma,Rrsigma,Rxr,eta,kappa,sigmabar) v_A = A(u,tau,eta,lambd,Rxsigma,Rrsigma,Rxr,gamma,kappa,sigmabar) v_0 = sigma0*sigma0 hlp = eta*eta/(2.0*lambd*lambd)*(tau+2.0/lambd*(np.exp(-lambd*tau)-1.0)-1.0/(2.0*lambd)*(np.exp(-2.0*lambd*tau)-1.0)) correction = (i*u-1.0)*(np.log(1/P0T(tau))+hlp) cf = np.exp(v_0*v_D + sigma0*v_E + v_A + correction) return cf.tolist() def ChFBSHW(u, T, P0T, lambd, eta, rho, sigma): i = np.complex(0.0,1.0) f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) # Initial interest rate is a forward rate at time t->0 r0 = f0T(0.00001) theta = lambda t: 1.0/lambd * (f0T(t+dt)-f0T(t-dt))/(2.0*dt) + f0T(t) + eta*eta/(2.0*lambd*lambd)*(1.0-np.exp(-2.0*lambd*t)) C = lambda u,tau: 1.0/lambd*(i*u-1.0)*(1.0-np.exp(-lambd*tau)) # define a grid for the numerical integration of function theta zGrid = np.linspace(0.0,T,2500) term1 = lambda u: 0.5*sigma*sigma *i*u*(i*u-1.0)*T term2 = lambda u: i*u*rho*sigma*eta/lambd*(i*u-1.0)*(T+1.0/lambd *(np.exp(-lambd*T)-1.0)) term3 = lambda u: eta*eta/(4.0*np.power(lambd,3.0))*np.power(i+u,2.0)*(3.0+np.exp(-2.0*lambd*T)-4.0*np.exp(-lambd*T)-2.0*lambd*T) term4 = lambda u: lambd*integrate.trapz(theta(T-zGrid)*C(u,zGrid), zGrid) A= lambda u: term1(u) + term2(u) + term3(u) + term4(u) # Note that we don't include B(u)*x0 term as it is included in the COS method cf = lambda u : np.exp(A(u) + C(u,T)*r0 ) # Iterate over all u and collect the ChF, iteration is necessary due to the integration over in term4 cfV = [] for ui in u: cfV.append(cf(ui)) return cfV def mainCalculation(): CP = OptionType.CALL # HW model settings lambd = 0.425 eta = 0.1 S0 = 100 T = 5.0 # The SZHW model sigma0 = 0.1 gamma = 0.11 Rrsigma = 0.32 Rxsigma = -0.42 Rxr = 0.3 kappa = 0.4 sigmabar= 0.05 # Strike range K = np.linspace(40,200.0,20) K = np.array(K).reshape([len(K),1]) # We define a ZCB curve (obtained from the market) P0T = lambda T: np.exp(-0.025*T) # Forward stock frwdStock = S0 / P0T(T) # Settings for the COS method N = 2000 L = 10 # effect of gamma plt.figure(1) plt.grid() plt.xlabel('strike, K') plt.ylabel('implied volatility') gammaV = [0.1, 0.2, 0.3, 0.4] legend = [] for gammaTemp in gammaV: # Evaluate the SZHW model cf = lambda u: ChFSZHW(u,P0T,sigma0,T,lambd,gammaTemp,Rxsigma,Rrsigma,Rxr,eta,kappa,sigmabar) #cf = lambda u: ChFBSHW(u, T, P0T, lambd, eta, -0.7, 0.1) # The COS method valCOS = CallPutOptionPriceCOSMthd_StochIR(cf, CP, S0, T, K, N, L,P0T(T)) valCOSFrwd = valCOS/P0T(T) # Implied volatilities IV =np.zeros([len(K),1]) for idx in range(0,len(K)): IV[idx] = ImpliedVolatilityBlack76(CP,valCOSFrwd[idx],K[idx],T,frwdStock) plt.plot(K,IV*100.0) legend.append('gamma={0}'.format(gammaTemp)) plt.legend(legend) # effect of kappa plt.figure(2) plt.grid() plt.xlabel('strike, K') plt.ylabel('implied volatility') kappaV = [0.05, 0.2, 0.3, 0.4] legend = [] for kappaTemp in kappaV: # Evaluate the SZHW model cf = lambda u: ChFSZHW(u,P0T,sigma0,T,lambd,gamma,Rxsigma,Rrsigma,Rxr,eta,kappaTemp,sigmabar) # The COS method valCOS = CallPutOptionPriceCOSMthd_StochIR(cf, CP, S0, T, K, N, L,P0T(T)) valCOSFrwd = valCOS/P0T(T) # Implied volatilities IV =np.zeros([len(K),1]) for idx in range(0,len(K)): IV[idx] = ImpliedVolatilityBlack76(CP,valCOSFrwd[idx],K[idx],T,frwdStock) plt.plot(K,IV*100.0) legend.append('kappa={0}'.format(kappaTemp)) plt.legend(legend) # effect of rhoxsigma plt.figure(3) plt.grid() plt.xlabel('strike, K') plt.ylabel('implied volatility') RxsigmaV = [-0.75, -0.25, 0.25, 0.75] legend = [] for RxsigmaTemp in RxsigmaV: # Evaluate the SZHW model cf = lambda u: ChFSZHW(u,P0T,sigma0,T,lambd,gamma,RxsigmaTemp,Rrsigma,Rxr,eta,kappa,sigmabar) # The COS method valCOS = CallPutOptionPriceCOSMthd_StochIR(cf, CP, S0, T, K, N, L,P0T(T)) valCOSFrwd = valCOS/P0T(T) # Implied volatilities IV =np.zeros([len(K),1]) for idx in range(0,len(K)): IV[idx] = ImpliedVolatilityBlack76(CP,valCOSFrwd[idx],K[idx],T,frwdStock) plt.plot(K,IV*100.0) legend.append('Rxsigma={0}'.format(RxsigmaTemp)) plt.legend(legend) #effect of sigmabar plt.figure(4) plt.grid() plt.xlabel('strike, K') plt.ylabel('implied volatility') sigmabarV = [0.1, 0.2, 0.3, 0.4] legend = [] for sigmabarTemp in sigmabarV: # Evaluate the SZHW model cf = lambda u: ChFSZHW(u,P0T,sigma0,T,lambd,gamma,Rxsigma,Rrsigma,Rxr,eta,kappa,sigmabarTemp) # The COS method valCOS = CallPutOptionPriceCOSMthd_StochIR(cf, CP, S0, T, K, N, L,P0T(T)) valCOSFrwd = valCOS/P0T(T) # Implied volatilities IV =np.zeros([len(K),1]) for idx in range(0,len(K)): IV[idx] = ImpliedVolatilityBlack76(CP,valCOSFrwd[idx],K[idx],T,frwdStock) plt.plot(K,IV*100.0) legend.append('sigmabar={0}'.format(sigmabarTemp)) plt.legend(legend) mainCalculation() ================================================ FILE: Lecture 09-Hybrid Models and Stochastic Interest Rates/Materials/SZHW_MonteCarlo_DiversificationProduct.py ================================================ #%% """ Created on July 05 2021 The SZHW model and pricing of diversification product This code is purely educational and comes from "Financial Engineering" course by L.A. Grzelak The course is based on the book “Mathematical Modeling and Computation in Finance: With Exercises and Python and MATLAB Computer Codes”, by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing Europe Ltd, 2019. @author: Lech A. Grzelak """ import numpy as np import matplotlib.pyplot as plt import scipy.integrate as integrate import enum # set i= imaginary number i = np.complex(0.0,1.0) # time-step needed for differentiation dt = 0.0001 # This class defines puts and calls class OptionType(enum.Enum): CALL = 1.0 PUT = -1.0 def HW_theta(lambd,eta,P0T): dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) theta = lambda t: 1.0/lambd * (f0T(t+dt)-f0T(t-dt))/(2.0*dt) + f0T(t) + eta*eta/(2.0*lambd*lambd)*(1.0-np.exp(-2.0*lambd*t)) return theta def HW_A(lambd,eta,P0T,T1,T2): tau = T2-T1 zGrid = np.linspace(0.0,tau,250) B_r = lambda tau: 1.0/lambd * (np.exp(-lambd *tau)-1.0) theta = HW_theta(lambd,eta,P0T) temp1 = lambd * integrate.trapz(theta(T2-zGrid)*B_r(zGrid),zGrid) temp2 = eta*eta/(4.0*np.power(lambd,3.0)) * (np.exp(-2.0*lambd*tau)*(4*np.exp(lambd*tau)-1.0) -3.0) + eta*eta*tau/(2.0*lambd*lambd) return temp1 + temp2 def HW_B(lambd,eta,T1,T2): return 1.0/lambd *(np.exp(-lambd*(T2-T1))-1.0) def HW_ZCB(lambd,eta,P0T,T1,T2,rT1): B_r = HW_B(lambd,eta,T1,T2) A_r = HW_A(lambd,eta,P0T,T1,T2) return np.exp(A_r + B_r *rT1) def EUOptionPriceFromMCPathsGeneralizedStochIR(CP,S,K,T,M): # S is a vector of Monte Carlo samples at T result = np.zeros([len(K),1]) if CP == OptionType.CALL: for (idx,k) in enumerate(K): result[idx] = np.mean(1.0/M*np.maximum(S-k,0.0)) elif CP == OptionType.PUT: for (idx,k) in enumerate(K): result[idx] = np.mean(1.0/M*np.maximum(k-S,0.0)) return result def GeneratePathsSZHWEuler(NoOfPaths,NoOfSteps,P0T,T,S0,sigma0,sigmabar,kappa,gamma,lambd,eta,Rxsigma,Rxr,Rsigmar): # time-step needed for differentiation dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) # Initial interest rate is a forward rate at time t->0 r0 = f0T(0.00001) theta = lambda t: 1.0/lambd * (f0T(t+dt)-f0T(t-dt))/(2.0*dt) + f0T(t) + eta*eta/(2.0*lambd*lambd)*(1.0-np.exp(-2.0*lambd*t)) # Empty containers for Brownian motion Wx = np.zeros([NoOfPaths, NoOfSteps+1]) Wsigma = np.zeros([NoOfPaths, NoOfSteps+1]) Wr = np.zeros([NoOfPaths, NoOfSteps+1]) Sigma = np.zeros([NoOfPaths, NoOfSteps+1]) X = np.zeros([NoOfPaths, NoOfSteps+1]) R = np.zeros([NoOfPaths, NoOfSteps+1]) M_t = np.ones([NoOfPaths,NoOfSteps+1]) R[:,0] = r0 Sigma[:,0] = sigma0 X[:,0] = np.log(S0) dt = T / float(NoOfSteps) cov = np.array([[1.0, Rxsigma,Rxr],[Rxsigma,1.0,Rsigmar], [Rxr,Rsigmar,1.0]]) time = np.zeros([NoOfSteps+1]) for i in range(0,NoOfSteps): Z = np.random.multivariate_normal([.0,.0,.0],cov,NoOfPaths) if NoOfPaths > 1: Z[:,0] = (Z[:,0] - np.mean(Z[:,0])) / np.std(Z[:,0]) Z[:,1] = (Z[:,1] - np.mean(Z[:,1])) / np.std(Z[:,1]) Z[:,2] = (Z[:,2] - np.mean(Z[:,2])) / np.std(Z[:,2]) Wx[:,i+1] = Wx[:,i] + np.power(dt, 0.5)*Z[:,0] Wsigma[:,i+1] = Wsigma[:,i] + np.power(dt, 0.5)*Z[:,1] Wr[:,i+1] = Wr[:,i] + np.power(dt, 0.5)*Z[:,2] # Euler discretization R[:,i+1] = R[:,i] + lambd*(theta(time[i]) - R[:,i])*dt + eta * (Wr[:,i+1]-Wr[:,i]) M_t[:,i+1] = M_t[:,i] * np.exp(0.5*(R[:,i+1] + R[:,i])*dt) Sigma[:,i+1] = Sigma[:,i] + kappa*(sigmabar - Sigma[:,i])*dt + gamma* (Wsigma[:,i+1]-Wsigma[:,i]) X[:,i+1] = X[:,i] + (R[:,i] - 0.5*Sigma[:,i]**2.0)*dt + Sigma[:,i] * (Wx[:,i+1]-Wx[:,i]) time[i+1] = time[i] +dt # Moment matching component, i.e.: ensure that E(S(T)/M(T))= S0 a = S0 / np.mean(np.exp(X[:,i+1])/M_t[:,i+1]) X[:,i+1] = X[:,i+1] + np.log(a) paths = {"time":time,"S":np.exp(X),"M_t":M_t,"R":R} return paths def DiversifcationPayoff(P0T,S_T,S0,r_T,M_T,T,T1,lambd,eta,omegaV): P_T_T1= HW_ZCB(lambd,eta,P0T,T,T1,r_T) P_0_T1= P0T(T1) value =np.zeros(omegaV.size) for (idx,omega) in enumerate(omegaV): payoff = omega * S_T/S0 + (1.0-omega) * P_T_T1/P_0_T1 value[idx] = np.mean(1/M_T*np.maximum(payoff,0.0)) return value def mainCalculation(): # HW model settings lambd = 1.12 eta = 0.02 S0 = 100.0 # Fixed mean reversion parameter kappa = 0.5 # Diversification product T = 9.0 T1 = 10.0 # We define a ZCB curve (obtained from the market) P0T = lambda T: np.exp(-0.033*T) # Range of the waiting factor omegaV= np.linspace(-3.0,3.0,50) # Monte Carlo setting NoOfPaths =5000 NoOfSteps = int(100*T) # The SZHW model parameters # The parameters can be obtained by running the calibration of the SZHW model and with # varying the correlation rhoxr. parameters=[{"Rxr":0.0,"sigmabar":0.167,"gamma":0.2,"Rxsigma":-0.850,"Rrsigma":-0.008,"kappa":0.5,"sigma0":0.035}, {"Rxr":-0.7,"sigmabar":0.137,"gamma":0.236,"Rxsigma":-0.381,"Rrsigma":-0.339,"kappa":0.5,"sigma0":0.084}, {"Rxr":0.7,"sigmabar":0.102,"gamma":0.211,"Rxsigma":-0.850,"Rrsigma":-0.340,"kappa":0.5,"sigma0":0.01}] legend = [] for (idx,par) in enumerate(parameters): sigma0 = par["sigma0"] gamma = par["gamma"] Rrsigma = par["Rrsigma"] Rxsigma = par["Rxsigma"] Rxr = par["Rxr"] sigmabar= par["sigmabar"] # Generate MC paths np.random.seed(1) paths = GeneratePathsSZHWEuler(NoOfPaths,NoOfSteps,P0T,T,S0,sigma0,sigmabar,kappa,gamma,lambd,eta,Rxsigma,Rxr,Rrsigma) S = paths["S"] M = paths["M_t"] R = paths["R"] S_T= S[:,-1] R_T= R[:,-1] M_T= M[:,-1] value_0 = DiversifcationPayoff(P0T,S_T,S0,R_T,M_T,T,T1,lambd,eta,omegaV) # reference with rho=0.0 if Rxr==0.0: refR0 = value_0 plt.figure(1) plt.plot(omegaV,value_0) legend.append('par={0}'.format(idx)) plt.figure(2) plt.plot(omegaV,value_0/refR0) plt.figure(1) plt.grid() plt.legend(legend) plt.figure(2) plt.grid() plt.legend(legend) mainCalculation() ================================================ FILE: Lecture 10-Foreign Exchange (FX) and Inflation/Materials/H1_HW_COS_vs_MC_FX.py ================================================ #%% """ Created on August 25 2021 The Heston Hull-White model used for pricing of European type of FX options using the COS method and comparisons with the Monte Carlo simulation. This code is purely educational and comes from "Financial Engineering" course by L.A. Grzelak The course is based on the book “Mathematical Modeling and Computation in Finance: With Exercises and Python and MATLAB Computer Codes”, by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing Europe Ltd, 2019. @author: Lech A. Grzelak """ import numpy as np import matplotlib.pyplot as plt import scipy.stats as st import scipy.special as sp import scipy.integrate as integrate import scipy.optimize as optimize import enum # This class defines puts and calls class OptionType(enum.Enum): CALL = 1.0 PUT = -1.0 # Black-Scholes Call option price def BS_Call_Put_Option_Price(CP,S_0,K,sigma,tau,r): if K is list: K = np.array(K).reshape([len(K),1]) d1 = (np.log(S_0 / K) + (r + 0.5 * np.power(sigma,2.0)) * tau) / (sigma * np.sqrt(tau)) d2 = d1 - sigma * np.sqrt(tau) if CP == OptionType.CALL: value = st.norm.cdf(d1) * S_0 - st.norm.cdf(d2) * K * np.exp(-r * tau) elif CP == OptionType.PUT: value = st.norm.cdf(-d2) * K * np.exp(-r * tau) - st.norm.cdf(-d1)*S_0 return value # Implied volatility method def ImpliedVolatilityBlack76(CP,marketPrice,K,T,S_0): # To determine initial volatility we interpolate define a grid for sigma # and interpolate on the inverse sigmaGrid = np.linspace(0.0,5.0,5000) optPriceGrid = BS_Call_Put_Option_Price(CP,S_0,K,sigmaGrid,T,0.0) sigmaInitial = np.interp(marketPrice,optPriceGrid,sigmaGrid) print("Strike = {0}".format(K)) print("Initial volatility = {0}".format(sigmaInitial)) # Use determined input for the local-search (final tuning) func = lambda sigma: np.power(BS_Call_Put_Option_Price(CP,S_0,K,sigma,T,0.0) - marketPrice, 1.0) impliedVol = optimize.newton(func, sigmaInitial, tol=1e-15) print("Final volatility = {0}".format(impliedVol)) if impliedVol > 2.0: impliedVol = 0.0 return impliedVol def CallPutOptionPriceCOSMthd_StochIR(cf,CP,S0,tau,K,N,L,P0T): # cf - characteristic function as a functon, in the book denoted as \varphi # CP - C for call and P for put # S0 - Initial stock price # tau - time to maturity # K - list of strikes # N - Number of expansion terms # L - size of truncation domain (typ.:L=8 or L=10) # P0T - zero-coupon bond for maturity T. # reshape K to a column vector if K is not np.array: K = np.array(K).reshape([len(K),1]) #assigning i=sqrt(-1) i = np.complex(0.0,1.0) x0 = np.log(S0 / K) # truncation domain a = 0.0 - L * np.sqrt(tau) b = 0.0 + L * np.sqrt(tau) # sumation from k = 0 to k=N-1 k = np.linspace(0,N-1,N).reshape([N,1]) u = k * np.pi / (b - a) # Determine coefficients for Put Prices H_k = CallPutCoefficients(OptionType.PUT,a,b,k) mat = np.exp(i * np.outer((x0 - a) , u)) temp = cf(u) * H_k temp[0] = 0.5 * temp[0] value = K * np.real(mat.dot(temp)) # we use call-put parity for call options if CP == OptionType.CALL: value = value + S0 - K * P0T return value # Determine coefficients for Put Prices def CallPutCoefficients(CP,a,b,k): if CP==OptionType.CALL: c = 0.0 d = b coef = Chi_Psi(a,b,c,d,k) Chi_k = coef["chi"] Psi_k = coef["psi"] if a < b and b < 0.0: H_k = np.zeros([len(k),1]) else: H_k = 2.0 / (b - a) * (Chi_k - Psi_k) elif CP==OptionType.PUT: c = a d = 0.0 coef = Chi_Psi(a,b,c,d,k) Chi_k = coef["chi"] Psi_k = coef["psi"] H_k = 2.0 / (b - a) * (- Chi_k + Psi_k) return H_k def Chi_Psi(a,b,c,d,k): psi = np.sin(k * np.pi * (d - a) / (b - a)) - np.sin(k * np.pi * (c - a)/(b - a)) psi[1:] = psi[1:] * (b - a) / (k[1:] * np.pi) psi[0] = d - c chi = 1.0 / (1.0 + np.power((k * np.pi / (b - a)) , 2.0)) expr1 = np.cos(k * np.pi * (d - a)/(b - a)) * np.exp(d) - np.cos(k * np.pi * (c - a) / (b - a)) * np.exp(c) expr2 = k * np.pi / (b - a) * np.sin(k * np.pi * (d - a) / (b - a)) - k * np.pi / (b - a) * np.sin(k * np.pi * (c - a) / (b - a)) * np.exp(c) chi = chi * (expr1 + expr2) value = {"chi":chi,"psi":psi } return value def EUOptionPriceFromMCPathsGeneralizedFXFrwd(CP,S,K): # S is a vector of Monte Carlo samples at T result = np.zeros([len(K),1]) if CP == OptionType.CALL: for (idx,k) in enumerate(K): result[idx] = np.mean(np.maximum(S-k,0.0)) elif CP == OptionType.PUT: for (idx,k) in enumerate(K): result[idx] = np.mean(np.maximum(k-S,0.0)) return result def GeneratePathsHHWFXHWEuler(NoOfPaths,NoOfSteps,T,frwdFX,v0,vbar,kappa,gamma,lambdd,lambdf,etad,etaf,rhoxv,rhoxrd,rhoxrf,rhovrd,rhovrf,rhordrf): Wx = np.zeros([NoOfPaths, NoOfSteps+1]) Wv = np.zeros([NoOfPaths, NoOfSteps+1]) Wrd = np.zeros([NoOfPaths, NoOfSteps+1]) Wrf = np.zeros([NoOfPaths, NoOfSteps+1]) V = np.zeros([NoOfPaths, NoOfSteps+1]) FX = np.zeros([NoOfPaths, NoOfSteps+1]) V[:,0] = v0 FX[:,0] = frwdFX dt = T / float(NoOfSteps) Bd = lambda t,T: 1.0/lambdd*(np.exp(-lambdd*(T-t))-1.0) Bf = lambda t,T: 1.0/lambdf*(np.exp(-lambdf*(T-t))-1.0) cov = np.array([[1.0, rhoxv,rhoxrd,rhoxrf],[rhoxv,1.0,rhovrd,rhovrf],\ [rhoxrd,rhovrd,1.0,rhordrf],[rhoxrf,rhovrf,rhordrf,1.0]]) time = np.zeros([NoOfSteps+1]) for i in range(0,NoOfSteps): Z = np.random.multivariate_normal([.0,.0,.0,.0],cov,NoOfPaths) if NoOfPaths > 1: Z[:,0] = (Z[:,0] - np.mean(Z[:,0])) / np.std(Z[:,0]) Z[:,1] = (Z[:,1] - np.mean(Z[:,1])) / np.std(Z[:,1]) Z[:,2] = (Z[:,2] - np.mean(Z[:,2])) / np.std(Z[:,2]) Z[:,3] = (Z[:,3] - np.mean(Z[:,3])) / np.std(Z[:,3]) Wx[:,i+1] = Wx[:,i] + np.power(dt, 0.5)*Z[:,0] Wv[:,i+1] = Wv[:,i] + np.power(dt, 0.5)*Z[:,1] Wrd[:,i+1] = Wrd[:,i] + np.power(dt, 0.5)*Z[:,2] Wrf[:,i+1] = Wrf[:,i] + np.power(dt, 0.5)*Z[:,3] # Variance process- Euler discretization V[:,i+1] = V[:,i] + kappa*(vbar - V[:,i])*dt \ + gamma*rhovrd*etad*Bd(time[i],T)*np.sqrt(V[:,i]) * dt \ + gamma* np.sqrt(V[:,i]) * (Wv[:,i+1]-Wv[:,i]) V[:,i+1] = np.maximum(V[:,i+1],0.0) # FX process under the forward measure FX[:,i+1] = FX[:,i] *(1.0 + np.sqrt(V[:,i])*(Wx[:,i+1]-Wx[:,i]) \ -etad*Bd(time[i],T)*(Wrd[:,i+1]-Wrd[:,i])\ +etaf*Bf(time[i],T)*(Wrf[:,i+1]-Wrf[:,i])) time[i+1] = time[i] +dt paths = {"time":time,"FX":FX} return paths # Exact expectation E(sqrt(V(t))) def meanSqrtV_3(kappa,v0,vbar,gamma): delta = 4.0 *kappa*vbar/gamma/gamma c= lambda t: 1.0/(4.0*kappa)*gamma*gamma*(1.0-np.exp(-kappa*(t))) kappaBar = lambda t: 4.0*kappa*v0*np.exp(-kappa*t)/(gamma*gamma*(1.0-np.exp(-kappa*t))) temp1 = lambda t: np.sqrt(2.0*c(t))* sp.gamma((1.0+delta)/2.0)/sp.gamma(delta/2.0)*sp.hyp1f1(-0.5,delta/2.0,-kappaBar(t)/2.0) return temp1 def C_H1HW_FX(u,tau,kappa,gamma,rhoxv): i = np.complex(0.0,1.0) D1 = np.sqrt(np.power(kappa-gamma*rhoxv*i*u,2.0)+(u*u+i*u)*gamma*gamma) g = (kappa-gamma*rhoxv*i*u-D1)/(kappa-gamma*rhoxv*i*u+D1) C = (1.0-np.exp(-D1*tau))/(gamma*gamma*(1.0-g*np.exp(-D1*tau)))\ *(kappa-gamma*rhoxv*i*u-D1) return C def ChFH1HW_FX(u,tau,gamma,Rxv,Rxrd,Rxrf,Rrdrf,Rvrd,Rvrf,lambdd,etad,lambdf,etaf,kappa,vBar,v0): i = np.complex(0.0,1.0) C = lambda u,tau: C_H1HW_FX(u,tau,kappa,gamma,Rxv) Bd = lambda t,T: 1.0/lambdd*(np.exp(-lambdd*(T-t))-1.0) Bf = lambda t,T: 1.0/lambdf*(np.exp(-lambdf*(T-t))-1.0) G = meanSqrtV_3(kappa,v0,vBar,gamma) zeta = lambda t: (Rxrd*etad*Bd(t,tau) - Rxrf*etaf*Bf(t,tau))*G(t) + \ Rrdrf*etad*etaf*Bd(t,tau)*Bf(t,tau) - 0.5*(etad**2.0*Bd(t,tau)**2.0+etaf**2.0*Bf(t,tau)**2.0) # Integration in the function A(u,tau) int1=np.zeros([len(u),1],dtype=np.complex_) N = 500 z=np.linspace(0.0+1e-10,tau-1e-10,N) temp1 =lambda z1: kappa*vBar + Rvrd*gamma*etad*G(tau-z1)*Bd(tau-z1,tau) temp2 =lambda z1, u: -Rvrd*gamma*etad*G(tau-z1)*Bd(tau-z1,tau)*i*u temp3 =lambda z1, u: Rvrf*gamma*etaf*G(tau-z1)*Bf(tau-z1,tau)*i*u f = lambda z1,u: (temp1(z1)+temp2(z1,u)+temp3(z1,u))*C(u,z1) value1 =integrate.trapz(np.real(f(z,u)),z).reshape(u.size,1) value2 =integrate.trapz(np.imag(f(z,u)),z).reshape(u.size,1) int1=(value1 + value2*i) """ for k in range(0,len(u)): temp1 = kappa*vBar + Rvrd*gamma*etad*G(tau-z)*Bd(tau-z,tau) temp2 = -Rvrd*gamma*etad*G(tau-z)*Bd(tau-z,tau)*i*u[k] temp3 = Rvrf*gamma*etaf*G(tau-z)*Bf(tau-z,tau)*i*u[k] f = (temp1+temp2+temp3)*C(u[k],z) value1 =integrate.trapz(np.real(f),z) value2 =integrate.trapz(np.imag(f),z) int1[k]=(value1 + value2*i) """ int2 = (u**2.0 + i*u)*integrate.trapz(zeta(tau-z),z) A = int1 + int2 cf = np.exp(A + v0*C(u,tau)) return cf def GenerateStrikes(frwd,Ti): c_n = np.array([-1.5, -1.0, -0.5,0.0, 0.5, 1.0, 1.5]) return frwd * np.exp(0.1 * c_n * np.sqrt(Ti)) def mainCalculation(): CP = OptionType.CALL T = 5.0 NoOfPaths = 1000 NoOfSteps = (int)(T*50) # Settings for the COS method N = 500 L = 8 # Market Settings P0Td = lambda t: np.exp(-0.02*t) P0Tf = lambda t: np.exp(-0.05*t) y0 = 1.35 frwdFX = y0*P0Tf(T)/P0Td(T) kappa = 0.5 gamma = 0.3 vbar = 0.1 v0 = 0.1 # HW model settings lambdd = 0.01 lambdf = 0.05 etad = 0.007 etaf = 0.012 # correlations Rxv = -0.4 Rxrd = -0.15 Rxrf = -0.15 Rvrd = 0.3 Rvrf = 0.3 Rrdrf = 0.25 # Strikes K = GenerateStrikes(frwdFX,T) K = np.array(K).reshape([len(K),1]) # number of repeated simulations for different Monte Carlo seed SeedV = range(0,20) optMCM = np.zeros([len(SeedV),len(K)]) for (idx,seed) in enumerate(SeedV): print('Seed number = {0} out of {1}'.format(idx,len(SeedV))) np.random.seed(seed) paths = GeneratePathsHHWFXHWEuler(NoOfPaths,NoOfSteps,T,frwdFX,v0,vbar,kappa,gamma,lambdd,lambdf,etad,etaf,Rxv,Rxrd,Rxrf,Rvrd,Rvrf,Rrdrf) frwdfxT = paths["FX"] optMC = P0Td(T)* EUOptionPriceFromMCPathsGeneralizedFXFrwd(CP,frwdfxT[:,-1],K) optMCM[idx,:]= np.squeeze(optMC) # Average of the runs + standard deviation optionMC_E = np.zeros([len(K)]) optionMC_StDev = np.zeros([len(K)]) for (idx,k) in enumerate(K): optionMC_E[idx] = np.mean(optMCM[:,idx]) optionMC_StDev[idx] = np.std(optMCM[:,idx]) # Value from the COS method cf = lambda u: ChFH1HW_FX(u,T,gamma,Rxv,Rxrd,Rxrf,Rrdrf,Rvrd,Rvrf,lambdd,etad,lambdf,etaf,kappa,vbar,v0) valCOS_H1HW = P0Td(T)*CallPutOptionPriceCOSMthd_StochIR(cf, CP, frwdFX, T, K, N, L,1.0) # checking martingale property EyT = P0Td(T)/P0Tf(T)*EUOptionPriceFromMCPathsGeneralizedFXFrwd(CP,frwdfxT[:,-1],[0.0]) print("Martingale check: P_d(T)/P_f(T)*E[FX(T)] ={0:.4f} and y0 ={1}".format(EyT[0][0],y0)) print("Maturity chosen to T={0}".format(T)) for (idx,k) in enumerate(K): print("Option price for strike K={0:.4f} is equal to: COS method = {1:.4f} and MC = {2:.4f} with stdDev = {3:.4f}".format(k[0],valCOS_H1HW[idx][0],optionMC_E[idx],optionMC_StDev[idx])) plt.figure(1) plt.plot(K,optionMC_E) plt.plot(K,valCOS_H1HW,'--r') plt.grid() plt.legend(['Monte Carlo Option Price','COS method']) plt.title("Fx Option prices") # implied volatilities IVCos =np.zeros([len(K),1]) IVMC =np.zeros([len(K),1]) for (idx,k) in enumerate(K): priceCOS = valCOS_H1HW[idx]/P0Td(T) IVCos[idx] = ImpliedVolatilityBlack76(CP,priceCOS ,k,T,frwdFX)*100.0 priceMC = optionMC_E[idx]/P0Td(T) IVMC[idx] = ImpliedVolatilityBlack76(CP,priceMC ,k,T,frwdFX)*100.0 plt.figure(2) plt.plot(K,IVMC) plt.plot(K,IVCos,'--r') plt.grid() plt.legend(['IV-COS','IV-MC']) plt.title("Fx Implied volatilities") mainCalculation() ================================================ FILE: Lecture 11-Market Model and Convexity Adjustments/Materials/ConvexityCorrection.py ================================================ #%% """ Created on August 25 2021 Convexity correction exercise This code is purely educational and comes from "Financial Engineering" course by L.A. Grzelak The course is based on the book “Mathematical Modeling and Computation in Finance: With Exercises and Python and MATLAB Computer Codes”, by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing Europe Ltd, 2019. @author: Lech A. Grzelak """ import numpy as np import enum import matplotlib.pyplot as plt import scipy.integrate as integrate # This class defines puts and calls class OptionType(enum.Enum): CALL = 1.0 PUT = -1.0 def GeneratePathsHWEuler(NoOfPaths,NoOfSteps,T,P0T, lambd, eta): # time-step needed for differentiation dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) # Initial interest rate is a forward rate at time t->0 r0 = f0T(0.00001) theta = lambda t: 1.0/lambd * (f0T(t+dt)-f0T(t-dt))/(2.0*dt) + f0T(t) + eta*eta/(2.0*lambd*lambd)*(1.0-np.exp(-2.0*lambd*t)) #theta = lambda t: 0.1 +t -t #print("changed theta") Z = np.random.normal(0.0,1.0,[NoOfPaths,NoOfSteps]) W = np.zeros([NoOfPaths, NoOfSteps+1]) R = np.zeros([NoOfPaths, NoOfSteps+1]) R[:,0]=r0 time = np.zeros([NoOfSteps+1]) dt = T / float(NoOfSteps) for i in range(0,NoOfSteps): # making sure that samples from normal have mean 0 and variance 1 if NoOfPaths > 1: Z[:,i] = (Z[:,i] - np.mean(Z[:,i])) / np.std(Z[:,i]) W[:,i+1] = W[:,i] + np.power(dt, 0.5)*Z[:,i] R[:,i+1] = R[:,i] + lambd*(theta(time[i]) - R[:,i]) * dt + eta* (W[:,i+1]-W[:,i]) time[i+1] = time[i] +dt # Outputs paths = {"time":time,"R":R} return paths def HW_theta(lambd,eta,P0T): dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) theta = lambda t: 1.0/lambd * (f0T(t+dt)-f0T(t-dt))/(2.0*dt) + f0T(t) + eta*eta/(2.0*lambd*lambd)*(1.0-np.exp(-2.0*lambd*t)) #print("CHANGED THETA") return theta#lambda t: 0.1+t-t def HW_A(lambd,eta,P0T,T1,T2): tau = T2-T1 zGrid = np.linspace(0.0,tau,250) B_r = lambda tau: 1.0/lambd * (np.exp(-lambd *tau)-1.0) theta = HW_theta(lambd,eta,P0T) temp1 = lambd * integrate.trapz(theta(T2-zGrid)*B_r(zGrid),zGrid) temp2 = eta*eta/(4.0*np.power(lambd,3.0)) * (np.exp(-2.0*lambd*tau)*(4*np.exp(lambd*tau)-1.0) -3.0) + eta*eta*tau/(2.0*lambd*lambd) return temp1 + temp2 def HW_B(lambd,eta,T1,T2): return 1.0/lambd *(np.exp(-lambd*(T2-T1))-1.0) def HW_ZCB(lambd,eta,P0T,T1,T2,rT1): B_r = HW_B(lambd,eta,T1,T2) A_r = HW_A(lambd,eta,P0T,T1,T2) return np.exp(A_r + B_r *rT1) def HWMean_r(P0T,lambd,eta,T): # time-step needed for differentiation dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2.0*dt) # Initial interest rate is a forward rate at time t->0 r0 = f0T(0.00001) theta = HW_theta(lambd,eta,P0T) zGrid = np.linspace(0.0,T,2500) temp =lambda z: theta(z) * np.exp(-lambd*(T-z)) r_mean = r0*np.exp(-lambd*T) + lambd * integrate.trapz(temp(zGrid),zGrid) return r_mean def HW_r_0(P0T,lambd,eta): # time-step needed for differentiation dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) # Initial interest rate is a forward rate at time t->0 r0 = f0T(0.00001) return r0 def mainCalculation(): NoOfPaths = 20000 NoOfSteps = 1000 lambd = 0.02 eta = 0.02 # We define a ZCB curve (obtained from the market) P0T = lambda T: np.exp(-0.1*T)#np.exp(-0.03*T*T-0.1*T) r0 = HW_r_0(P0T,lambd,eta) # In this experiment we compare ZCB from the Market and Analytical expression N = 25 T_end = 50 Tgrid= np.linspace(0,T_end,N) Exact = np.zeros([N,1]) Proxy= np.zeros ([N,1]) for i,Ti in enumerate(Tgrid): Proxy[i] = HW_ZCB(lambd,eta,P0T,0.0,Ti,r0) Exact[i] = P0T(Ti) plt.figure(1) plt.grid() plt.plot(Tgrid,Exact,'-k') plt.plot(Tgrid,Proxy,'--r') plt.legend(["Analytcal ZCB","Monte Carlo ZCB"]) plt.title('P(0,T) from Monte Carlo vs. Analytical expression') # Here we define a libor rate and measure the convexity effect T1 = 4.0 T2 = 8.0 paths= GeneratePathsHWEuler(NoOfPaths,NoOfSteps,T1 ,P0T, lambd, eta) r = paths["R"] timeGrid = paths["time"] dt = timeGrid[1]-timeGrid[0] M_t = np.zeros([NoOfPaths,NoOfSteps]) for i in range(0,NoOfPaths): M_t[i,:] = np.exp(np.cumsum(r[i,:-1])*dt) P_T1_T2 = HW_ZCB(lambd,eta,P0T,T1,T2,r[:,-1]) L_T1_T2 = 1.0/(T2-T1)*(1.0/P_T1_T2-1) MC_Result = np.mean(1/M_t[:,-1]*L_T1_T2) print('Price of E(L(T1,T1,T2)/M(T1)) = {0}'.format(MC_Result)) L_T0_T1_T2 = 1.0/(T2-T1)*(P0T(T1)/P0T(T2)-1.0) # Define convexity correction cc = lambda sigma: P0T(T2)*(L_T0_T1_T2 + (T2-T1)*L_T0_T1_T2**2.0*np.exp(sigma**2*T1))-L_T0_T1_T2 # take random sigma and check the effect on price sigma = 0.2 print('Price of E(L(T1,T1,T2)/M(T1)) = {0} (no cc)'.format(L_T0_T1_T2)) print('Price of E(L(T1,T1,T2)/M(T1)) = {0} (with cc, sigma={1})'.format(L_T0_T1_T2+cc(sigma),sigma)) # Plot some results plt.figure(2) sigma_range = np.linspace(0.0,0.6,100) plt.plot(sigma_range,cc(sigma_range)) plt.grid() plt.xlabel('sigma') plt.ylabel('cc') plt.figure(3) plt.plot(sigma_range,MC_Result*np.ones([len(sigma_range),1])) plt.plot(sigma_range,L_T0_T1_T2+cc(sigma_range),'--r') plt.grid() plt.xlabel('sigma') plt.ylabel('value of derivative') plt.legend(['market price','price with cc']) mainCalculation() ================================================ FILE: Lecture 11-Market Model and Convexity Adjustments/Materials/DD_ImpliedVolatility.py ================================================ #%% """ Created on August 25 2021 Displaced Diffusion and implied volatilities This code is purely educational and comes from "Financial Engineering" course by L.A. Grzelak The course is based on the book “Mathematical Modeling and Computation in Finance: With Exercises and Python and MATLAB Computer Codes”, by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing Europe Ltd, 2019. @author: Lech A. Grzelak """ import numpy as np import matplotlib.pyplot as plt import scipy.stats as st import scipy.optimize as optimize import enum # set i= imaginary number i = np.complex(0.0,1.0) # time-step needed for differentiation dt = 0.0001 # This class defines puts and calls class OptionType(enum.Enum): CALL = 1.0 PUT = -1.0 # Black-Scholes Call option price def BS_Call_Option_Price(CP,S_0,K,sigma,tau,r): if K is list: K = np.array(K).reshape([len(K),1]) d1 = (np.log(S_0 / K) + (r + 0.5 * np.power(sigma,2.0)) * tau) / (sigma * np.sqrt(tau)) d2 = d1 - sigma * np.sqrt(tau) if CP == OptionType.CALL: value = st.norm.cdf(d1) * S_0 - st.norm.cdf(d2) * K * np.exp(-r * tau) elif CP == OptionType.PUT: value = st.norm.cdf(-d2) * K * np.exp(-r * tau) - st.norm.cdf(-d1)*S_0 return value # Implied volatility method def ImpliedVolatilityBlack76_xxx(CP,marketPrice,K,T,S_0): func = lambda sigma: np.power(BS_Call_Option_Price(CP,S_0,K,sigma,T,0.0) - marketPrice, 1.0) impliedVol = optimize.newton(func, 0.2, tol=1e-9) #impliedVol = optimize.brent(func, brack= (0.05, 2)) return impliedVol # Implied volatility method def ImpliedVolatilityBlack76(CP,marketPrice,K,T,S_0): # To determine initial volatility we interpolate define a grid for sigma # and interpolate on the inverse sigmaGrid = np.linspace(0.0,2.0,5000) optPriceGrid = BS_Call_Option_Price(CP,S_0,K,sigmaGrid,T,0.0) sigmaInitial = np.interp(marketPrice,optPriceGrid,sigmaGrid) print("Initial volatility = {0}".format(sigmaInitial)) # Use determined input for the local-search (final tuning) func = lambda sigma: np.power(BS_Call_Option_Price(CP,S_0,K,sigma,T,0.0) - marketPrice, 1.0) impliedVol = optimize.newton(func, sigmaInitial, tol=1e-5) print("Final volatility = {0}".format(impliedVol)) return impliedVol def DisplacedDiffusionModel_CallPrice(K,P0T,beta,sigma,frwd,T): d1 = (np.log(frwd / (beta*K+(1.0-beta)*frwd)) + 0.5 * np.power(sigma*beta,2.0) * T) / (sigma * beta* np.sqrt(T)) d2 = d1 - sigma * beta * np.sqrt(T) return P0T(T) * (frwd/beta * st.norm.cdf(d1) - (K + (1.0-beta)/beta*frwd) * st.norm.cdf(d2)) def mainCalculation(): CP = OptionType.CALL K = np.linspace(0.3,2.8,22) K = np.array(K).reshape([len(K),1]) # We define a ZCB curve (obtained from the market) P0T = lambda T: np.exp(-0.05*T) # DD model parameters beta = 0.5 sigma = 0.15 # Forward rate frwd = 1.0 # Maturity T = 2.0 # Effect of sigma plt.figure(1) plt.grid() plt.xlabel('strike, K') plt.ylabel('implied volatility') sigmaV = [0.1,0.2,0.3,0.4] legend = [] for sigmaTemp in sigmaV: callPrice = DisplacedDiffusionModel_CallPrice(K,P0T,beta,sigmaTemp,frwd,T) # Implied volatilities IV =np.zeros([len(K),1]) for idx in range(0,len(K)): valCOSFrwd = callPrice[idx]/P0T(T) IV[idx] = ImpliedVolatilityBlack76(CP,valCOSFrwd,K[idx],T,frwd) plt.plot(K,IV*100.0) legend.append('sigma={0}'.format(sigmaTemp)) plt.ylim([0.0,60]) plt.legend(legend) # Effect of beta plt.figure(2) plt.grid() plt.xlabel('strike, K') plt.ylabel('implied volatility') betaV = [-0.5, 0.0001, 0.5, 1.0] legend = [] for betaTemp in betaV: callPrice = DisplacedDiffusionModel_CallPrice(K,P0T,betaTemp,sigma,frwd,T) # Implied volatilities IV =np.zeros([len(K),1]) for idx in range(0,len(K)): valCOSFrwd = callPrice[idx]/P0T(T) IV[idx] = ImpliedVolatilityBlack76(CP,valCOSFrwd,K[idx],T,frwd) plt.plot(K,IV*100.0) legend.append('beta={0}'.format(betaTemp)) plt.legend(legend) mainCalculation() ================================================ FILE: Lecture 12-Valuation Adjustments- xVA (CVA,BCVA and FVA)/Materials/Exposures_HW_Netting.py ================================================ #%% """ Created on July 05 2021 Exposures for an IR swaps, under the Hull-White model - case of netting This code is purely educational and comes from "Financial Engineering" course by L.A. Grzelak The course is based on the book “Mathematical Modeling and Computation in Finance: With Exercises and Python and MATLAB Computer Codes”, by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing Europe Ltd, 2019. @author: Lech A. Grzelak """ import numpy as np import enum import matplotlib.pyplot as plt import scipy.stats as st import scipy.integrate as integrate # This class defines puts and calls class OptionTypeSwap(enum.Enum): RECEIVER = 1.0 PAYER = -1.0 def GeneratePathsHWEuler(NoOfPaths,NoOfSteps,T,P0T, lambd, eta): # time-step needed for differentiation dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) # Initial interest rate is a forward rate at time t->0 r0 = f0T(0.00001) theta = lambda t: 1.0/lambd * (f0T(t+dt)-f0T(t-dt))/(2.0*dt) + f0T(t) + eta*eta/(2.0*lambd*lambd)*(1.0-np.exp(-2.0*lambd*t)) #theta = lambda t: 0.1 +t -t #print("changed theta") Z = np.random.normal(0.0,1.0,[NoOfPaths,NoOfSteps]) W = np.zeros([NoOfPaths, NoOfSteps+1]) R = np.zeros([NoOfPaths, NoOfSteps+1]) R[:,0]=r0 time = np.zeros([NoOfSteps+1]) dt = T / float(NoOfSteps) for i in range(0,NoOfSteps): # making sure that samples from normal have mean 0 and variance 1 if NoOfPaths > 1: Z[:,i] = (Z[:,i] - np.mean(Z[:,i])) / np.std(Z[:,i]) W[:,i+1] = W[:,i] + np.power(dt, 0.5)*Z[:,i] R[:,i+1] = R[:,i] + lambd*(theta(time[i]) - R[:,i]) * dt + eta* (W[:,i+1]-W[:,i]) time[i+1] = time[i] +dt # Outputs paths = {"time":time,"R":R} return paths def HW_theta(lambd,eta,P0T): dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) theta = lambda t: 1.0/lambd * (f0T(t+dt)-f0T(t-dt))/(2.0*dt) + f0T(t) + eta*eta/(2.0*lambd*lambd)*(1.0-np.exp(-2.0*lambd*t)) #print("CHANGED THETA") return theta#lambda t: 0.1+t-t def HW_A(lambd,eta,P0T,T1,T2): tau = T2-T1 zGrid = np.linspace(0.0,tau,250) B_r = lambda tau: 1.0/lambd * (np.exp(-lambd *tau)-1.0) theta = HW_theta(lambd,eta,P0T) temp1 = lambd * integrate.trapz(theta(T2-zGrid)*B_r(zGrid),zGrid) temp2 = eta*eta/(4.0*np.power(lambd,3.0)) * (np.exp(-2.0*lambd*tau)*(4*np.exp(lambd*tau)-1.0) -3.0) + eta*eta*tau/(2.0*lambd*lambd) return temp1 + temp2 def HW_B(lambd,eta,T1,T2): return 1.0/lambd *(np.exp(-lambd*(T2-T1))-1.0) def HW_ZCB(lambd,eta,P0T,T1,T2,rT1): n = np.size(rT1) if T10 r0 = f0T(0.00001) theta = HW_theta(lambd,eta,P0T) zGrid = np.linspace(0.0,T,2500) temp =lambda z: theta(z) * np.exp(-lambd*(T-z)) r_mean = r0*np.exp(-lambd*T) + lambd * integrate.trapz(temp(zGrid),zGrid) return r_mean def HW_r_0(P0T,lambd,eta): # time-step needed for differentiation dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) # Initial interest rate is a forward rate at time t->0 r0 = f0T(0.00001) return r0 def HW_Mu_FrwdMeasure(P0T,lambd,eta,T): # time-step needed for differentiation dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) # Initial interest rate is a forward rate at time t->0 r0 = f0T(0.00001) theta = HW_theta(lambd,eta,P0T) zGrid = np.linspace(0.0,T,500) theta_hat =lambda t,T: theta(t) + eta*eta / lambd *1.0/lambd * (np.exp(-lambd*(T-t))-1.0) temp =lambda z: theta_hat(z,T) * np.exp(-lambd*(T-z)) r_mean = r0*np.exp(-lambd*T) + lambd * integrate.trapz(temp(zGrid),zGrid) return r_mean def HWVar_r(lambd,eta,T): return eta*eta/(2.0*lambd) *( 1.0-np.exp(-2.0*lambd *T)) def HWDensity(P0T,lambd,eta,T): r_mean = HWMean_r(P0T,lambd,eta,T) r_var = HWVar_r(lambd,eta,T) return lambda x: st.norm.pdf(x,r_mean,np.sqrt(r_var)) def HW_SwapPrice(CP,notional,K,t,Ti,Tm,n,r_t,P0T,lambd,eta): # CP- payer of receiver # n- notional # K- strike # t- today's date # Ti- beginning of the swap # Tm- end of Swap # n- number of dates payments between Ti and Tm # r_t -interest rate at time t if n == 1: ti_grid =np.array([Ti,Tm]) else: ti_grid = np.linspace(Ti,Tm,n) tau = ti_grid[1]- ti_grid[0] # overwrite Ti if t>Ti prevTi = ti_grid[np.where(ti_grid 0: #prevTi != []: Ti = prevTi[-1] # Now we need to handle the case when some payments are already done ti_grid = ti_grid[np.where(ti_grid>t)] temp= np.zeros(np.size(r_t)); P_t_TiLambda = lambda Ti : HW_ZCB(lambd,eta,P0T,t,Ti,r_t) for (idx,ti) in enumerate(ti_grid): if ti>Ti: temp = temp + tau * P_t_TiLambda(ti) P_t_Ti = P_t_TiLambda(Ti) P_t_Tm = P_t_TiLambda(Tm) if CP==OptionTypeSwap.PAYER: swap = (P_t_Ti - P_t_Tm) - K * temp elif CP==OptionTypeSwap.RECEIVER: swap = K * temp- (P_t_Ti - P_t_Tm) return swap*notional def mainCalculation(): NoOfPaths = 2000 NoOfSteps = 1000 lambd = 0.5 eta = 0.03 notional = 10000.0 notional2 = 10000.0 alpha = 0.99 alpha2 = 0.95 # We define a ZCB curve (obtained from the market) P0T = lambda T: np.exp(-0.01*T) r0 = HW_r_0(P0T,lambd,eta) # In this experiment we compare ZCB from the Market and Analytical expression N = 25 T_end = 50 Tgrid= np.linspace(0,T_end,N) Exact = np.zeros([N,1]) Proxy= np.zeros ([N,1]) for i,Ti in enumerate(Tgrid): Proxy[i] = HW_ZCB(lambd,eta,P0T,0.0,Ti,r0) Exact[i] = P0T(Ti) plt.figure(1) plt.grid() plt.plot(Tgrid,Exact,'-k') plt.plot(Tgrid,Proxy,'--r') plt.legend(["Analytcal ZCB","Monte Carlo ZCB"]) plt.title('P(0,T) from Monte Carlo vs. Analytical expression') # Here we simulate the exposure profiles for a swap, using the HW model # Swap settings K = 0.01 # strike Ti = 1.0 # begining of the swap Tm = 10.0 # end date of the swap n = 10 # number of payments between Ti and Tm paths= GeneratePathsHWEuler(NoOfPaths,NoOfSteps,Tm+1.0 ,P0T, lambd, eta) r = paths["R"] timeGrid = paths["time"] dt = timeGrid[1]-timeGrid[0] # Here we compare the price of an option on a ZCB from Monte Carlo and Analytical expression M_t = np.zeros([NoOfPaths,NoOfSteps]) for i in range(0,NoOfPaths): M_t[i,:] = np.exp(np.cumsum(r[i,0:-1])*dt) # Portfolio without netting Value= np.zeros([NoOfPaths,NoOfSteps+1]) E = np.zeros([NoOfPaths,NoOfSteps+1]) EE = np.zeros([NoOfSteps+1]) PFE = np.zeros([NoOfSteps+1]) PFE2 = np.zeros([NoOfSteps+1]) for (idx, ti) in enumerate(timeGrid[0:-2]): V = HW_SwapPrice(OptionTypeSwap.PAYER,notional,K,timeGrid[idx],Ti,Tm,n,r[:,idx],P0T,lambd,eta) Value[:,idx] = V E[:,idx] = np.maximum(V,0.0) EE[idx] = np.mean(E[:,idx]/M_t[:,idx]) PFE[idx] = np.quantile(E[:,idx],alpha) PFE2[idx] = np.quantile(E[:,idx],alpha2) # Portfolio with netting ValuePort = np.zeros([NoOfPaths,NoOfSteps+1]) EPort = np.zeros([NoOfPaths,NoOfSteps+1]) EEPort = np.zeros([NoOfSteps+1]) PFEPort = np.zeros([NoOfSteps+1]) for (idx, ti) in enumerate(timeGrid[0:-2]): Swap1 = HW_SwapPrice(OptionTypeSwap.PAYER,notional,K,timeGrid[idx],Ti,Tm,n,r[:,idx],P0T,lambd,eta) Swap2 = HW_SwapPrice(OptionTypeSwap.RECEIVER,notional2,0.0,timeGrid[idx],Tm-2.0*(Tm-Ti)/n,Tm,1,r[:,idx],P0T,lambd,eta) VPort = Swap1 + Swap2 ValuePort[:,idx] = VPort EPort[:,idx] = np.maximum(VPort,0.0) EEPort[idx] = np.mean(EPort[:,idx]/M_t[:,idx]) PFEPort[idx] = np.quantile(EPort[:,idx],alpha) plt.figure(2) plt.plot(timeGrid,Value[0:100,:].transpose(),'b') plt.grid() plt.xlabel('time') plt.ylabel('exposure, Value(t)') plt.title('Value of a swap') plt.figure(3) plt.plot(timeGrid,E[0:100,:].transpose(),'r') plt.grid() plt.xlabel('time') plt.ylabel('exposure, E(t)') plt.title('Positive Exposure E(t)') plt.figure(4) plt.plot(timeGrid,EE,'r') plt.grid() plt.xlabel('time') plt.ylabel('exposure, EE(t)') plt.title('Discounted Expected (positive) exposure, EE') plt.legend(['EE','PFE']) plt.figure(5) plt.plot(timeGrid,EE,'r') plt.plot(timeGrid,PFE,'k') plt.plot(timeGrid,PFE2,'--b') plt.grid() plt.xlabel('time') plt.ylabel(['EE, PEE(t)']) plt.title('Discounted Expected (positive) exposure, EE') plt.figure(6) plt.plot(timeGrid,EEPort,'r') plt.plot(timeGrid,PFEPort,'k') plt.grid() plt.title('Portfolio with two swaps') plt.legend(['EE-port','PFE-port']) plt.figure(7) plt.plot(timeGrid,EE,'r') plt.plot(timeGrid,EEPort,'--r') plt.grid() plt.title('Comparison of EEs ') plt.legend(['EE, swap','EE, portfolio']) plt.figure(8) plt.plot(timeGrid,PFE,'k') plt.plot(timeGrid,PFEPort,'--k') plt.grid() plt.title('Comparison of PFEs ') plt.legend(['PFE, swap','PFE, portfolio']) mainCalculation() ================================================ FILE: Lecture 13-Value-at-Risk and Expected Shortfall/Materials/HistoricalVaR_Calculation.py ================================================ #%% """ Created on August 27 2021 Historical Value-at-Risk Calculation based on real market data https://www.treasury.gov/resource-center/data-chart-center/interest-rates/Pages/TextView.aspx?data=yieldYear&year=2021 This code is purely educational and comes from "Financial Engineering" course by L.A. Grzelak The course is based on the book “Mathematical Modeling and Computation in Finance: With Exercises and Python and MATLAB Computer Codes”, by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing Europe Ltd, 2019. @author: Lech A. Grzelak """ import numpy as np import enum import matplotlib.pyplot as plt from copy import deepcopy import pandas as pd # This class defines puts and calls class OptionTypeSwap(enum.Enum): RECEIVER = 1.0 PAYER = -1.0 def IRSwap(CP,notional,K,t,Ti,Tm,n,P0T): # CP- payer of receiver # n- notional # K- strike # t- today's date # Ti- beginning of the swap # Tm- end of Swap # n- number of dates payments between Ti and Tm # r_t -interest rate at time t ti_grid = np.linspace(Ti,Tm,int(n)) tau = ti_grid[1]- ti_grid[0] # overwrite Ti if t>Ti prevTi = ti_grid[np.where(ti_grid 0: #prevTi != []: Ti = prevTi[-1] # Now we need to handle the case when some payments are already done ti_grid = ti_grid[np.where(ti_grid>t)] temp= 0.0 for (idx,ti) in enumerate(ti_grid): if ti>Ti: temp = temp + tau * P0T(ti) P_t_Ti = P0T(Ti) P_t_Tm = P0T(Tm) if CP==OptionTypeSwap.PAYER: swap = (P_t_Ti - P_t_Tm) - K * temp elif CP==OptionTypeSwap.RECEIVER: swap = K * temp - (P_t_Ti - P_t_Tm) return swap * notional def P0TModel(t,ti,ri,method): rInterp = method(ti,ri) r = rInterp(t) return np.exp(-r*t) def YieldCurve(instruments, maturities, r0, method, tol): r0 = deepcopy(r0) ri = MultivariateNewtonRaphson(r0, maturities, instruments, method, tol=tol) return ri def MultivariateNewtonRaphson(ri, ti, instruments, method, tol): err = 10e10 idx = 0 while err > tol: idx = idx +1 values = EvaluateInstruments(ti,ri,instruments,method) J = Jacobian(ti,ri, instruments, method) J_inv = np.linalg.inv(J) err = - np.dot(J_inv, values) ri[0:] = ri[0:] + err err = np.linalg.norm(err) #print('index in the loop is',idx,' Error is ', err) return ri def Jacobian(ti, ri, instruments, method): eps = 1e-05 swap_num = len(ti) J = np.zeros([swap_num, swap_num]) val = EvaluateInstruments(ti,ri,instruments,method) ri_up = deepcopy(ri) for j in range(0, len(ri)): ri_up[j] = ri[j] + eps val_up = EvaluateInstruments(ti,ri_up,instruments,method) ri_up[j] = ri[j] dv = (val_up - val) / eps J[:, j] = dv[:] return J def EvaluateInstruments(ti,ri,instruments,method): P0Ttemp = lambda t: P0TModel(t,ti,ri,method) val = np.zeros(len(instruments)) for i in range(0,len(instruments)): val[i] = instruments[i](P0Ttemp) return val def linear_interpolation(ti,ri): interpolator = lambda t: np.interp(t, ti, ri) return interpolator def BuildYieldCurve(K,mat): # Convergence tolerance tol = 1.0e-15 # Initial guess for the spine points r0 = np.array([0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01]) # Interpolation method method = linear_interpolation swap1 = lambda P0T: IRSwap(OptionTypeSwap.PAYER,1,K[0],0.0,0.0,mat[0],4*mat[0],P0T) swap2 = lambda P0T: IRSwap(OptionTypeSwap.PAYER,1,K[1],0.0,0.0,mat[1],4*mat[1],P0T) swap3 = lambda P0T: IRSwap(OptionTypeSwap.PAYER,1,K[2],0.0,0.0,mat[2],4*mat[2],P0T) swap4 = lambda P0T: IRSwap(OptionTypeSwap.PAYER,1,K[3],0.0,0.0,mat[3],4*mat[3],P0T) swap5 = lambda P0T: IRSwap(OptionTypeSwap.PAYER,1,K[4],0.0,0.0,mat[4],4*mat[4],P0T) swap6 = lambda P0T: IRSwap(OptionTypeSwap.PAYER,1,K[5],0.0,0.0,mat[5],4*mat[5],P0T) swap7 = lambda P0T: IRSwap(OptionTypeSwap.PAYER,1,K[6],0.0,0.0,mat[6],4*mat[6],P0T) swap8 = lambda P0T: IRSwap(OptionTypeSwap.PAYER,1,K[7],0.0,0.0,mat[7],4*mat[7],P0T) instruments = [swap1,swap2,swap3,swap4,swap5,swap6,swap7,swap8] # determine optimal spine points ri = YieldCurve(instruments, mat, r0, method, tol) #print('\n Spine points are',ri,'\n') # Build a ZCB-curve/yield curve from the spine points P0T = lambda t: P0TModel(t,mat,ri,method) return P0T, instruments def Portfolio(P0T): #IRSwap(CP, notional, K, t, Ti, Tm, n, P0T): value = IRSwap(OptionTypeSwap.RECEIVER,1000000,0.02,0.0, 0.0, 20, 20, P0T) +\ IRSwap(OptionTypeSwap.PAYER, 500000, 0.01,0.0, 0.0, 10, 20, P0T) +\ IRSwap(OptionTypeSwap.RECEIVER,25000,0.02,0.0, 0.0, 30, 60, P0T) +\ IRSwap(OptionTypeSwap.PAYER,74000,0.005,0.0, 0.0, 5, 10, P0T) +\ IRSwap(OptionTypeSwap.RECEIVER,254000,0.032,0.0, 0.0, 15, 10, P0T) +\ IRSwap(OptionTypeSwap.RECEIVER,854000,0.01,0.0, 0.0, 7, 20, P0T) +\ IRSwap(OptionTypeSwap.PAYER,350000,0.028,0.0, 0.0, 10, 20, P0T) +\ IRSwap(OptionTypeSwap.PAYER,1000000,-0.01,0.0, 0.0, 5, 20, P0T) +\ IRSwap(OptionTypeSwap.RECEIVER,1000000,0.01,0.0, 0.0, 14, 20, P0T) +\ IRSwap(OptionTypeSwap.PAYER,1000000,0.03,0.0, 0.0, 2, 4, P0T) return value def mainCode(): # Curves from the market marketdataXLS = pd.read_excel('MrktData.xlsx') # Divide the quotes by 100 as they are expressed in % marketData = np.array(marketdataXLS) / 100.0 # Building up 1D scenarios shape= np.shape(marketData) NoOfScen = shape[0] NoOfInsts = shape[1] Scenarios = np.zeros([NoOfScen-1,NoOfInsts]) for i in range(0,NoOfScen-1): for j in range(0,NoOfInsts): Scenarios[i,j] = marketData[i+1,j]-marketData[i,j] # Construct instruments for TODAY's curve Swaps_mrkt = np.array([0.08, 0.2, 0.4, 0.77, 1.07, 1.29, 1.82, 1.9]) / 100 mat = np.array([1.0, 2.0, 3.0, 5.0, 7.0, 10.0, 20.0, 30.0]) # Given market quotes for swaps and scenarios we generate now "shocked" yield curves Swaps_mrkt_shocked = np.zeros([NoOfScen-1,NoOfInsts]) for i in range(0,NoOfScen-1): for j in range(0,NoOfInsts): Swaps_mrkt_shocked[i,j] = Swaps_mrkt[j] + Scenarios[i,j] # For every shocked market scenario we build a yield curve YC_for_VaR = []#np.zeros([NoOfScen-1]) for i in range(0,NoOfScen-1): P0T,instruments = BuildYieldCurve(Swaps_mrkt_shocked[i,:],mat) YC_for_VaR.append(P0T) print('Scenario number',i,' out of ', NoOfScen-1) # For every shocked yield curve we re-value the portfolio of interest rate derivatives PortfolioPV = np.zeros([NoOfScen-1]) for i in range(0,NoOfScen-1): PortfolioPV[i] = Portfolio(YC_for_VaR[i]) # Current Yield curve YC_today,insts = BuildYieldCurve(Swaps_mrkt,mat) print('Current Portfolio PV is ', Portfolio(YC_today)) # Histograms and Var Calculatiosn plt.figure(1) plt.grid() plt.hist(PortfolioPV,20) # VaR calculation alpha = 0.05 HVaR_estimate = np.quantile(PortfolioPV,alpha) print('(H)VaR for alpha = ', alpha, ' is equal to=', HVaR_estimate) # Expected shortfal condLosses = PortfolioPV[PortfolioPV < HVaR_estimate] print('P&L which < VaR_alpha =',condLosses) ES = np.mean(condLosses) print('Expected shortfal = ', ES) plt.plot(HVaR_estimate,0,'or') plt.plot(ES,0,'ok') plt.legend(['VaR','ES','P&L']) return 0.0 mainCode() ================================================ FILE: Lecture 13-Value-at-Risk and Expected Shortfall/Materials/MonteCarloVaR.py ================================================ #%% """ Created on August 25 2021 Value-At-Risk computation based on Monte Carlo simulation of the Hull-White model This code is purely educational and comes from "Financial Engineering" course by L.A. Grzelak The course is based on the book “Mathematical Modeling and Computation in Finance: With Exercises and Python and MATLAB Computer Codes”, by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing Europe Ltd, 2019. @author: Lech A. Grzelak """ import numpy as np import enum import matplotlib.pyplot as plt import scipy.stats as st import scipy.integrate as integrate import scipy.optimize as optimize # This class defines puts and calls class OptionTypeSwap(enum.Enum): RECEIVER = 1.0 PAYER = -1.0 def GeneratePathsHWEuler(NoOfPaths,NoOfSteps,T,P0T, lambd, eta): # time-step needed for differentiation dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) # Initial interest rate is a forward rate at time t->0 r0 = f0T(0.00001) theta = lambda t: 1.0/lambd * (f0T(t+dt)-f0T(t-dt))/(2.0*dt) + f0T(t) + eta*eta/(2.0*lambd*lambd)*(1.0-np.exp(-2.0*lambd*t)) #theta = lambda t: 0.1 +t -t #print("changed theta") Z = np.random.normal(0.0,1.0,[NoOfPaths,NoOfSteps]) W = np.zeros([NoOfPaths, NoOfSteps+1]) R = np.zeros([NoOfPaths, NoOfSteps+1]) R[:,0]=r0 time = np.zeros([NoOfSteps+1]) dt = T / float(NoOfSteps) for i in range(0,NoOfSteps): # making sure that samples from normal have mean 0 and variance 1 if NoOfPaths > 1: Z[:,i] = (Z[:,i] - np.mean(Z[:,i])) / np.std(Z[:,i]) W[:,i+1] = W[:,i] + np.power(dt, 0.5)*Z[:,i] R[:,i+1] = R[:,i] + lambd*(theta(time[i]) - R[:,i]) * dt + eta* (W[:,i+1]-W[:,i]) time[i+1] = time[i] +dt # Outputs paths = {"time":time,"R":R} return paths def HW_theta(lambd,eta,P0T): dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) theta = lambda t: 1.0/lambd * (f0T(t+dt)-f0T(t-dt))/(2.0*dt) + f0T(t) + eta*eta/(2.0*lambd*lambd)*(1.0-np.exp(-2.0*lambd*t)) #print("CHANGED THETA") return theta#lambda t: 0.1+t-t def HW_A(lambd,eta,P0T,T1,T2): tau = T2-T1 zGrid = np.linspace(0.0,tau,250) B_r = lambda tau: 1.0/lambd * (np.exp(-lambd *tau)-1.0) theta = HW_theta(lambd,eta,P0T) temp1 = lambd * integrate.trapz(theta(T2-zGrid)*B_r(zGrid),zGrid) temp2 = eta*eta/(4.0*np.power(lambd,3.0)) * (np.exp(-2.0*lambd*tau)*(4*np.exp(lambd*tau)-1.0) -3.0) + eta*eta*tau/(2.0*lambd*lambd) return temp1 + temp2 def HW_B(lambd,eta,T1,T2): return 1.0/lambd *(np.exp(-lambd*(T2-T1))-1.0) def HW_ZCB(lambd,eta,P0T,T1,T2,rT1): n = np.size(rT1) if T10 r0 = f0T(0.00001) theta = HW_theta(lambd,eta,P0T) zGrid = np.linspace(0.0,T,2500) temp =lambda z: theta(z) * np.exp(-lambd*(T-z)) r_mean = r0*np.exp(-lambd*T) + lambd * integrate.trapz(temp(zGrid),zGrid) return r_mean def HW_r_0(P0T,lambd,eta): # time-step needed for differentiation dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) # Initial interest rate is a forward rate at time t->0 r0 = f0T(0.00001) return r0 def HW_Mu_FrwdMeasure(P0T,lambd,eta,T): # time-step needed for differentiation dt = 0.0001 f0T = lambda t: - (np.log(P0T(t+dt))-np.log(P0T(t-dt)))/(2*dt) # Initial interest rate is a forward rate at time t->0 r0 = f0T(0.00001) theta = HW_theta(lambd,eta,P0T) zGrid = np.linspace(0.0,T,500) theta_hat =lambda t,T: theta(t) + eta*eta / lambd *1.0/lambd * (np.exp(-lambd*(T-t))-1.0) temp =lambda z: theta_hat(z,T) * np.exp(-lambd*(T-z)) r_mean = r0*np.exp(-lambd*T) + lambd * integrate.trapz(temp(zGrid),zGrid) return r_mean def HWVar_r(lambd,eta,T): return eta*eta/(2.0*lambd) *( 1.0-np.exp(-2.0*lambd *T)) def HWDensity(P0T,lambd,eta,T): r_mean = HWMean_r(P0T,lambd,eta,T) r_var = HWVar_r(lambd,eta,T) return lambda x: st.norm.pdf(x,r_mean,np.sqrt(r_var)) def HW_SwapPrice(CP,notional,K,t,Ti,Tm,n,r_t,P0T,lambd,eta): # CP- payer of receiver # n- notional # K- strike # t- today's date # Ti- beginning of the swap # Tm- end of Swap # n- number of dates payments between Ti and Tm # r_t -interest rate at time t if n == 1: ti_grid =np.array([Ti,Tm]) else: ti_grid = np.linspace(Ti,Tm,n) tau = ti_grid[1]- ti_grid[0] # overwrite Ti if t>Ti prevTi = ti_grid[np.where(ti_grid 0: #prevTi != []: Ti = prevTi[-1] # Now we need to handle the case when some payments are already done ti_grid = ti_grid[np.where(ti_grid>t)] temp= np.zeros(np.size(r_t)); P_t_TiLambda = lambda Ti : HW_ZCB(lambd,eta,P0T,t,Ti,r_t) for (idx,ti) in enumerate(ti_grid): if ti>Ti: temp = temp + tau * P_t_TiLambda(ti) P_t_Ti = P_t_TiLambda(Ti) P_t_Tm = P_t_TiLambda(Tm) if CP==OptionTypeSwap.PAYER: swap = (P_t_Ti - P_t_Tm) - K * temp elif CP==OptionTypeSwap.RECEIVER: swap = K * temp- (P_t_Ti - P_t_Tm) return swap*notional def Portfolio(P0T,r_t,lambd,eta): #IRSwap(CP, notional, K, t, Ti, Tm, n, P0T): value = HW_SwapPrice(OptionTypeSwap.RECEIVER,1000000,0.02,0.0, 0.0, 20, 20,r_t, P0T,lambd,eta) +\ HW_SwapPrice(OptionTypeSwap.PAYER, 500000, 0.01,0.0, 0.0, 10, 20,r_t, P0T,lambd,eta) +\ HW_SwapPrice(OptionTypeSwap.RECEIVER,25000,0.02,0.0, 0.0, 30, 60,r_t, P0T,lambd,eta) +\ HW_SwapPrice(OptionTypeSwap.PAYER,74000,0.005,0.0, 0.0, 5, 10,r_t, P0T,lambd,eta) +\ HW_SwapPrice(OptionTypeSwap.RECEIVER,254000,0.032,0.0, 0.0, 15, 10,r_t, P0T,lambd,eta) +\ HW_SwapPrice(OptionTypeSwap.RECEIVER,854000,0.01,0.0, 0.0, 7, 20,r_t, P0T,lambd,eta) +\ HW_SwapPrice(OptionTypeSwap.PAYER,900000,0.045,0.0, 0.0, 10, 20,r_t, P0T,lambd,eta) +\ HW_SwapPrice(OptionTypeSwap.PAYER,400000,0.02,0.0, 0.0, 10, 20,r_t, P0T,lambd,eta) +\ HW_SwapPrice(OptionTypeSwap.RECEIVER,1000000,0.01,0.0, 0.0, 14, 20,r_t, P0T,lambd,eta) +\ HW_SwapPrice(OptionTypeSwap.PAYER,115000,0.06,0.0, 0.0, 9, 10,r_t, P0T,lambd,eta) #HW_SwapPrice(CP,notional,K,t,Ti,Tm,n,r_t,P0T,lambd,eta) return value def mainCalculation(): NoOfPaths = 2000 NoOfSteps = 100 lambd = 0.5 eta = 0.03 # We define a ZCB curve (obtained from the market) P0T = lambda T: np.exp(-0.001*T) r0 = HW_r_0(P0T,lambd,eta) # In this experiment we compare ZCB from the Market and Analytical expression N = 25 T_end = 50 Tgrid= np.linspace(0,T_end,N) Exact = np.zeros([N,1]) Proxy= np.zeros ([N,1]) for i,Ti in enumerate(Tgrid): Proxy[i] = HW_ZCB(lambd,eta,P0T,0.0,Ti,r0) Exact[i] = P0T(Ti) plt.figure(1) plt.grid() plt.plot(Tgrid,Exact,'-k') plt.plot(Tgrid,Proxy,'--r') plt.legend(["Analytcal ZCB","Monte Carlo ZCB"]) plt.title('P(0,T) from Monte Carlo vs. Analytical expression') # Here we simulate the exposure profiles for a swap, using the HW model T_end = 20 paths= GeneratePathsHWEuler(NoOfPaths,NoOfSteps,T_end ,P0T, lambd, eta) r = paths["R"] timeGrid = paths["time"] dt = timeGrid[1]-timeGrid[0] # Here we compare the price of an option on a ZCB from Monte Carlo and Analytical expression M_t = np.zeros([NoOfPaths,NoOfSteps]) for i in range(0,NoOfPaths): M_t[i,:] = np.exp(np.cumsum(r[i,0:-1])*dt) # Compute exposure profile r0 = r[0,0] stepSize = 10 V_M = np.zeros([NoOfPaths,NoOfSteps-stepSize]) for i in range(0,NoOfSteps-stepSize): dr = r[:,i + stepSize] - r[:,i] V_t0 = Portfolio(P0T, r[:,0] + dr,lambd,eta) V_M[:,i] = V_t0 plt.figure(2) V_t0_vec = np.matrix.flatten(V_M) plt.hist(V_t0_vec,100) plt.grid() print('Value V(t_0)= ',Portfolio(P0T,r[0,0],lambd,eta)) # VaR calculation alpha = 0.05 HVaR_estimate = np.quantile(V_t0_vec,alpha) print('(H)VaR for alpha = ', alpha, ' is equal to=', HVaR_estimate) # Expected shortfal condLosses = V_t0_vec[V_t0_vec < HVaR_estimate] print('P&L which < VaR_alpha =',condLosses) ES = np.mean(condLosses) print('Expected shortfal = ', ES) plt.plot(HVaR_estimate,0,'or') plt.plot(ES,0,'ok') plt.legend(['VaR','ES','P&L']) mainCalculation() ================================================ FILE: README.md ================================================ # Financial Engineering Course Here you will find materials for the course of "Financial Engineering: Interest rates & xVA" YouTube lectures you can find at: https://www.youtube.com/ComputationsInFinance The course is based on the book: "Mathematical Modeling and Computation in Finance: With Exercises and Python and MATLAB Computer Codes", by C.W. Oosterlee and L.A. Grzelak, World Scientific Publishing, 2019. The website of the book is: https://QuantFinanceBook.com Personal website of the author (with more courses) is: https://LechGrzelak.com Content of the course: Lecture 1- Introduction & Details Regarding the Course\ Lecture 2- Understanding of Filtrations and Measures\ Lecture 3- The HJM Framework\ Lecture 4- Yield Curve Dynamics under Short Rate\ Lecture 5- Interest Rate Products\ Lecture 6- Construction of Yield Curve and Multi-Curves\ Lecture 7- Pricing of Swaptions and Negative Interest Rates\ Lecture 8- Mortgages and Prepayments\ Lecture 9- Hybrid Models and Stochastic Interest Rates\ Lecture 10- Foreign Exchange (FX) and Inflation\ Lecture 11- Market Models and Convexity Adjustments\ Lecture 12- Valuation Adjustments- xVA (CVA, BCVA and FVA)\ Lecture 13- Value-at-Risk and Expected Shortfall\ Lecture 14- The Summary of the Course\