赞
踩
Interest rates affect economic activities at all levels. Central banks, including the Federal Reserve (informally known as the Fed), target interest rates as a policy tool to influence economic activity. Interest rate derivatives利率衍生品 are popular with investors who require customized cash flow needs or specific views on interest-rate movements.
One of the key challenges that interest-rate derivative traders face is to have a good and robust pricing procedure for these products. This involves understanding the complicated behavior of an individual interest-rate movement. Several interest-rate models have been proposed for financial studies. Some common models studied in finance are the Vasicek, CIR, and Hull-White models. These interest-rate models involve modeling the short-rate and rely on factors (or sources of uncertainty) with most of them using only one factor. Two-factor and multi-factor interest rate models have been proposed.
In this chapter, we will cover the following topics:
Corporations and governments issue fixed-income securities as a means of raising money筹集资金.
The holders of debt securities, such as US Treasury bills美国国库券, notes票据, and bonds, face the risk of default by the issuer. The federal government and municipal( /mjuːˈnɪsɪp(ə)l/ 城市的,市政的) government are thought to face the least default risk, since they can easily raise taxes and create more money to repay the outstanding debts.
Most bonds pay a fixed amount of interest semi-annually(/ ˈsemi ˈænjuəli /每半年一次), while some pay quarterly, or annually. These interest payments are also referred to as coupons息票. They are quoted as a percentage of the face value or par/ pɑːr / amount面值 of the bond on an annual basis.
For example, a 5-year $10,000 Treasury bond国债 with a coupon rate of 5% pays coupons of $500 each year, or coupons of $250 every 6 months, up to and including the maturity date. Should the interest rates drop and new T-bonds pay a 3% coupon rate, the buyer of the new bond will only receive coupons of $300 annually, while existing holders of the 5% bond will continue to receive $500 annually. As the characteristics of the bonds influence their prices, they're closely related to current levels of interest rates in an inverse way:
In a normal yield curve environment, long-term interest rates are higher than short-term interest rates. Investors expect to be compensated with higher returns when they lend money for a longer period since they are exposed to a higher default risk. The normal or positive yield curve is said to be upward sloping, as shown in the following graph:
In certain economic conditions, the yield curve can be inverted. Long-term interest rates are lower than short-term interest rates. Such a condition occurs when the supply of money is tight. Investors are willing to forgo(/ fɔːrˈɡoʊ /放弃) long-term gains to preserve their wealth in the short-term. During periods of high inflation, where the inflation rate exceeds the rate of coupon interests, negative interest rates may be observed. Investors are willing to pay in the short-term just to secure their long-term wealth. The inverted yield curve is said to be downward sloping, as shown in the following graph:
##################
Compound interest is interest calculated on the initial principal and also on the accumulated interest of previous periods of a deposit or loan. The effect of compound interest depends on frequency.
Assume an annual interest rate of 12%. If we start the year with $100 and compound only once, at the end of the year, the principal grows to $112 (P∗(1+r∗t)=100∗(1+0.12∗1) = $112). Interest applied only to the principal is referred to as simple interest.
compounded monthly, n =12:
If we instead compound each month at 1%(monthly-compounded yield=R=rn=rfrequency=1212=1), we end up with more than $112 at the end of the year(T=1 year). That is, FV=P∗(1+rn)n∗T=PV∗(1+R)t=100∗(1+0.01)12=112.68. (It's higher because we compounded more frequently.)
If we known the monthly-compounded yield is 1%, and the value is $112.68 after one year, we want to calculate the present value: pv=fv(1+rn)nT=fv(1+R)nT=112.68(1+0.01)1∗12=100
Continuously compounded returns compound the most frequently of all. Continuous compounding is the mathematical limit that compound interest can reach. It is an extreme case of compounding since most interest is compounded on a monthly, quarterly, or semiannual basis.
##################http://www.math.iupui.edu/~momran/m119/old/ch4h.htm
BP=∑ntT=1Cn(1+yn)T+FV(1+yn)nt
A zero-coupon bond is a bond that does not pay any periodic interest(C=0) except on maturity(n=1), where the principal or face value is repaid. Zero-coupon bonds are also called pure discount bonds纯贴现债券.
A zero-coupon bond can be valued as follows:
Here, y is the annually-compounded yield年复合收益率 or rate of the bond, and t is the time remaining to the maturity of the bond. Let's take a look at an example of a five-year zero-coupon bond with a face value of $100. The yield is 5%, compounded annually. The price can be calculated as follows:
A simple Python zero-coupon bond calculator can be used to illustrate this example:
- def zero_coupon_bond( par, y, t):
- """
- Price a zero coupon bond.
-
- :param par: face value of the bond.
- :param y: annual yield or rate of the bond.
- :param t: time to maturity, in years.
- """
- return par/(1+y)**t
Using the preceding example, we get the following result:
print( zero_coupon_bond(100, 0.05, 5) )
In the preceding example, we assumed that the investor is able to invest $78.35 at the prevailing(/ prɪˈveɪlɪŋ 现存的,现行) annual interest rate of 5% for 5 years, compounded annually.
Now that we have a zero-coupon bond calculator, we can use it to determine zero rates by bootstrapping引导 the yield curve, as explained in the next section.
#######################
The effective interest rate is calculated as if compounded annually. The effective rate is calculated in the following way, where reff=(1+rn)n−1is the effective annual rate,
For example, a nominal interest rate of 6% compounded monthly is equivalent to an effective interest rate of 6.17%.
reff=(1+612)12−1≈1.0617−1≈6.17
When the frequency of compounding is increased up to infinity (as for many processes in nature) the calculation simplifies to:rcontinuous=ereff−1
If we increase the compound frequency to its limit, we are compounding continuously. While this may not be practical, the continuously compounded interest rate offers marvelously convenient properties. It turns out that the continuously compounded interest rate is given by:
rcontinuous=ln(1+reff) note: reff=(1+rn)n−1
erc=ercontinuous=1+reff=(1+rn)n
FV=PV∗(1+R)nT=p∗(1+rn)nT=p∗(1+reff)T=p∗erc∗T
When interest is compounded more frequently, the amount of interest earned in each increment of time becomes smaller, but the total amount of accumulated interest grows faster
What's so great about the continuously compounded rate (or return) that we will denote with rc? First, it's easy to scale it forward. Given a principal of (P), our final wealth over (T) years is given by:
w=A=FV=p∗erc∗TIf the interest is compounded continuously for T years at a rate of rc per year, then the compounded amount is given byw=FV=p∗erc∗T
Note that e is the exponential function. For example, if we start with $100 and continuously compound at 8% over three years, the final wealth is given by:
w=A=p∗erc∗T =
Discounting to the present value (PV or p) is merely compounding in reverse, so the present value of a future value (FV) compounded continuously at a rate of (rc) is given by:
PV of FV received in (n) years PV=FVerc∗T
#######################Continuous Compound Interest
As the compounding frequency increases (say, from compounded yearly to compounded daily), the future value of money reaches an exponential limit指数极限. That is to say, the value of $100 today will reach a future value of $100*eRT when it is invested at a continuously compounded rate, R, for a period of time, T. If we discount these values for a security that pays $100 at a future time, T, with a continuously-compounded discount rate, R, its value at time 0 is e100RT. This rate is known as the spot rate即期利率.需要注意得是,即期利率不是一个能够直接观察到的市场变量,而是一个基于现金流折现法,通过对市场数据进行分析而得到的利率。
Spot rates represent the current interest rates for several maturities, should we want to borrow or lend money now. Zero rates( called zero-coupon interest rate or spot rate) represent the internal rate of return of zero-coupon bonds.
By deriving the spot rates of bonds with different maturities, we can construct the present yield curve through a bootstrapping引导 process using zero-coupon bonds.
Short-term spot rates短期即期利率 can be derived directly from various short-term securities, such as zero-coupon bonds, T-bills国库券, notes, and eurodollar deposits欧洲美元存款. However, longer-term spot rates are typically derived from the prices of long-term bonds through a bootstrapping process, taking into account the spot rates of maturities that correspond to the coupon payment date. After obtaining short-term and long-term spot rates, the yield curve can then be constructed.
Let's illustrate the bootstrapping of the yield curve with an example. The following table shows a list of bonds with different maturities and prices:
When T<=1 year, then C=0 Annual coupon (interest in dollars) payment
is replace with eR, and N==T, and R is the sport rate
∑ntT=1Cn(1+yn)T+FV(1+yn)nt if n=1 ==>∑tT=1C(1+y)T+FV(1+y)t OR
==>BP=∑Tt=1CeRt+PVTeRT
An investor in a three-month zero-coupon(interest payment C=0) bond today at $97.50 would earn interest of $2.50. The three-month spot rate can be calculated as follows:
Thus, the 3-month zero rate is 10.127% with continuous compounding. The spot rates of the zero-coupon bonds are shown in the following table:
- import numpy as np
-
- def spot_rate( price, T, par=100 ):
- return np.log(par/price)/T
-
- print( "0.25 year's Sport rate:{:.5f}".format( spot_rate( 97.5, 0.25, par=100 ) ) )
- print( "0.5 year's Sport rate(:{:.5f}".format( spot_rate( 94.9, 0.5, par=100 ) ) )
- print( "1 year's Sport rate:{:.5f}".format( spot_rate( 90, 1., par=100 ) ) )
Using these spot rates, we can now price the 1.5-year bond (> 1year) as follows:
C = semi-annual coupon (interest) payment = 8/round(1.5) = 8/2 =4
==>BP=∑Tt=1CeRt+PVTeRT
BP=4e0.10469∗0.5+4e0.10536∗1.0+4ey∗1.5+100ey∗1.5
The value of y (annual yield or rate of the bond) can easily be solved by rearranging the equation, as follows:
With the spot rate of the 1.5-year bond as 10.681%, we can use it to price the 2-year bond with coupons of $6 semi-annually, as follows:
C/2 = semiannual coupon payment = 12/2=6
BP=∑Tt=1CeRt+PVTeRT
BP=6e0.10469∗0.5+6e0.10536∗1.0+6e0.106809∗1.5+6ey∗2+100ey∗2
Rearranging the equation and solving for y gives us the spot rate of the 2-year bond as 10.808.
Through this iterative procedure of calculating the spot rate of every bond in increasing order of maturity and using it in the next iteration, we obtain a list of spot rates of different maturities that we can use to construct the yield curve.
The steps for writing the Python code to bootstrap the yield curve and running with a plot output are outlined as follows:
- import math
-
- class BootstrapYieldCurve( object ):
- def __init__(self):
- self.zero_rates = dict() # self.zero_rates[T] = spot_rate
- self.instruments = dict() # self.instruments[T] = (par, coup, price, compounding_freq)
- ################## step 1 ##################
- def add_instrument( self, par, T, coup, price, compounding_freq=2 ):
- # par : face value of the bond
- # price : current price of the bond
- # T as key : indexed by the maturity time T
- self.instruments[T] = (par, coup, price, compounding_freq)
-
- def get_maturities( self ):
- # return: a list of maturities Ts of added instruments in ascending order
- return sorted( self.instruments.keys() )
-
- # calculate the spot rate of a zero-coupon bond(interest payment=0)
- # This method is called by bootstrap_zero_coupons()
- def zero_coupon_spot_rate(self, par, price, T):
- # return: the zero coupon spot rate with continuous compounding
- spot_rate = math.log( par/price ) / T
- return spot_rate
-
- ################## step 3 ##################
- # calculate the annual yield rate of the bond when interest payment=0
- # OR
- # spot rates: zero-coupon interest rate or Zero rates
- # calculate the spot rates of the given zero-coupon bonds(interest payment=0)
- # and adds them to the zero_rates dictionary, indexed by maturity.
- def bootstrap_zero_coupons( self ):
- # Bootstrap the yield curve with zero coupon(interest payment=0) instruments first.
- for (T, instrument ) in self.instruments.items():
- # self.instruments[T] = (par, coup, price, coumpounding_freq)
- par, coup, price, freq = instrument
-
- if coup==0:
- # the annual yield or rate of the bond when interest payment=0
- spot_rate = self.zero_coupon_spot_rate( par, price, T )
- self.zero_rates[T] = spot_rate
-
- # calculate the spot rate at a particular maturity period T
- def calculate_bond_spot_rate( self, T, instrument ):
- try:
- # par : face value of the bond
- par, coup, price, freq = instrument
- periods = T*freq # 1.5 years * (1/0.5) = 1.5 * 2 = 3
- # 2 years * (1.0.5) = 2 *2 = 4
- # 8/2=4, 12/2=6 if freq=2
- per_coupon = coup/freq # annual coupon payment/frequent
- value = price # current bond price
-
- for i in range( int(periods)-1 ): # if freq=2
- t = (i+1)/float(freq) # 1/2 = 0.5, 2/2 = 1, 3/2 =1.5, ...
-
- # self.zero_rates[T] : def bootstrap_zero_coupons( self )
- spot_rate = self.zero_rates[t]
-
- # Coupon payment / e^( R*t )
- discounted_coupon = per_coupon * math.exp(-spot_rate*t)
- # value = current bond price
- value -= discounted_coupon
-
- # last_period == maturity T
- last_period = int(periods)/float(freq) # 3//2 = 1.5, 4//2=2.0
- spot_rate = -math.log( value/(par+per_coupon) )/last_period
- return spot_rate
- except:
- print("Error: spot rate not found for T=",t)
-
- ################## step 4 ##################
- # calculate the annual yield rate of the bond when interest payment !=0
- # spot rates: zero-coupon interest rate or Zero rates
- # calculate the spot rates of non zero-coupon bonds(interest payment !=0)
- # and adds them to the zero_rates dictionary, indexed by maturity T.
- def get_bond_spot_rates(self):
- """
- Get spot rates implied by bonds, using short-term instruments.
- # par : face value of the bond
- """
- for T in self.get_maturities(): # T_list in ascending order
- instrument = self.instruments[T]
- # self.instruments[T] = (par, coup, price, coumpounding_freq)
- par, coup, price, freq = instrument
-
- if coup !=0 :
- spot_rate = self.calculate_bond_spot_rate(T, instrument)
- self.zero_rates[T] = spot_rate
-
- ################## step 2 ##################
- def get_zero_rates( self ):
-
- # calculate the annual yield rate of the bond when interest payment=0
- self.bootstrap_zero_coupons()
-
- # calculate the annual yield rate of the bond when interest payment!=0
- self.get_bond_spot_rates()
-
- # return a list of zero rates in ascending order of maturity..
- # self.get_maturities() : # T_list in ascending order
- return [ self.zero_rates[T] for T in self.get_maturities() ]
Instantiate the BootstrapYieldCurve class and add each bond's information from the preceding table:
- yield_curve = BootstrapYieldCurve()
- # (par, T, coup, price, compounding_freq=2 )
- yield_curve.add_instrument(100, 0.25, 0., 97.5)
- yield_curve.add_instrument(100, 0.5, 0., 94.9)
- yield_curve.add_instrument(100, 1.0, 0., 90.)
- yield_curve.add_instrument(100, 1.5, 8, 96., 2)
- yield_curve.add_instrument(100, 2., 12, 101.6, 2)
-
- ys = yield_curve.get_zero_rates()
- ys
==>BP=∑Tt=1CeRt+PVTeRT==>
BP=4e0.10469∗0.5+4e0.10536∗1.0+4ey∗1.5+100ey∗1.5 ==>
==>
- xs = yield_curve.get_maturities()
- xs
Calling the get_zero_rates() method in the class instance returns a list of spot rates in the same order as the maturities, which are stored in the x and y variables, respectively. Issue the following Python code to plot x and y on a graph:
- %matplotlib inline
- import matplotlib.pyplot as plt
-
- fig = plt.figure( figsize=(12,8) )
-
- plt.plot(xs,ys)
- plt.title("Zero-Coupon Yield Curve(Zero Curve)", fontsize=14)
- plt.ylabel( "Zero Rate (%)", fontsize=14 )
- plt.xlabel( "Maturity in Years", fontsize=14 )
- plt.show()
In a normal yield curve environment, where the interest rates increase as the maturities increase, we obtain an upward-sloping yield curve.
############################
The forward rate is the future yield on a bond. It is calculated using the yield curve.
We are trying to find the future interest rate r1,2 for time period (t1,t2), t1 and t2 expressed in years, given
To do this, we use the property that the proceeds from
r1,2 depends on the rate calculation mode (simple, yearly compounded or continuously compounded), which yields three different results.
############################
An investor who plans to invest at a later time may be curious to know what the future interest rate will look like, as implied by today's term structure of interest rates. For example, you might ask, What is the one-year spot rate one year from now? To answer this question, you can calculate forward rates for the period between T1 and T2 using this formula:
Here, r1 and r2 are the continuously-compounded annual interest rates at time periods T1 and T2, respectively.
The following ForwardRates class helps us generate a list of forward rates from a list of spot rates:
- # Continuously compounded rate
- class ForwardRates( object ):
-
- def __init__(self):
- self.forward_rates = []
- self.spot_rates = dict()
-
- def add_spot_rate( self, T, spot_rate ):
- self.spot_rates[T] = spot_rate
-
- def calculate_forward_rate( self, T1, T2 ):
- # 0 ~ R1 ~ T1 ~ Forward rate ~ T2
- # | -- R2 -- |
- R1 = self.spot_rates[T1]
- R2 = self.spot_rates[T2]
- forward_rate = (R2*T2 - R1*T1)/(T2-T1)
-
- return forward_rate
-
- def get_forward_rates( self ):
- """
- Returns a list of forward rates
- starting from the second time period.
- """
- periods = sorted( self.spot_rates.keys() )
- for T2, T1 in zip( periods, periods[1:] ):
- forward_rate = self.calculate_forward_rate(T1, T2)
- self.forward_rates.append( forward_rate )
-
- return self.forward_rates
Using spot rates derived from our preceding yield curve, we get the following result:
- fr = ForwardRates()
-
- fr.add_spot_rate( 0.25, 10.127 )
- fr.add_spot_rate( 0.50, 10.469 )
- fr.add_spot_rate( 1.00, 10.536 )
- fr.add_spot_rate( 1.50, 10.681 )
- fr.add_spot_rate( 2.00, 10.808 )
-
- print( fr.get_forward_rates() )
Calling the get_forward_rates() method of the ForwardRates class returns a list of forward rates, starting from the next time period.
The yield to maturity (YTM到期收益率) measures the interest rate, as implied by the bond, which takes into account the present value of all the future coupon payments and the principal. It is assumed that bond holders can invest received coupons(or interest payments) at the YTM rate until the maturity of the bond; according to risk-neutral expectations, the payments received should be the same as the price paid for the bond.
BP=∑ntT=1Cn(1+yn)T+FV(1+yn)nt
Let's take a look at an example of a 5.75% bond that will mature in 1.5 years(t=1.5) with a par value of 100(C=5.75%*100=5.75). The price of the bond is $95.0428 and coupons are paid semi-annually(n=2, c=C/n= 5.75/2=2.875). The pricing equation can be stated as follows:
BP=∑ntT=1Cn(1+yn)T+FV(1+yn)nt=∑3=2∗1.5T=1Cn(1+yn)T+100(1+yn)3=∑2T=1Cn(1+yn)T+Cn+100(1+yn)3=∑2T=1c(1+yn)T+c+100(1+yn)3
To solve the YTM is typically a complex process, and most bond YTM calculators use Newton's method as an iterative process.
https://blog.csdn.net/Linli522362242/article/details/125662545
==>
The bond YTM calculator is illustrated by the following bond_ytm() function:
- import scipy.optimize as optimize
-
- def bond_ytm( price, par, T, coupon, freq=2, guess_y=0.05):
- freq = float( freq )
- periods = T*freq
-
- # dt = [ (i+1)/freq for T_i in range(int(periods)) ]
- dt = [ (i+1)/freq for i in range(int(periods))]
- # price : Bound_Price
- ytm_func = lambda y : sum([coupon/freq / (1+y/freq)**(freq*t_i)
- for t_i in dt
- ])+\
- par / (1+y/freq)**(freq*T)\
- - price
- # Bound_Price=ytm_func ==> ytm_func-Bound_Price=0
- return optimize.newton(ytm_func, guess_y)
Remember that we covered the use of Newton's method and other nonlinear function root solvers in https://blog.csdn.net/Linli522362242/article/details/125662545, Nonlinearity in Finance. For this YTM calculator function, we used the scipy.optimize package to solve the YTM.
Let's take a look at an example of a 5.75% bond(==> coupon=5.75% * par / nt) that will mature in 1.5 years with a par value of 100. The price of the bond is $95.0428 and coupons are paid semi-annually. The pricing equation can be stated as follows:
- par = 100
- T = 1.5 # deltaT = 1
- freq_in_deltaT = 2 # dt = deltaT/freq_in_deltaT ==> dt = 0.5
- coupon_at_Ti = 5.75
-
- bondPrice = 95.0428
- ytm = bond_ytm(bondPrice,
- par, T, coupon_at_Ti, freq_in_deltaT)
- print( "YTM: {}".format( ytm ) )
The YTM of the bond is 9.369%. Now we have a bond YTM calculator that can help us compare a bond's expected return with those of other securities.
BP=∑ntT=1Cn(1+yn)T+FV(1+yn)nt if n=1==>
The cash flows for the Ford bond consist of the purchase price for the bond today of $744.80, annual interest payments for Years 1 through 10 of $65, and a final interest payment of $65 plus the principal payment of $1,000, both at the end of Year 11.
- par = 1000
- T = 11 # deltaT = 1
- freq_in_deltaT = 1 # dt = deltaT/freq_in_deltaT ==> dt = 1
- coupon_at_Ti = 65 # Ti : 1,2,3...11=T
-
- bondPrice = 744.80
- ytm = bond_ytm(bondPrice,
- par, T, coupon_at_Ti, freq_in_deltaT)
- print( "YTM: {:.2f}%".format( ytm*100 ) )
The yield to maturity on the bond is 10.52 percent, considerably higher than the coupon rate of interest of 6.5 percent($65/$1000) that the bond promises to pay. The fact that the yield to maturity is higher than the coupon rate is also consistent with the fact that the current market price of the bond is considerably less than the bond’s par value of $1,000. In fact, if the bond price were equal to the bond’s par value, then the yield to maturity and coupon rate of interest would be the same.
- par = 1000
- T = 11 # deltaT = 1
- freq_in_deltaT = 1 # dt = deltaT/freq_in_deltaT ==> dt = 1
- coupon_at_Ti = 65 # Ti : 1,2,3...11=T
-
- bondPrice = 1000
- ytm = bond_ytm(bondPrice,
- par, T, coupon_at_Ti, freq_in_deltaT)
- print( "YTM: {:.2f}%".format( ytm*100 ) )
if the bond price were equal to the bond’s par value, then the yield to maturity and coupon rate of interest would be the same.
When the YTM is known, we can get back the bond price in the same way we used the pricing equation. This is implemented by the bond_price() function:
- def bond_price( par, T, ytm, coup, freq=2 ):
- freq = float(freq)
- periods = T*freq
- coupon = coup
- # dt = [ (i+1)/freq for T_i in range(int(periods)) ]
- dt = [ (i+1)/freq for i in range(int(periods))]
- # price : Bound_Price
-
- price = sum([coupon/freq / (1+ytm/freq)**(freq*t_i)
- for t_i in dt
- ])+\
- par / (1+ytm/freq)**(freq*T)
- return price
Plugging in the same values from the earlier example, we get the following result:
- par = 100
- T = 1.5 # deltaT = 1
- freq_in_deltaT = 2 # dt = deltaT/freq_in_deltaT ==> dt = 0.5
- coupon_at_Ti = 5.75 # Ti
-
- ytm_in_deltaT = 0.09369155345239522
- price = bond_price(par, T,
- ytm_in_deltaT,
- coupon_at_Ti, freq_in_deltaT)
- print("Bond price:", price)
This gives us the same original bond price discussed in the earlier example, Calculating the yield to maturity. With the bond_ytm() and bond_price() functions, we can apply these for further uses in bond pricing, such as finding the bond's modified duration and convexity. These two characteristics of bonds are of importance to bond traders in helping them formulate制定 various trading strategies and hedge their risk.
Consider a $1,000 par value bond issued by AT&T (T) with a maturity date of 2032 and a stated coupon rate of 8.5 percent. On January 1, 2013, the bond had 20 years left to maturity, and the market’s required yield to maturity for similar rated debt was 7.5 percent. What is the value of the bond?
The cash flows for the AT&T bond consist of the value of the bond today, which we are trying to estimate, the annual interest payments for Years 1 through 20 of $85 each, and principal payment at the end of Year 20 equal to $1,085 (this is in addition to the $85 interest payment also at the end of Year 20).
- par = 1000
- T = 20 # deltaT = 1
- freq_in_deltaT = 1 # dt = deltaT/freq_in_deltaT ==> dt = 1
- coupon_at_Ti = 85 # Ti : 1,2,3...20=T
-
- ytm_in_deltaT = 0.075
- price = bond_price(par, T,
- ytm_in_deltaT,
- coupon_at_Ti, freq_in_deltaT)
- print("Bond price:", price)
-
- bondPrice = 1101.94
- ytm = bond_ytm( bondPrice,
- par, T, coupon_at_Ti, freq_in_deltaT)
- print( "{:.2f}%".format( ytm*100 ) )
The value we calculated for the AT&T bond is $1,101.94, which exceeds the $1,000 par value of the bond. This reflects the fact that the market’s required yield to maturity on a comparable-risk bond is less than the coupon rate of interest paid on the bond of 8.5 percent($85/1000). In all likelihood, when the AT&T bonds were originally issued, they were issued at par because the market’s required yield to maturity on comparable-risk bonds was at or very near to the 8.5 percent coupon rate. Since the time of issue, however, market rates of interest have declined such that the bonds now sell at a premium(sell at >$1,000 par value). Thus if the bonds are sold for $1,101.94, they will yield a return of only 7.5 percent.
To illustrate this bond price formula in action, consider a 20-year, 9.5% bond priced to yield 10%. That is, the bond pays an annual coupon of 9.5% (or $95), has 20 years left to maturity, and should be priced to provide a market yield of 10%. We can now use Equation 11.3 to find the price of our bond.
- par = 1000
- T = 20 # deltaT = 1
- freq_in_deltaT = 1 # dt = deltaT/freq_in_deltaT ==> dt = 0.5
- coupon_at_Ti = 95 # Ti : 1,2,3...20=T*1 if freq_in_deltaT =1
- # Ti : 1,2,3...40=T*2 if freq_in_deltaT =2
-
- ytm_in_deltaT = 0.1
- price = bond_price(par, T,
- ytm_in_deltaT,
- coupon_at_Ti, freq_in_deltaT)
- print("Bond price:", price)
Note that because this is a coupon-bearing bond, we have an annuity of coupon payments of $95 a year for 20 years, plus a single cash flow of $1,000 that occurs at the end of year 20. Thus, we find the present value of the coupon annuity and then add that amount to the present value of the recovery of principal at maturity. In this particular case, you should be willing to pay about $958 for this bond, so long as you’re satisfied with earning 10% on your money.
Notice that this bond trades at a discount of $42.57 ($1,000 – $957.43). It trades at a discount because its coupon rate (9.5%) is below the market’s required return (10%). You can directly link the size of the discount on this bond to the present value of the difference between the coupons that it pays ($95) and the coupons that would be required if the bond matched the market’s 10% required return ($100). In other words, this bond’s coupon payment is $5 less than what the market requires, so if you take the present value of that difference over the bond’s life, you will calculate the size of the bond’s discount:
semiannual basis ==>
Reconsider the bond issued by AT&T (T) with a maturity date of 2032 and a stated coupon rate of 8.5 percent. AT&T pays interest to bondholders on a semiannual basis on January 15 and July 15. On January 1, 2013, the bond had 20 years left to maturity. The market’s required yield to maturity for a similarly rated debt was 7.5 percent per year or 3.75 percent(3.75%=7.5%/2.0) for six months. What is the value of the bond?
The cash flows for the AT&T bond consist of the value of the bond today, which we are trying to estimate, the semiannual interest payments for Periods 1 through 39 of $42.50 each($42.50=$85/2), and a final interest-plus-principal pay-ment at the end of Year 20 or Period 40 equal to $1,042.50.
- par = 1000
- T = 20 # deltaT = 1
- freq_in_deltaT = 2 # dt = deltaT/freq_in_deltaT ==> dt = 0.5
- coupon_at_Ti = 85 # Ti : 1,2,3...20=T*1 if freq_in_deltaT =1
- # Ti : 1,2,3...40=T*2 if freq_in_deltaT =2
-
- ytm_in_deltaT = 0.075
- price = bond_price(par, T,
- ytm_in_deltaT,
- coupon_at_Ti, freq_in_deltaT)
- print("Bond price:", price)
-
- bondPrice = 1102.75
- ytm = bond_ytm(bondPrice,
- par, T, coupon_at_Ti, freq_in_deltaT)
- print( "YTM: {:.2f}%".format( ytm*100 ) )
- print( "YTM/{}: {:.2f}%".format( freq_in_deltaT, ytm*100/freq_in_deltaT ) )
Using semiannual compounding, we calculate a value of $1,102.75 for the AT&T bond compared to the $1,101.94 we calculated using annual compounding—this value is not vastly different没有太大的不同, but for a large investor buying thousands of bonds, this can certainly add up over time.
###################
BP=∑ntT=1Cn(1+yn)T+FV(1+yn)nt==>
Duration is a measure of the average maturity of a fixed-income security. The term average maturity may be confusing because bonds have only one final maturity date. An alternative definition of average maturity might be that it captures the average timing of the bond’s cash flow.
Duration is a measure that puts some weight on these intermediate payments, so that the “average maturity” is a little less than 10 years.
Equation 11.8 Macaulay duration
where
The duration measure obtained from Equation 11.8 is commonly referred to as Macaulay duration—named after the actuary who developed the concept.
Although duration is often computed using semiannual compounding, Equation 11.8 uses annual coupons and annual compounding to keep the ensuing discussion and calculations as simple as possible. Even so, the formula looks more formidable than it really is. If you follow the basic steps noted below, you’ll find that duration is not tough to calculate.
Table 11.1 (on page 435) illustrates the four-step procedure for calculating the duration of a 7.5%(n=1, C = 7.5% * FV /n = 7.5%*1000 = $75), 15-year bond priced at $957.20 to yield 8%.
Table 11.1 provides the basic input data:
In addition, keep in mind that the duration on any bond will change over time as YTM and term to maturity change. For example, the duration on this 7.5%, 15-year bond will fall as the bond nears maturity and/or as the market yield (YTM) on the bond increases.
A bond’s price volatility is, in part, a function of its term to maturity and, in part, a function of its coupon. Unfortunately, there is no exact relationship between bond maturities and bond price volatilities with respect to interest rate changes. There is, however, a fairly close relationship between bond duration and price volatility—as long as the market doesn’t experience wide swings in interest rates. A bond’s duration can be used as a viable predictor of its price volatility only so long as the yield swings are relatively small (no more than 50 to 100 basis points, or so). That’s because as interest rates change, bond prices change in a nonlinear (convex) fashion. For example,
The duration measure essentially predicts that as interest rates change, bond prices will move in the opposite direction in a linear fashion. This means that
The bottom line最重要的是 is that the duration measure helps investors understand how bond prices will respond to changes in market rates, as long as those changes are not too large.
The mathematical link between changes in interest rates and changes in bond prices involves the concept of modified duration. To find modified duration, we simply take the (Macaulay) duration for a bond (as found from Equation 11.8andPV(Ct)=Ct+FV||Ct(1+YTM)t) and adjust it for the bond’s yield to maturity.
Equation 11.9
Thus, the modified duration for the 15-year bond discussed above is Note that here we use the bond’s computed (Macaulay) duration of 9.36 years and the same YTM we used to compute duration in Equation 11.8; in this case, the bond was priced to yield 8%, so we use a yield to maturity of 8%.
To determine, in percentage terms, how much the price of this bond would change as market interest rates increased by 50 basis points from 8% to 8.5%,
we multiply the modified duration value calculated above first by –1 (because of the inverse relationship between bond prices and interest rates) and then by the change in market interest rates. That is,
Thus, a 50-basis-point (or 0.5% = ½ of 1%) increase in market interest rates will lead to an approximate 4.33% drop in the price of this 15-year bond. Such information is useful to bond investors seeking—or trying to avoid—price volatility.
One problem with the duration measures that we’ve studied so far is that they do not always work well for bonds with embedded options such as callable bonds. That is, the duration measures we’ve been using assume that the bond’s future cash flows (Ct+FV or Ct) are paid as originally scheduled through maturity, but that may not be the case with callable bonds (or convertible bonds or bonds with other types of options attached to them). An alternative duration measure that is used for these types of bonds is the effective duration. To calculate effective duration (ED), you use Equation 11.11:
VS
= the new price of the bond if market interest ratesri go down
the price of the bond from a decrease in yield by dY
Suppose you want to know the effective duration of a 25-year bond that pays a 6% coupon semiannually(2). The bond is currently priced at $882.72 for a yield of 7%.
- par = 1000
- T = 25 # deltaT = 1
- freq_in_deltaT = 2 # dt = deltaT/freq_in_deltaT ==> dt = 0.5
- coupon_at_Ti = 60 # Ti : 1,2,3...20=T*1 if freq_in_deltaT =1
- # Ti : 1,2,3...40=T*2 if freq_in_deltaT =2
-
- ytm_in_deltaT = 0.07
- price = bond_price(par, T,
- ytm_in_deltaT,
- coupon_at_Ti, freq_in_deltaT)
- print("Bond price:", price)
BP
Now suppose the bond’s yield goes up by 0.5%
( use 50-basis-point = 50% * 1% = 0.5% change in yield )
change in yield) to 7.5%. At that yield the new price would be $831.74 (using a calculator, N = 50 = 25*2, I = 3.75 = 7%/2, PMT = 30 = 6%/2 * 1000, and PV = 1,000).
- par = 1000
- T = 25 # deltaT = 1
- freq_in_deltaT = 2 # dt = deltaT/freq_in_deltaT ==> dt = 0.5
- coupon_at_Ti = 60 # Ti : 1,2,3...20=T*1 if freq_in_deltaT =1
- # Ti : 1,2,3...40=T*2 if freq_in_deltaT =2
-
- ytm_in_deltaT = 0.075
- price = bond_price(par, T,
- ytm_in_deltaT,
- coupon_at_Ti, freq_in_deltaT)
- print("Bond price:", price)
What if the yield drops by 0.5%
( use 50-basis-point = 50% * 1% = 0.5% change in yield )
to 6.5%? In that case, the price rises to $938.62 (N = 50, I = 3.25, PMT = 30, PV = 1,000).
- par = 1000
- T = 25 # deltaT = 1
- freq_in_deltaT = 2 # dt = deltaT/freq_in_deltaT ==> dt = 0.5
- coupon_at_Ti = 60 # Ti : 1,2,3...20=T*1 if freq_in_deltaT =1
- # Ti : 1,2,3...40=T*2 if freq_in_deltaT =2
-
- ytm_in_deltaT = 0.065
- price = bond_price(par, T,
- ytm_in_deltaT,
- coupon_at_Ti, freq_in_deltaT)
- print("Bond price:", price)
Now we can use Equation 11.11 to calculate the bond’s effective duration.
Effective duration = 938.62−831.742×882.72×0.005=12.11
This means that if interest rates rise or fall by a full percentage point一个百分点(50-basis-point = 50% * 1% = 0.5% change in yield, 0.5%*2=1%), the price of the bond would fall or rise by approximately 12.11%.
Note that you can use effective duration in place of modified duration in Equation 11.10to find the percent change in the price of a bond when interest rates move by more or less than 1.0%.
When calculating the effective duration of a callable bond, one modification may be necessary.
###################
Duration is a sensitivity measure of bond prices to yield changes. Some duration measures are
The type of duration that we will discuss is modified duration, which measures the percentage change in bond price with respect to a percentage change in yield衡量债券 价格的百分比变化 相对于 收益率百分比变化 (typically 1% or 100 basis points (bps)).
The modified duration of a bond can be thought of as the first derivative of the relationship between price and yield:
VS
Here:
It should be noted that the duration describes the linear price-yield relationship for a small change in Y. Because the yield curve is not linear, using a large value of dY does not approximate the duration measure well.
The implementation of the modified duration(should be called Effective duration) calculator is given in the following bond_mod_duration() function.
- def bond_mod_duration( price, par, T, coup, freq, dy=0.01 ):
- # dy : the change in market interest rates
- # 0.01 : 1-basis-point change in yield
- ytm = bond_ytm( price, par, T, coup, freq )
-
- ytm_minus = ytm - dy
- price_minus = bond_price( par, T, ytm_minus, coup, freq )
- # the price of the bond from a decrease in yield by dY
- # or the new price of the bond if market interest rate(ytm) go down
- print( 'the price of the bond from a decrease in yield by {}: {:.4f}'.format(dy, price_minus) )
-
- ytm_plus = ytm + dy
- price_plus = bond_price( par, T, ytm_plus, coup, freq )
- # the price of the bond from an increase in yield by dY
- # the new price of the bond if market interest rate(ytm) go up
- print( 'the price of the bond from an increase in yield by {}: {:.4f}'.format(dy, price_plus) )
-
- mduration = ( price_minus-price_plus )/( 2*price*dy )
-
- return mduration
We can find out the modified duration of the 5.75% bond(coupon payment: 5.75%*100=$5.75) discussed earlier, in Calculating the yield to maturity, which will mature in 1.5 years(t) with a par value of 100 and a bond price of 95.0428(BP):
BP=∑ntT=1Cn(1+yn)T+FV(1+yn)ntn=2 since this bond will pay a fixed amount of interest semi-annually,
vs
- mod_duration = bond_mod_duration( 95.0428, 100, 1.5, 5.75, 2 )
- print( mod_duration )
The modified duration of the bond is 1.392 years(< the maturity of bond 1.5 yr). Duration is a measure of the average maturity of a fixed-income security. The term average maturity may be confusing because bonds have only one final maturity date. An alternative definition of average maturity might be that it captures the average timing of the bond’s cash flow.
Suppose you want to know the effective duration of a 25-year bond that pays a 6% coupon semiannually(n=2). The bond is currently priced at $882.72 for a yield of 7%.
Now suppose the bond’s yield goes up by 0.5%
( use 50-basis-point = 50% * 1% = 0.5% change in yield ) change in yield )
to 7.5%. At that yield the new price would be $831.74 (using a calculator, N = 50 = 25*2, I = 3.75 = 7%/2, PMT = 30 = 6%/2 * 1000, and PV = 1,000).
What if the yield drops by 0.5% to 6.5%? In that case, the price rises to $938.62 (N = 50, I = 3.25, PMT = 30, PV = 1,000).BP=∑ntT=1Cn(1+yn)T+FV(1+yn)nt
- par = 1000
- T = 25 # deltaT = 1
- freq_in_deltaT = 2 # dt = deltaT/freq_in_deltaT ==> dt = 0.5
- coupon_at_Ti = 60 # Ti : 1,2,3...20=T*1 if freq_in_deltaT =1
- # Ti : 1,2,3...40=T*2 if freq_in_deltaT =2
-
- ytm_in_deltaT = 0.07
-
- bondPrice = 882.72
-
- dy=0.005 # use 50-basis-point = 50% * 1% = 0.5% change in yield
- mod_duration = bond_mod_duration( bondPrice, par, T, coupon_at_Ti, freq_in_deltaT, dy)
- print( mod_duration )
This means that if interest rates rise or fall by a full percentage point一个百分点(50-basis-point = 50% * 1% = 0.5% change in yield, 0.5%*2=1%), the price of the bond would fall or rise by approximately 12.11%.
Convexity is the sensitivity measure of the duration of a bond to yield changes衡量债券久期对收益率变化的敏感度. Think of convexity as the second derivative of the relationship between the price and yield:
Bond traders use convexity as a risk-management tool to measure the amount of market risk in their portfolio衡量其投资组合中的市场风险量.
The implementation of a bond convexity is given as follows:
- def bond_convexity( price, par, T, coup, freq, dy=0.01 ):
- # dy : the change in market interest rates
- # 0.01 : 1-basis-point change in yield
- ytm = bond_ytm( price, par, T, coup, freq )
-
- ytm_minus = ytm-dy
- price_minus = bond_price( par, T, ytm_minus, coup, freq )
- # the price of the bond from a decrease in yield by dY
- # or the new price of the bond if market interest rate(ytm) go down
- print( 'the price of the bond from a decrease in yield by {}: {:.4f}'.format(dy, price_minus) )
-
- ytm_plus = ytm + dy
- price_plus = bond_price(par, T, ytm_plus, coup, freq)
- # the price of the bond from an increase in yield by dY
- # the new price of the bond if market interest rate(ytm) go up
- print( 'the price of the bond from an increase in yield by {}: {:.4f}'.format(dy, price_plus) )
-
- convexity = (price_minus + price_plus - 2*price)/(price*dy**2)
-
- return convexity
We can now find the convexity of the 5.75% bond discussed earlier, in Calculating the yield to maturity section, which will mature in 1.5 years with a par value of 100 and a bond price of 95.0428:
- convexity = bond_convexity( 95.0428, 100, 1.5, 5.75, 2 )
- print( convexity )
The convexity of the bond is 2.63. For two bonds with the same par value(FV), couponCn, and maturity(t), their convexity may be different, depending on their location on the yield curve.BP=∑ntT=1Cn(1+yn)T+FV(1+yn)ntHigher-convexity bonds will exhibit higher price changes for the same change in yield(dY).
In short-rate modeling, the short-rate, r(t), is the spot rate即期利率 at a particular time. It is described as a continuously-compounded连续复利, annualized interest rate term for an infinitesimally short period of time on the yield curve. The short-rate takes on the form of a stochastic variable in interest-rate models, where the interest rates may change by small amounts at every point in time. Short-rate models attempt to model the evolution of interest rates over time, and hopefully describe the economic conditions at certain periods.
Short-rate models are frequently used in the evaluation of interest-rate derivatives利率衍生品的评估. Bonds, credit instruments, mortgages, and loan products are sensitive to interest-rate changes. Short-rate models are used as interest rate components in conjunction with pricing implementations, such as numerical methods, to help price such derivatives.
Interest-rate modeling is considered a fairly complex topic since interest rates are affected by a multitude of factors, such as
A number of interest-rate models have been proposed to account for various characteristics of interest rates.
In this section, we will take a look at some of the most popular one-factor short-rate models单因素短期利率模型 used in financial studies, namely,
Using Python, we will perform a one-path simulation to obtain a general overview of the interest-rate path process. Other models commonly discussed in finance include the Ho-Lee, Hull-White, and Black-Karasinki.
#########################
以W(t)表示运动中一微粒从时刻t=0到时刻t>0的位移的横坐标(同样也可以讨论纵坐标),且设W0=0,根据爱因斯坦1905年提出的理论,微粒的这种运动是由于受到大量随机的相互独立的分子的碰撞的结果. 于是,粒子在时段(s,t]上的位移可以看作是许多微小位移的代数和. 则Wt−Ws服从正态分布。
The Wiener process Wt is characterised by the following properties:
That the process has independent increments means that if then Wt1−Ws1 andWt2−Ws2 are independent random variables, and the similar condition holds for n increments.
Basic properties of a one-dimensional Wiener process
The unconditional probability density function follows a normal distribution with mean = 0 and variance = t, at a fixed time t:
normal distribution:
The expectation is zero:
The variance, using the computational formula, is t:
These results follow immediately from the definition that increments have a normal distribution, centered at zero. Thus
######################
In finance, the Vasicek model is a mathematical model describing the evolution of interest rates. It is a type of one-factor short-rate model as it describes interest rate movements as driven by only one source of market risk. The model can be used in the valuation估值 of interest rate derivatives, and has also been adapted for credit markets. It was introduced in 1977 by Oldřich Vašíček,[1] and can be also seen as a stochastic investment model.
In the one-factor Vasicek model, the short-rate is modeled as a single stochastic factor单个随机因素:
r(t) Process level at date t
The drift coefficient K∗(θ−rt) depends on the current value of r(t).
If r(t) is less than θ, then the drift coefficient is positive;
if r(t) is greater than θ, then it is negative. Thus, the interest-rate tends to revert to its reversion level θ at a rate which depends upon the speed of adjustment K
θ Long-term mean of the process
All future trajectories of r(t) will evolve around a mean level θ in the long run;
K Mean-reversion factor
"speed of reversion回归速度". K characterizes the velocity at which such trajectories will regroup around θ in time; K 表征这些轨迹将在时间 θ周围重新组合的速度
Here, K, θ, and σ are constants, and σ is the instantaneous/ˌɪnstənˈteɪniəs/ standard deviation. W(t) is the random Wiener process. The Vasicek follows an Ornstein–Uhlenbeck stochastic process, where the model reverts around the mean, θ, with K, the speed of mean reversion. As a result, the interest rates(rt) may become negative, which is an undesirable不受欢迎的 property in most normal economic conditions.
(Making the long term meanθ stochastic to another SDE is a simplified version of the cointelation SDEhttps://blog.csdn.net/Linli522362242/article/details/96593981)
https://blog.csdn.net/Linli522362242/article/details/125355166
since and are identical in distribution, where Z is a standard normal variable.(==
)
drt=K∗(θ−rt)∗dt+σ∗√dt∗dZt
rt+Δt=rt+drt
To help us understand this model, the following code generates a list of interest rates:
- import math
- import numpy as np
-
- def vasicek( r0, K, theta, sigma, T=1., N=10, seed=777 ):
- # r0 : the initial rate of interest at t=0
- # K : "speed of Mean-reversion
- # theta : All future trajectories of r(t) will evolve around a 'mean' level 'theta' in the long run
- # sigma : determines the volatility of the interest rate and amplitude(range of r(t))
- # T : the period in terms of number of years
- # N : the number of intervals for the modeling process
- # W : a Wiener process(Standard Brownian motion)
- # modelling the random market risk factor(under the risk neutral framework)
- np.random.seed( seed )
-
- dt = T/float(N)
- rates = [r0]
- # Z is a standard normal variable
- for i in range(N): # dW = sqrt(dt) * dZ
- dr = K * (theta - rates[-1]) * dt + sigma* math.sqrt(dt) * np.random.normal()
- rates.append( rates[-1]+dr )
- # returns a list of time periods and interest rates
- return range(N+1), rates
The vasicek() function returns a list of time periods and interest rates from the Vasicek model. It takes in a number of input arguments: r0 is the initial rate of interest at t=0; K , theta , and sigma are constants; T is the period in terms of number of years; N is the number of intervals for the modeling process; and seed is the initialization value for NumPy's standard normal random-number generator.
Assume that the current interest rate is close to zero at 0.5%, the long term mean level theta is 0.15, and the instantaneous volatility sigma is 5%. We will use a T value of 10 and an N value of 200 to model the interest rates for different speeds of mean reversion, K, with values of 0.002, 0.02, and 0.2 :
- %matplotlib inline
- import matplotlib.pyplot as plt
-
- fig = plt.figure( figsize=(12,8) )
-
- # r0 : the initial rate of interest at t=0
- r0 = 0.005
- # K : "speed of Mean-reversion
- # theta : All future trajectories of r(t) will evolve around a 'mean' level 'theta' in the long run
- theta = 0.15
- # sigma : determines the volatility of the interest rate and amplitude(range of r(t))
- sigma = 0.05
- # T : the period in terms of number of years
- T = 10
- # N : the number of intervals for the modeling process
- N = 200
- # K : "speed of Mean-reversion
-
- K_list = [0.002, 0.02, 0.2]
- colors = ['k','g','b']
-
- for index, K in enumerate(K_list):
- N_plus_1, rate_list = vasicek( r0, K, theta ,sigma, T, N )
- plt.plot( N_plus_1, rate_list,
- label='K=%s' % K,
- c = colors[index]
- )
-
- plt.axhline(y=theta, ls='--', c='r') # Long-term mean(theta) of the process
- plt.annotate( r'Long-term mean(${\theta}$)',
-
- ha='right',
-
- va='top',
-
- xytext=(80, theta - 0.01), #The xytext parameter specifies the text position
-
- xy=(80, theta), #The xy parameter specifies the arrow's destination
-
- arrowprops=dict(facecolor='blue', headwidth=10, headlength=4, width=2 ),
-
- #arrowprops={'facecolor':'blue', 'headwidth':10, 'headlength':4, 'width':2} #OR
- fontsize=14
- )
-
- plt.legend( loc='upper left' , fontsize=14)
- plt.ylabel('The interest rate', fontsize=14)
- plt.xlabel('The time t', fontsize=14)
- plt.title('Vasicek model', fontsize=14)
- plt.show()
After running the preceding commands, we get the following graph:
In this example, we are running just one simulation to see what the interest rates from the Vasicek model looks like. Observe that interest rates did become negative at some point. With higher levels of speed for mean reversion, K , the process reaches its long-term level of 0.15(the model reverts around the mean, θ=0.15) sooner.
The Cox-Ingersoll-Ross (CIR) model is a one-factor model that was proposed to address the negative interest rates found in the Vasicek model.(It is a type of one-factor short-rate model as it describes interest rate movements as driven by only one source of market risk)==>drt=K∗(θ−rt)∗dt+σ∗√dt∗dZt. The process is given as follows:
==>drt=K∗(θ−rt)∗dt+σ∗√rt∗√dt∗dZt
rt+Δt=rt+drt
r(t) Process level at date t
The drift coefficient K∗(θ−rt) depends on the current value of r(t).
If r(t) is less than θ, then the drift coefficient is positive;
if r(t) is greater than θ, then it is negative. Thus, the interest-rate tends to revert to its reversion level θ at a rate which depends upon the speed of adjustment K
θ Long-term mean of the process
All future trajectories of r(t) will evolve around a mean level θ in the long run;
K Mean-reversion factor
"speed of reversion回归速度". K characterizes the velocity at which such trajectories will regroup around θ in time; K 表征这些轨迹将在时间 θ周围重新组合的速度
The term √r(t) increases the standard deviation as the short-rate increases. Now the vasicek() function can be rewritten as the CIR model in Python:
- import math
- import numpy as np
-
- def CIR( r0, K, theta, sigma, T=1, N=10, seed=777 ):
- # r0 : the initial rate of interest at t=0
- # K : "speed of Mean-reversion
- # theta : All future trajectories of r(t) will evolve around a 'mean' level 'theta' in the long run
- # sigma : determines the volatility of the interest rate and amplitude(range of r(t))
- # T : the period in terms of number of years
- # N : the number of intervals for the modeling process
- # W : a Wiener process(Standard Brownian motion)
- # modelling the random market risk factor(under the risk neutral framework)
- np.random.seed(seed)
-
- dt = T/float(N)
- rates = [r0]
- # dW = sqrt(dt) * dZ
- for i in range(N): # Z is a standard normal variable
- dr = K * ( theta-rates[-1] ) * dt +\
- sigma * math.sqrt( rates[-1] ) * math.sqrt(dt)*np.random.normal()
- rates.append( rates[-1] + dr )
-
- # returns a list of time periods and interest rates
- return range(N+1), rates
Using the same example given in the The Vasicek model section, assume that the current interest rate is 0.5%, theta is 0.15 , and sigma is 0.05 . We will use a T value of 10 and N of 200 to model the interest rates with different speeds of mean reversion, K , using values of 0.002 , 0.02 , and 0.2:
- %matplotlib inline
- import matplotlib.pyplot as plt
-
- fig = plt.figure( figsize=(12,8) )
-
- # r0 : the initial rate of interest at t=0
- r0 = 0.005
- # K : "speed of Mean-reversion
- # theta : All future trajectories of r(t) will evolve around a 'mean' level 'theta' in the long run
- theta = 0.15
- # sigma : determines the volatility of the interest rate and amplitude(range of r(t))
- sigma = 0.05
- # T : the period in terms of number of years
- T = 10
- # N : the number of intervals for the modeling process
- N = 200
- # K : "speed of Mean-reversion
-
- K_list = [0.002, 0.02, 0.2]
- colors = ['k','g','b']
-
- for index, K in enumerate(K_list):
- N_plus_1, rate_list = CIR( r0, K, theta ,sigma, T, N )
- plt.plot( N_plus_1, rate_list,
- label='K=%s' % K,
- c = colors[index]
- )
-
- plt.axhline(y=theta, ls='--', c='r') # Long-term mean(theta) of the process
-
- plt.annotate( r'Long-term mean(${\theta}$)',
-
- ha='left',
-
- va='top',
-
- xytext=(50, theta - 0.01), #The xytext parameter specifies the text position
-
- xy=(50, theta), #The xy parameter specifies the arrow's destination
-
- arrowprops=dict(facecolor='blue', headwidth=10, headlength=4, width=2 ),
-
- #arrowprops={'facecolor':'blue', 'headwidth':10, 'headlength':4, 'width':2} #OR
- fontsize=14
- )
-
-
- plt.legend( loc='upper left' , fontsize=14)
- plt.ylabel('The interest rate$(R_t)$', fontsize=14)
- plt.xlabel('the time t', fontsize=14)
- plt.title('Cox-Ingersoll-Ross model', fontsize=14)
- plt.show()
"speed of reversion回归速度". K characterizes the velocity at which such trajectories will regroup around θ in time; K 表征这些轨迹将在时间 θ周围重新组合的速度 With higher levels of speed for mean reversion, K , the process reaches its long-term level of 0.15(the model reverts around the mean, θ=0.15) sooner.
Observe that the CIR interest model does not have negative interest rate values.
The Rendleman and Bartter model (Richard J. Rendleman, Jr. and Brit J. Bartter) in finance is a short-rate model describing the evolution of interest rates. It is a "one factor model" as it describes interest rate movements as driven by only one source of market risk. It can be used in the valuation估值 of interest rate derivatives. It is a stochastic asset model.
In the Rendleman and Bartter model, the short-rate process is given as follows:
drt=θ∗rt∗dt+σ∗rt∗√dt∗dZt
rt+Δt=rt+drt
r(t) Process level at date t
θ Long-term mean of the process
All future trajectories of r(t) will evolve around a mean level θ in the long run;
Here, the instantaneous drift is θ*r(t) with an instantaneous standard deviation, σ*r(t). The Rendleman and Bartter model can be thought of as a geometric Brownian motion, akin to a stock price stochastic process that is log-normally distributed(https://blog.csdn.net/Linli522362242/article/details/96593981). This model lacks the property of mean reversion( no K : "speed of reversion回归速度". K characterizes the velocity at which such trajectories will regroup around θ in time; K 表征这些轨迹将在时间 θ周围重新组合的速度). Mean reversion is a phenomenon where the interest rates seem to be pulled back toward a long-term average levelθ.
The following Python code models the Rendleman and Bartter interest-rate process:
- import math
- import numpy as np
-
- def rendleman_bartter( r0, theta, sigma, T=1, N=10, seed=777 ):
- # r0 : the initial rate of interest at t=0
- # theta : All future trajectories of r(t) will evolve around a 'mean' level 'theta' in the long run
- # sigma : determines the volatility of the interest rate and amplitude(range of r(t))
- # T : the period in terms of number of years
- # N : the number of intervals for the modeling process
- # W : a Wiener process(Standard Brownian motion)
- # modelling the random market risk factor(under the risk neutral framework)
-
- np.random.seed( seed )
- dt = T/float(N)
-
- rates = [r0] # dW = sqrt(dt) * dZ
- for i in range( N ): # Z is a standard normal variable
- dr = theta*rates[-1]*dt + sigma*rates[-1]*math.sqrt(dt)*np.random.normal()
-
- rates.append( rates[-1] + dr )
- # returns a list of time periods and interest rates
- return range(N+1), rates
We will continue to use the example from the previous sections and compare the model.
Assume that the current interest rate is 0.5%, and sigma σ is 0.05 . We will use a T value of 10 and N of 200 to model the interest rates with different instantaneous drift, thetaθ, using values of 0.01 , 0.05 , and 0.1:
- %matplotlib inline
- import matplotlib.pyplot as plt
-
- fig = plt.figure( figsize=(12,8) )
-
- # r0 : the initial rate of interest at t=0
- r0 = 0.005
- # sigma : determines the volatility of the interest rate and amplitude(range of r(t))
- sigma = 0.05
- # T : the period in terms of number of years
- T = 10
- # N : the number of intervals for the modeling process
- N = 200
-
- colors = ['k','g','b']
-
- # theta : All future trajectories of r(t) will evolve around a 'mean' level 'theta' in the long run
- theta_list = [0.01, 0.05, 0.1]
-
- for index, theta in enumerate(theta_list):
- N_plus_1, rate_list = rendleman_bartter( r0, theta, sigma, T, N )
- plt.plot( N_plus_1, rate_list,
- label=r'${\theta}$=%s' % theta,
- c = colors[index]
- )
-
- plt.annotate( r'Long-term mean(${\theta}$=0.01)',
-
- ha='left',
-
- va='top',
-
- xytext=(50, 0.01 + 0.001), # The xytext parameter specifies the text position
-
- xy=(50, 0.01), # The xy parameter specifies the arrow's destination
-
- arrowprops=dict(facecolor=colors[index], headwidth=10, headlength=4, width=2 ),
-
- # arrowprops={'facecolor':'blue', 'headwidth':10, 'headlength':4, 'width':2} #OR
- fontsize=14
- )
- plt.axhline(y=0.01, ls='--', c=colors[0]) # Long-term mean(theta) of the process
-
- plt.legend( loc='upper left' , fontsize=14)
- plt.ylabel('The interest rate$(R_t)$', fontsize=14)
- plt.xlabel('the time t', fontsize=14)
- plt.title('Rendleman and Bartter model', fontsize=14)
- plt.show()
In general, this model lacks the property of mean reversion and grows toward a long-term average levelθ.
The Brennan and Schwartz model is a two-factor model where the short-rater(t) reverts toward a long-term rate as the meanθ, which also follows a stochastic process. (The Brennan and Schwartz model is a two factors model that models the dynamics of the short and long term rates. , ==>
, ==>Cov(dWt1,dWt2)=ρdt http://www.ericbenhamou.net/documents/Encyclo/Brennan%20and%20Schwartz%20_1982_%20model.pdf
OR https://web.mst.edu/~bohner/fim-10/fim-chap6.pdf
)
The short-rate process is given as follows(
The Brennan-Schwartz model is one of the one-factor stochastic differential equation models and it represents the dynamics of the short-rate. It is a "one factor model" as it describes interest rate movements as driven by only one source of market risk): http://trihandika.staff.gunadarma.ac.id/Publications/files/1558/ICREM.pdf
drt=K∗(θ−rt)∗dt+σ∗rt∗dWt==>
drt=K∗(θ−rt)∗dt+σ∗rt∗√dt∗dZt
rt+Δt=rt+drt
r(t) Process level at date t,
K∗(θ−rt) is called the drift coefficient
σ∗rt is called diffusion coefficient(扩散系数) of the process
The drift coefficient K∗(θ−rt) depends on the current value of r(t).
If r(t) is less than θ, then the drift coefficient is positive;
if r(t) is greater than θ, then it is negative. Thus, the interest-rate tends to revert to its reversion level θ at a rate which depends upon the speed of adjustment K
θ Long-term mean of the process
All future trajectories of r(t) will evolve around a mean level θ in the long run;
K Mean-reversion factor
"speed of reversion回归速度". K characterizes the velocity at which such trajectories will regroup around θ in time; K 表征这些轨迹将在时间 θ周围重新组合的速度
It can be seen that the Brennan and Schwartz model is another form of a geometric Brownian motion(https://blog.csdn.net/Linli522362242/article/details/125355166).
- import math
- import numpy as np
-
- def rendleman_bartter( r0, theta, sigma, T=1, N=10, seed=777 ):
- # r0 : the initial rate of interest at t=0
- # theta : All future trajectories of r(t) will evolve around a 'mean' level 'theta' in the long run
- # sigma : determines the volatility of the interest rate and amplitude(range of r(t))
- # T : the period in terms of number of years
- # N : the number of intervals for the modeling process
- # W : a Wiener process(Standard Brownian motion)
- # modelling the random market risk factor(under the risk neutral framework)
-
- np.random.seed( seed )
- dt = T/float(N)
-
- rates = [r0] # dW = sqrt(dt) * dZ
- for i in range( N ): # Z is a standard normal variable
- dr = theta*rates[-1]*dt + sigma*rates[-1]*math.sqrt(dt)*np.random.normal()
-
- rates.append( rates[-1] + dr )
- return range(N+1), rates
Assume that the current interest rate remains at 0.5%, and the long-term mean level of theta is 0.006. sigma is 0.05 . We will use a T value of 10 and N of 200 to model the interest rates with different speeds of mean reversion, K , using values of 0 2 , 0.02 , and 0.002
- %matplotlib inline
- import matplotlib.pyplot as plt
-
- fig = plt.figure( figsize=(12,8) )
-
- # r0 : the initial rate of interest at t=0
- r0 = 0.005
- # K : "speed of Mean-reversion
- # theta : All future trajectories of r(t) will evolve around a 'mean' level 'theta' in the long run
- theta = 0.006 # long-term mean level of theta
- # sigma : determines the volatility of the interest rate and amplitude(range of r(t))
- sigma = 0.05
- # T : the period in terms of number of years
- T = 10
- # N : the number of intervals for the modeling process
- N = 200
- # K : "speed of Mean-reversion
-
- K_list = [0.2, 0.02, 0.002]
- colors = ['k','g','b']
-
- for index, K in enumerate(K_list):
- N_plus_1, rate_list =brennan_schwartz( r0, K, theta ,sigma, T, N )
- plt.plot( N_plus_1, rate_list,
- label='K=%s' % K,
- c = colors[index]
- )
-
- plt.axhline(y=theta, ls='--', c='r') # Long-term mean(theta) of the process
-
- plt.annotate( r'Long-term mean(${\theta}$=%s)' % theta,
-
- ha='center',
-
- va='top',
-
- xytext=(50, theta - 0.0001), #The xytext parameter specifies the text position
-
- xy=(50, theta), #The xy parameter specifies the arrow's destination
-
- arrowprops=dict(facecolor='blue', headwidth=10, headlength=4, width=2 ),
-
- #arrowprops={'facecolor':'blue', 'headwidth':10, 'headlength':4, 'width':2} #OR
- fontsize=14
- )
-
-
- plt.legend( loc='upper left' , fontsize=14)
- plt.ylabel('The interest rate$(R_t)$', fontsize=14)
- plt.xlabel('the time t', fontsize=14)
- plt.title('Brennan and Schwartz model', fontsize=14)
- plt.show()
When K is 0.2, the speed of the mean reversion is fastest toward the long-term mean of 0.006.
When bond issuers, such as corporations, issue bonds, one of the risks they face is the interest rate risk.
To capitalize on利用 interest-rate changes, bond issuers may embed options within a bond. This allows the issuer the right, but not the obligation, to buy or sell the issued bond at a predetermined price during a specified period of time. An American type of bond option allows the issuer to exercise the rights of the option at any point in time during the lifetime of a bond. A European type of bond option allows the issuer to exercise the rights of the option on a specific date. The exact style of the date of exercise行使日期的确切样式 varies from bond option to bond option. Some issuers may choose to exercise the right of the bond option when the bond has been in circulation流通 in the market for over a year. Some issuers may choose to exercise the bond option on one of several specific dates. Regardless of the exercise dates of the bond, you may price the bond with an embedded option, as follows:
Price of bond = price of bond with no option - price of embedded option
The pricing of a bond with no option is fairly straightforward: the present value of the bond to be received at a future date, including all coupon payments. A number of assumptions are to be made about the theoretical interest rates in the future at which the coupon payments may be reinvested.
To price an option, one would have to determine available exercise dates. Starting from the future value of the bond, the bond price is compared against the exercise price of the option and traverses back to the present time using a numerical procedurehttps://blog.csdn.net/Linli522362242/article/details/125355166, such as a binomial tree. This price comparison is performed at time points when the bond option may be exercised. With the no-arbitrage theory, accounting for the present excess values of the bond when exercised, we obtain the price of the option. For simplicity, in bond-pricing studies in the later section of this chapter, Pricing callable bond options, we will treat the embedded option of zero-coupon bonds as an American option.
In an economic condition where there are high interest rates, bond issuers are likely at risk of facing an interest-rate decrease( y,BP=∑ntT=1Cn(1+yn)T+FV(1+yn)nt
) and having to continue to issue higher-interest payments(Coupons) than the prevailing interest rate. As such, they may choose to issue callable bonds. A callable bond contains an embedded agreement to redeem赎回(股票/债券等) the bond at agreed dates. Existing bond holders are considered to have sold a call option to the bond issuer.
In the event that interest rates fall(y, BP) and the corporation has the right to exercise the option to buy back the bond during that period at a specific price(K, a call option's profit=BP- K, while existing bondholders' profits = Coupons + (K - initial_BP) and initial_BP<=par value<=K or a callable price ), they may choose to do so. The company can then issue new bonds at lower interest rates. This also means that the company is able to raise more capital in the form of higher bond prices.
################
A callable bond is a debt instrument in which the issuer reserves the right to return the investor's principal and stop interest payments before the bond's maturity date. Corporations may issue bonds to fund expansion or to pay off other loans. If they expect market interest rates to fall(y, BP), they may issue the bond as callable, allowing them to make an early redemption and secure other financings at a lowered rate. The bond's offering will specify the terms of when the company may recall the note.
A callable—redeemable—bond is typically called at a value that is slightly above the par value of the debt. The earlier in a bond's life span that it is called, the higher its call value will be. For example, a bond maturing in 2030 can be called in 2020. It may show a callable price of 102. This price means the investor receives $1,020 for each $1,000 in face value(par value) of their investment. The bond may also stipulate that the early call price goes down to 101 after a year
Price of bond( Callable bonds holders'cost ) = price of bond with no option - price of embedded option
################
Unlike callable bonds, the owner of puttable bonds has the right, but not the obligation, to sell the bond back to the issuer at an agreed price during a certain period. Owners of puttable bonds are considered to have bought a put option from the bond issuer. When interest rates increase(yBP=∑ntT=1Cn(1+yn)T+FV(1+yn)nt), values of existing bonds become less valuable(BP) and puttable bond holders are more incentivized to exercise the right to sell the bond at a higher exercise price(put option's profit= K-BP, existing bondholders' profits = Coupons + put option's profit ). Since puttable bonds are more beneficial to buyers than to the issuers, they are generally less common than callable bonds. Variants of puttable bonds can be found in the form of loan and deposit instruments. A customer who has placed a fixed-rate deposit with a financial institution receives interest payments on specified dates. They are entitled to withdraw the deposit at any time. As such, a fixed-rate deposit instrument can be thought of as a bond with an embedded American put option.
################
A bond is a debt instrument that makes periodic interest payments, known as coupons, to investors. When the bond matures, the investors or lenders receive their principal investment valued at par. It is cost-effective成本效益 for bond issuers to issue bonds with lower yields( y) as this reduces their cost of borrowing. However, to encourage investors to accept a lower yield on a bond, an issuer might embed options that are advantageous to bond investors. One type of bond that is favorable to investors is the put, or puttable, bond.
A put bond is a bond with an embedded put option, giving bondholders the right, but not the obligation, to demand early repayment of the principal from the issuer or a third party acting as an agent for the issuer. The put option on the bond can be exercised upon the occurrence of specified events or conditions or at a certain time or times prior to maturity. In effect, bondholders have the option of "putting" bonds back to the issuer either once during the lifetime of the bond (known as a one-time put bond) or on several different dates.
Bondholders can exercise their options if interest rate levels in the markets increase(y). As there is an inverse relationship between interest rates and bond prices,BP=∑ntT=1Cn(1+yn)T+FV(1+yn)nt when interest rates increase, the value of a bond decreases(BP) to reflect the fact that there are bonds in the market with higher coupon rates than what the investor is holding. In other words, the future value of coupon rates becomes less valuable in a rising interest rate environment. Issuers are forced to repurchase the bonds at par, and investors use the proceeds to buy a similar bond offering a higher yield, a process known as bond swap.
Of course, the special advantages of put bonds mean that some yield must be sacrificed. Investors are wiling to accept a lower yield on a put bond than the yield on a straight bond(bond with no option) because of the value added by the put option. Likewise, the price of a put bond is always higher than the price of a straight bond(bond with no option). While a put bond allows the investor to redeem a long-term bond before maturity, the yield generally equals the one on short-term rather than long-term securities.
################
An investor who wishes to borrow money from a bank enters a loan agreement, making interest payments during the lifetime of the agreement until the debt, together with the principal amount and agreed interest(Coupons), is fully repaid. The bank can be considered as buying a put option on a bond. Under certain circumstances, the bank may exercise the right to redeem the full value of the loan agreement.
Thus, the price of puttable bonds can be thought of as follows:
Price of puttable bond = price of bond with no option + price of put option
Price of puttable bond( Puttable bonds holders'cost ) = price of bond with no option + price of put option
Convertible bonds are issued by companies and contain an embedded option that allows the holder to convert the bond into a number of shares of common stock. The amount of shares to be converted for a bond is defined as the conversion ratio, which is determined such that the dollar amount of shares is the same as the value of the bond.
Convertible bonds have similarities with callable bonds(
Price of bond( Callable bonds holders'cost ) = price of bond with no option - price of embedded option
). They allow the bond holders to exercise the bond for an equivalent amount of shares at the specified conversion ratio at agreed times. Convertible bonds typically issue lower coupon rates than non-convertible bonds, to compensate for the additional value of the right to exercise.
When convertible bond holders exercise their rights into stocks, the company's debts are reduced. On the other hand, the company's stocks become more diluted/daɪˈluːtɪd/稀释的 as the number of shares in the circulation increases, and the company's stock price is expected to fall.
################
As an example, let's say Exxon Mobil Corp. (XOM) issued a convertible bond with a $1,000 face value that pays 4% interest(Coupons interest). The bond has a maturity of 10 years and a convertible ratio of 100 shares for every convertible bond.
If the bond is held until maturity, the investor will be paid $1,000 in principal plus $40 in interest for that year. However, the company's shares suddenly spike and are trading at $11 per share. As a result, the 100 shares of stock are worth $1,100 (100 shares x $11 share price), which exceeds the value of the bond. The investor can convert the bond into stock and receive 100 shares, which could be sold in the market for $1,100 in total.
########
Convertible bonds are debt securities that can be converted into a firm’s stock at a pre-specified price. For instance, you might have a convertible bond with a face (or par) value of $1,000 that pays 6 percent interest, or $60(Coupons), each year. The bond matures in five years, at which time the company must pay the $1,000 par value to the bondholder. However, at
the time the bond was issued, the firm gave the bondholder the option of
In other words, the bondholder would receive 20 shares (20=100050 , a convertible ratio of 20 shares for every convertible bond). What would you do if you were the bondholder? If the stock were selling for more than $50, then you would want to give up the $1,000 par value and take the stock. Thus, it’s the investor’s choice either to take the $1,000 promised when the bond was issued or to convert the bond into the firm’s stock.
################
Preferred stocks are stocks that have bond-like qualities. Owners of preferred stocks have seniority(/ˌsiːnˈjɔːrəti/) of claim on dividend payments over common stocks, which are usually negotiated as a fixed percentage of their par value. Although there is no guarantee of dividend payments, all dividends are paid on preferred stock first over common stock. In certain agreements on preferred stocks, dividends that are not paid as agreed may accumulate until they are paid at a later time. These preferred stocks are known as cumulative.
Prices of preferred stocks typically move in tandem/ˈtændəm/ with同步变动 their common stock. They may have voting rights associated with common shareholders. In the event of bankruptcy, preferred stocks have a first lien优先留置权 of its par value upon liquidation清算.
In this section, we will take a look at pricing a callable bond. We assume that the bond to be priced is a zero-coupon paying bond with an embedded European call option. The price of a callable bond can be thought of as follows:
Price of callable bond = price of bond with no option − price of call option
BP=∑Tt=1CeRt+PVTeRT=∑Tt=1CeRt+ParVareRT continuously compounded interest rate R or r
The value of a zero-coupon bond(C=0) with a par value of 1 at time t and a prevailing interest rate, r, is defined as follows:
Since the interest rate, r, is always changing, we rewrite the zero-coupon bond as follows:
, (Similarly to the above, when the short-term interest rate process ( r(t) ) t∈R+is a deterministic function of time)
Now, the interest rate, r, is a stochastic process that accounts for the price of the bond from time t to T, where T is the time to maturity of the zero-coupon bond.
To model the interest rate, r, we can use one of the short-rate models as a stochastic process. For this purpose, we will use the Vasicek model to model the short-rate process.
The expectation of a log-normally distributed variable, X, is given as follows:
the arithmetic mean算术平均值 of a log-normally distributed variable X
Taking moments of the log-normally distributed variable X (For any real or complex number s, the s-th moment(s阶矩) of a log-normally distributed variable X is given by):
E[Xs]=E[esu]=esu+s2σ22
We obtained the expected value of a log-normally distributed variable, which we will use in the interest-rate process for the zero-coupon bond.
Remember the Vasicek short-rate process model (specifies that the instantaneous interest rate follows the stochastic differential equation(SDE), Under the the risk-neutral probability measure ):
drt=K∗(θ−rt)∗dt+σ∗√dt∗dZt OR
where
where K, θ, σ > 0 and W is a Brownian motion under the risk-neutral measure
==>Multiplying both sides by yields
==>Multiplying by yields
==> integrating from to
if OR Let 0 ≤ u ≤ t
##################
Since the interest rate, r, is always changing, we rewrite the zero-coupon bond as follows:
, (Similarly to the above, when the short-term interest rate process ( r(t) ) t∈R+is a deterministic function of time)
##################https://web.mst.edu/~bohner/fim-10/fim-chap4.pdf
Using the characteristic equation and the interest-rate movements of the Vasicek model, we can rewrite the zero-coupon bond price in terms of expectations:
In the one-factor Vasicek model, the short-term interest rate is modeled as a mean reverting Gaussian stochastic process on a probability space equipped with aa filtration
Consider a frictionless and no-arbitrage无摩擦且无套利 financial market which consists of a bank account with its price process given by and a T-maturity default-free, zero coupon bond with its no-arbitrage price at time t given by
0⩽t⩽T where is the expectation value under the risk-neutral probability measure . Vasicek provides the explicit form of the zero-bond as follows:
(2.3)
B(t,T)=1−e−K(T−t)K OR
OR
From (2.3), we are easy to obtain the following partial differential equation (PDE):
with terminal condition .
https://downloads.hindawi.com/archive/2009/215163.pdf
https://web.mst.edu/~bohner/fim-10/fim-chap4.pdf
http://public.kenan-flagler.unc.edu/faculty/lundblac/bcrp/vasicek77.pdf https://quant.stackexchange.com/questions/45216/finding-bt-in-the-vasicek-model-relating-to-the-bond-equation-more-specifcall
The Python implementation of the zero-coupon bond price is given in the exact_zcb function:
- # Pricing a zero-coupon bond by the Vasicek model
- import numpy as np
- import math
-
- # zcb: Zero_Coupon_Bond
- def exact_zcb( theta, Kappa, sigma, t_T, r0=0. ):
- # r0 : the initial rate of interest at t=0
- # Kappa : "speed of Mean-reversion
- # theta : All future trajectories of r(t) will evolve around a 'mean' level 'theta' in the long run
- # sigma : determines the volatility of the interest rate and amplitude(range of r(t))
- # T : the period in terms of number of years
-
- # 0<= t <=T
- B = ( 1-np.exp(-Kappa*t_T ) ) / Kappa
- A = np.exp( ( theta - (sigma**2)/(2*Kappa**2) ) *\
- ( B - t_T ) - (sigma**2)/(4*Kappa)*(B**2)
- )
- return A * np.exp( -r0*B )
For example, we are interested in finding out the prices of zero-coupon bond prices for a number of maturities. We model the Vasicek short-rate process with a theta('mean') value of 0.5, kappa value of 0.02 , sigma value of 0.03 , and an initial interest rate, r0 , of 0.015. Plugging these values into the exact_zcb function, we obtain zero-coupon bond prices, for the time period from 0 to 25 years with intervals of 0.5 years, and plot out the graph:
- # r0 : the initial rate of interest at t=0
- r0=0.015
- # Kappa : "speed of Mean-reversion
- Kappa = 0.2
- # theta : All future trajectories of r(t) will evolve around a 'mean' level 'theta' in the long run
- theta = 0.5
- # sigma : determines the volatility of the interest rate and amplitude(range of r(t))
- sigma = 0.03
- # T : the period in terms of number of years
-
- Ts = np.r_[0.0:25.5:0.5]
-
-
- zcbs= [ exact_zcb( theta, Kappa, sigma, t, r0 )
- for t in Ts
- ]
-
- print(zcbs)
- %matplotlib inline
- import matplotlib.pyplot as plt
-
- fig = plt.figure( figsize=(12,8) )
-
- plt.title( "Zero coupon Bond (ZCB) Values by Time" )
- plt.plot( Ts, zcbs, label='ZCB')
- plt.ylabel( "Value ($)" )
- plt.xlabel( "Time in years" )
- plt.legend()
- plt.grid(True)
- plt.show()
Issuers of callable bonds may redeem the bond at an agreed price, as specified in the contract. To price such a bond, the discounted early-exercise values can be defined as follows:
Here,
The Python implementation of the early-exercise option can then be written as follows:
- import numpy as np
-
- def exercise_value( K, R, t ):
- # k is the price ratio of the strike price to the par value
- # R is the interest rate for the strike price.
- return K * np.exp(-R*t)
In the preceding example, we are interested in valuing a call option with a strike ratio of 0.95 and an initial interest rate of 1.5%. We can then plot the values as a function of time and superimpose them onto a graph of zero-coupon bond prices to give us a better visual representation of the relationship between zero-coupon bond prices and callable bond prices:
- # r0 : the initial rate of interest at t=0
- r0=0.015
- # Kappa : "speed of Mean-reversion
- Kappa = 0.2
- # theta : All future trajectories of r(t) will evolve around a 'mean' level 'theta' in the long run
- theta = 0.5
- # sigma : determines the volatility of the interest rate and amplitude(range of r(t))
- sigma = 0.03
- # T : the period in terms of number of years
-
- Ts = np.r_[0.0:25.5:0.5]
-
-
- zcbs= [ exact_zcb( theta, Kappa, sigma, t, r0 )
- for t in Ts
- ]
-
- # k is the price ratio of the strike price to the par value
- k=0.95
-
- Ks = [ exercise_value( k, r0, t)
- for t in Ts
- ]
-
- print(Ks)
- %matplotlib inline
- import matplotlib.pyplot as plt
-
- fig = plt.figure( figsize=(12,8) )
-
- plt.title( "Zero Coupon Bond (ZCB) and Strike (K) Values by Time" )
-
- plt.plot( Ts, zcbs, label='ZCB')
- plt.plot( Ts, Ks, label='K', linestyle="--", marker=".")
-
- plt.ylabel( "Value ($)" )
- plt.xlabel( "Time in years" )
- plt.legend()
- plt.grid(True)
- plt.show()
From the preceding graph, we can approximate the price of callable zero-coupon bond prices. Since the bond issuer owns the call, the price of the callable zero-coupon bond can be stated as follows:
This callable bond price is an approximation, given the current interest-rate level. The next step would be to treat early-exercise by going through a form of policy iteration, which is a cycle used to determine optimum early-exercise values and their effect on other nodes, and check whether they become due for an early exercise. In practice, such an iteration only occurs once.
So far, we have used the Vasicek model in our short-rate process to model a zero-coupon bond. We can undergo policy iteration by finite differences to check for early-exercise conditions and their effect on other nodes. We will use the implicit method of finite differences for the numerical pricing procedure, as discussed in https://blog.csdn.net/Linli522362242/article/details/125355166, Numerical Procedures for Pricing Options.
Let's create a class named VasicekCZCB , which will incorporate all the methods used to implement the pricing of callable zero-coupon bonds by the Vasicek model. The class with its constructor definition is given as follows:
- class VasicekCZCB:
-
- def __init__(self):
- # the 'inverse' normal cumulative distribution function
- # https://blog.csdn.net/Linli522362242/article/details/103646927
- # confidence (interval)= 0.95 ==>(1-confidence)/2 = 0.025 ==>1-0.025=0.975
- # P(z<Z?) = 0.975 ==> Z?=1.96
- self.norminv = st.distributions.norm.ppf
- # the normal Cumulative Distribution Function
- # https://blog.csdn.net/Linli522362242/article/details/125662545
- # P(z<1.96) ==> 0.975 # probability(area) under the curve
- self.norm = st.distributions.norm.cdf
In the constructor, the norminv and norm variable is made available to all methods that
require the computation of the inverse normal cumulative distribution function and the
normal cumulative distribution function of SciPy, respectively.
- # to compute the minimum and maximum of the Vasicek interest rate process
- # by a normal distribution process.
- def vasicek_limits(self, r0, sigma, Kappa, theta, T, prob=1e-6):
-
- E_rt = theta + (r0-theta)*math.exp(-Kappa*T )
- variance = (sigma**2)*T if Kappa==0 else \
- (sigma**2)/( 2*Kappa )*( 1-math.exp(-2*Kappa*T) )
- stddev = math.sqrt(variance)
- # Compute the inverse of cdf values evaluated
- # at the probability values in 'prob'
- # for the normal distribution with 'mean/mu/theta
- # and standard deviation 'sigma'
- # ppf(q, loc=0, scale=1) = ppf(prob, theta, sigma)
- r_min = self.norminv( prob, E_rt, stddev ) # P(Z<=r_min) = prob
- r_max = self.norminv( 1-prob, E_rt, stddev ) # P(Z>=r_max) = 1-prob
-
- #####################
- z = np.linspace(-4*stddev,8*stddev,1000)
- fz = st.distributions.norm.pdf(z, E_rt, stddev)
- plt.plot(z, fz, label='f(z)')
- # plt.axvline(r_min, color='blue', label='z>=r_min')
- # plt.axvline(r_max, color='red', label='z<=r_max')
- plt.fill_between(z, fz, 0, where= (r_min<z) & (z<r_max) )
- plt.annotate( 'r_min={:.4f}'.format(r_min),
- ha='center',
- va='bottom',
-
- xytext=(r_min, 1), #The xytext parameter specifies the text position
- xy=(r_min, 0), #The xy parameter specifies the arrow's destination
-
- arrowprops=dict(facecolor='blue', headwidth=5, headlength=3, width=1 ),
- #arrowprops={'facecolor':'blue', 'headwidth':10, 'headlength':4, 'width':2} #OR
- fontsize=10
- )
- plt.annotate( 'r_max={:.4f}'.format(r_max),
- ha='center',
- va='bottom',
-
- xytext=(r_max, 1), #The xytext parameter specifies the text position
- xy=(r_max, 0), #The xy parameter specifies the arrow's destination
-
- arrowprops=dict(facecolor='blue', headwidth=5, headlength=3, width=1 ),
-
- #arrowprops={'facecolor':'blue', 'headwidth':10, 'headlength':4, 'width':2} #OR
- fontsize=10
- )
-
- plt.annotate( r'$\theta$={:.2f}'.format(E_rt),
- ha='center',
- va='bottom',
-
- xytext=(E_rt, 1), #The xytext parameter specifies the text position
- xy=(E_rt, 0), #The xy parameter specifies the arrow's destination
-
- arrowprops=dict(facecolor='blue', headwidth=5, headlength=3, width=1 ),
-
- #arrowprops={'facecolor':'blue', 'headwidth':10, 'headlength':4, 'width':2} #OR
- fontsize=10
- )
-
- plt.text(
- E_rt, 6, #The xytext parameter specifies the text position
- 'P(r_min<= (Z={:.6f}) <=r_max)'.format(1-2*prob),
- fontsize=10,
- horizontalalignment = 'center'
- )
- plt.xlabel('Z-score')
- plt.title('$E(r_t)$, stddev from Vasicek model, prob={} to get r_min and rmax'.format(prob))
- plt.legend()
- #####################
-
- return (r_min, r_max)
- # to compute the grid size parameter, N
- def calculate_N( self, max_structure_const,
- dt, sigma, r_max, r_min
- ):
- N = 0
- # sigma : "instantaneous volatility",
- # measures instant by instant the amplitude of randomness entering the system.
- # The spread(range) of the value of r_t
- while True:
- N += 1
- # constraints: dt * sigma**2 / (dr)**2 <= max_structure_const
- grid_structure_interval = dt*sigma**2/\
- ( (r_max-r_min)/float(N) )**2
- # dr = dS = (r_max-r_min)/float(N)
- # at a time point: n = sigma**2 / (dr)**2 = big square/small square
- # n * dt : over time dt
- # grid_struct_const is the maximum threshold of dt movement
- # that determines N in the calculate_N() method;
- if grid_structure_interval > max_structure_const:
- break
-
- return N
- # to compute the implicit scheme parameters for the Vasicek model.
- def vasicek_params( self, r0, M, sigma, Kappa, theta, T,
- prob, grid_struct_const=0.25, r_list=None
- ):
- if r_list is not None:
- (r_min, r_max) = ( r_list[0], r_list[-1] )
- else:
- (r_min, r_max) = self.vasicek_limits( r0, sigma, Kappa,
- theta, T, prob )
-
- dt = T/float(M) # 0<=t<=T
-
- N = self.calculate_N( grid_struct_const, dt, sigma, r_max, r_min )
- dr = (r_max-r_min)/(N-1)
-
- return (r_min, dr, N, dt )
- # Returns the discounted value of the strike price
- def exercise_call_price( self, R, ratio, tau ):
- K_from_maturity_to_t = ratio*np.exp(-R*tau)
- return K_from_maturity_to_t # 0<= s <=T and T-s=t
- def vasicek_diagonals( self, sigma, kappa, theta,
- r_min, dr, N, dtau
- ):
- r_list = np.r_[0:N]*dr + r_min
- # a_i
- subdiagonals = kappa*(theta-r_list)*dtau/(2*dr) -\
- 0.5*(sigma**2)*dtau/(dr**2)
- # b_i
- diagonals = 1 + r_list*dtau + sigma**2*dtau/(dr**2)
- # c_i
- superdiagonals = -kappa*(theta-r_list)*dtau/(2*dr) -\
- 0.5*(sigma**2)*dtau/(dr**2)
-
- # Implement boundary conditions.
- if N > 0: # N=1
- v_sub_d0 = subdiagonals[0] # a0
- # c0 = c0-a0 = -K*(theta-r0)*dt/dr
- superdiagonals[0] = superdiagonals[0]-subdiagonals[0]
- diagonals[0] += 2*v_sub_d0 # b0 += 2*a0 = K*(theta-r0)*dt/dr - sigma^2*dt/dr^2
- subdiagonals[0] = 0 # a0 = 0 = 0
-
- if N > 1:
- v_super_d_last = superdiagonals[-1] # cn
- # cn=cn-an = -K*(theta-rn)*dt/dr
- superdiagonals[-1] = superdiagonals[-1] - subdiagonals[-1]
- diagonals[-1] += 2*v_super_d_last # bn += 2*cn
- superdiagonals[-1] = 0 # cn = 0
-
- return (subdiagonals, diagonals, superdiagonals)
- # (a,b,c,d) : (subdiagonal, diagonal, superdiagonal, v_old)
- def tridiagonal_solve(self, a, b, c, d):
- n = len(a) # Number of equations
- ak, bk, ck, dk = map(np.array, (a, b, c, d)) # Copy the array
- # Ax=LUx=d and A: Matrix composed of all (lk[], bk[] and ck[])
- # d : Matrix of all dk[]
- for k in range(1, n):
- # To determine L, U
- lk = ak[k]/bk[k-1] # L = Matrixcomposed of all lk[] and identity matrix
- bk[k] -= lk*ck[k-1] # U = Matrix composed of all (bk[] and ck[])
- #ck[k] -= 0 # all values above c[k] are all 0
- # To solve Lp = d ==> p : Forward Substitution
- dk[k] -= lk*dk[k-1] # p[1]=1 && lp[k-1]+1*p[k]=d[k] ==> p[k]=(d[k]-lp[k-1] )/1
- # p[k] = d[k]-= lp[k-1]
- xk = ak
- # LUx=d ==> L(Ux)=d ==> Lp=d ==>p and p[k]=d[k]
- # x <== Ux=p <== p and know U
-
- # Back Substitution
- xk[-1] = dk[-1]/bk[-1]
-
- for k in range(n-2, -1, -1):
- xk[k] = (dk[k]-ck[k]*xk[k+1])/bk[k]
-
- del bk, ck, dk # Delete variables from memory
-
- return xk
- def check_exercise(self, V, eex):
- return V > eex
- def vasicek_policy_diagonals(self, subdiagonal, diagonal,
- superdiagonal, v_old,
- v_new, eex):
- has_early_exercise = self.check_exercise(v_new, eex)
- subdiagonal[has_early_exercise] = 0
- superdiagonal[has_early_exercise] = 0
- policy = v_old/eex
- policy_values = policy[has_early_exercise]
- diagonal[has_early_exercise] = policy_values
- return (subdiagonal, diagonal, superdiagonal)
- def iterate( self, subdiagonal, diagonal, superdiagonal,
- v_old, eex, max_policy_iter=10
- ):
- v_m_plus1 = v_old
- v_m = v_old
- change = np.zeros(len(v_old))
- prev_changes = np.zeros(len(v_old))
-
- iterations = 0
- while iterations <= max_policy_iter:
- iterations += 1
-
- v_m_plus1 = self.tridiagonal_solve(subdiagonal,
- diagonal, superdiagonal, v_old
- )
- subdiagonal, diagonal, superdiagonal = \
- self.vasicek_policy_diagonals(
- subdiagonal, diagonal, superdiagonal,
- v_old,
- v_m_plus1, eex)
-
- is_eex = self.check_exercise(v_m_plus1, eex)
- change[is_eex] = 1
-
- if iterations > 1:
- change[v_m_plus1 != v_m] = 1
-
- is_no_more_eex = False if True in is_eex else True
- if is_no_more_eex:
- break
-
- v_m_plus1[is_eex] = eex[is_eex]
- changes = (change == prev_changes)
-
- is_no_further_changes = all((x == 1)
- for x in changes
- )
- if is_no_further_changes:
- break
-
- prev_changes = change
- v_m = v_m_plus1
-
- return v_m_plus1, iterations-1
- # This method returns a list of evenly-spaced short-rates
- # and a list of option prices,
- def vasicek_czcb_values( self, r0, R, ratio, T, sigma, Kappa, theta,
- M, prob=1e-6, max_policy_iter=10,
- grid_struct_const=0.25, r_list=None
- ):
- '''
- r0 : the short-rate at time t=0;
- R : the strike zero rate for the bond price;
- ratio : the strike price per par value of the bond;
- T : the time to maturity;
- sigma : the volatility of the short-rate, r;
- Kappa : the rate of mean reversion;
- theta : the mean of the short-rate process;
- M : the number of steps in the finite differences scheme used by
- the vasicek_limits method later to determine short-rates;
- max_policy_iter : the maximum number of policy iterations used to
- find early-exercise nodes;
- grid_struct_const : the maximum threshold of dt movement that
- determines N in the calculate_N() method;
- r_list : the list of interest rates from which the short-rate process follows
- '''
- (r_min, dr, N, dtau) = self.vasicek_params( r0, M, sigma, kappa, theta,
- T, prob,
- grid_struct_const, r_list
- )
- # N : N rate levels
- # np.r_[0:N] : time step index [0:N-1]
- r_list = np.r_[0:N]*dr + r_min
- v_m_plus1 = np.ones(N)
-
- # M is the number of steps in the finite differences scheme
- for i in range(1, M+1):
- # K:at step i, the discounted value of the strike price to
- # the par value as a ratio
- K = self.exercise_call_price(R, ratio, i*dtau)
- eex = np.ones(N)*K # par value=1
- (subdiagonal, diagonal, superdiagonal) = self.vasicek_diagonals(
- sigma, Kappa, theta, r_min, dr, N, dtau
- )
- (v_m_plus1, iterations) = \
- self.iterate(subdiagonal, diagonal, superdiagonal,
- v_m_plus1, eex, max_policy_iter)
- return r_list, v_m_plus1
- import math
- import numpy as np
- import scipy.stats as st
- import matplotlib.pyplot as plt
-
- class VasicekCZCB:
-
- def __init__(self):
- # the 'inverse' normal cumulative distribution function
- # https://blog.csdn.net/Linli522362242/article/details/103646927
- # confidence (interval)= 0.95 ==>(1-confidence)/2 = 0.025 ==>1-0.025=0.975
- # P(z<Z?) = 0.975 ==> Z=1.96
- # ppf(q, loc=0, scale=1) or ppf(prob, mean, stddev)
- self.norminv = st.distributions.norm.ppf
- # the normal Cumulative Distribution Function
- # https://blog.csdn.net/Linli522362242/article/details/125662545
- # P(z<1.96) ==> 0.975 # probability(area) under the curve
- self.norm = st.distributions.norm.cdf
-
- # to compute the minimum and maximum of the Vasicek interest rate process
- # by a normal distribution process.
- def vasicek_limits(self, r0, sigma, Kappa, theta, T, prob=1e-6):
-
- E_rt = theta + (r0-theta)*math.exp(-Kappa*T )
- variance = (sigma**2)*T if Kappa==0 else \
- (sigma**2)/( 2*Kappa )*( 1-math.exp(-2*Kappa*T) )
- stddev = math.sqrt(variance)
- # Compute the inverse of cdf values evaluated
- # at the probability values in 'prob'
- # for the normal distribution with 'mean/mu/theta
- # and standard deviation 'sigma'
- # ppf(q, loc=0, scale=1) = ppf(prob, theta, sigma)
- r_min = self.norminv( prob, E_rt, stddev ) # P(Z<=r_min) = prob
- r_max = self.norminv( 1-prob, E_rt, stddev ) # P(Z>=r_max) = 1-prob
-
- #####################
- z = np.linspace(-4*stddev,8*stddev,1000)
- fz = st.distributions.norm.pdf(z, E_rt, stddev)
- plt.plot(z, fz, label='f(z)')
- # plt.axvline(r_min, color='blue', label='z>=r_min')
- # plt.axvline(r_max, color='red', label='z<=r_max')
- plt.fill_between(z, fz, 0, where= (r_min<z) & (z<r_max) )
- plt.annotate( 'r_min={:.4f}'.format(r_min),
- ha='center',
- va='bottom',
-
- xytext=(r_min, 1), #The xytext parameter specifies the text position
- xy=(r_min, 0), #The xy parameter specifies the arrow's destination
-
- arrowprops=dict(facecolor='blue', headwidth=5, headlength=3, width=1 ),
- #arrowprops={'facecolor':'blue', 'headwidth':10, 'headlength':4, 'width':2} #OR
- fontsize=10
- )
- plt.annotate( 'r_max={:.4f}'.format(r_max),
- ha='center',
- va='bottom',
-
- xytext=(r_max, 1), #The xytext parameter specifies the text position
- xy=(r_max, 0), #The xy parameter specifies the arrow's destination
-
- arrowprops=dict(facecolor='blue', headwidth=5, headlength=3, width=1 ),
-
- #arrowprops={'facecolor':'blue', 'headwidth':10, 'headlength':4, 'width':2} #OR
- fontsize=10
- )
-
- plt.annotate( r'$\theta$={:.2f}'.format(E_rt),
- ha='center',
- va='bottom',
-
- xytext=(E_rt, 1), #The xytext parameter specifies the text position
- xy=(E_rt, 0), #The xy parameter specifies the arrow's destination
-
- arrowprops=dict(facecolor='blue', headwidth=5, headlength=3, width=1 ),
-
- #arrowprops={'facecolor':'blue', 'headwidth':10, 'headlength':4, 'width':2} #OR
- fontsize=10
- )
-
- plt.text(
- E_rt, 6, #The xytext parameter specifies the text position
- 'P(r_min<= (Z={:.6f}) <=r_max)'.format(1-2*prob),
- fontsize=10,
- horizontalalignment = 'center'
- )
- plt.xlabel('Z-score')
- plt.title('$E(r_t)$, stddev from Vasicek model, prob={} to get r_min and rmax'.format(prob))
- plt.legend()
- #####################
-
- return (r_min, r_max)
-
- # to compute the grid size parameter, N
- def calculate_N( self, max_structure_const,
- dt, sigma, r_max, r_min
- ):
- N = 0
- # sigma : "instantaneous volatility",
- # measures instant by instant the amplitude of randomness entering the system.
- # The spread(range) of the value of r_t
- while True:
- N += 1
- # constraints: dt * sigma**2 / (dr)**2 <= max_structure_const
- grid_structure_interval = dt*sigma**2/\
- ( (r_max-r_min)/float(N) )**2
- # dr = dS = (r_max-r_min)/float(N)
- # at a time point: n = sigma**2 / (dr)**2 = big square/small square
- # n * dt : over time dt
- # grid_struct_const is the maximum threshold of dt movement
- # that determines N in the calculate_N() method;
- if grid_structure_interval > max_structure_const:
- break
-
- return N
-
- # to compute the implicit scheme parameters for the Vasicek model.
- def vasicek_params( self, r0, M, sigma, Kappa, theta, T,
- prob, grid_struct_const=0.25, r_list=None
- ):
- if r_list is not None:
- (r_min, r_max) = ( r_list[0], r_list[-1] )
- else:
- (r_min, r_max) = self.vasicek_limits( r0, sigma, Kappa,
- theta, T, prob )
-
- dt = T/float(M) # 0<=t<=T
-
- N = self.calculate_N( grid_struct_const, dt, sigma, r_max, r_min )
- dr = (r_max-r_min)/(N-1)
-
- return (r_min, dr, N, dt )
-
- # Returns the discounted value of the strike price
- def exercise_call_price( self, R, ratio, tau ):
- K_from_maturity_to_t = ratio*np.exp(-R*tau)
- return K_from_maturity_to_t # 0<= s <=T and T-s=t
-
- def vasicek_diagonals( self, sigma, kappa, theta,
- r_min, dr, N, dtau
- ):
- r_list = np.r_[0:N]*dr + r_min
- # a_i
- subdiagonals = kappa*(theta-r_list)*dtau/(2*dr) -\
- 0.5*(sigma**2)*dtau/(dr**2)
- # b_i
- diagonals = 1 + r_list*dtau + sigma**2*dtau/(dr**2)
- # c_i
- superdiagonals = -kappa*(theta-r_list)*dtau/(2*dr) -\
- 0.5*(sigma**2)*dtau/(dr**2)
-
- # Implement boundary conditions.
- if N > 0: # N=1
- v_sub_d0 = subdiagonals[0] # a0
- # c0 = c0-a0 = -K*(theta-r0)*dt/dr
- superdiagonals[0] = superdiagonals[0]-subdiagonals[0]
- diagonals[0] += 2*v_sub_d0 # b0 += 2*a0 = K*(theta-r0)*dt/dr - sigma^2*dt/dr^2
- subdiagonals[0] = 0 # a0 = 0 = 0
-
- if N > 1:
- v_super_d_last = superdiagonals[-1] # cn
- # cn=cn-an = -K*(theta-rn)*dt/dr
- superdiagonals[-1] = superdiagonals[-1] - subdiagonals[-1]
- diagonals[-1] += 2*v_super_d_last # bn += 2*cn
- superdiagonals[-1] = 0 # cn = 0
-
- return (subdiagonals, diagonals, superdiagonals)
-
- # (a,b,c,d) : (subdiagonal, diagonal, superdiagonal, v_old)
- def tridiagonal_solve(self, a, b, c, d):
- n = len(a) # Number of equations
- ak, bk, ck, dk = map(np.array, (a, b, c, d)) # Copy the array
- # Ax=LUx=d and A: Matrix composed of all (lk[], bk[] and ck[])
- # d : Matrix of all dk[]
- for k in range(1, n):
- # To determine L, U
- lk = ak[k]/bk[k-1] # L = Matrixcomposed of all lk[] and identity matrix
- bk[k] -= lk*ck[k-1] # U = Matrix composed of all (bk[] and ck[])
- #ck[k] -= 0 # all values above c[k] are all 0
- # To solve Lp = d ==> p : Forward Substitution
- dk[k] -= lk*dk[k-1] # p[1]=1 && lp[k-1]+1*p[k]=d[k] ==> p[k]=(d[k]-lp[k-1] )/1
- # p[k] = d[k]-= lp[k-1]
- xk = ak
- # LUx=d ==> L(Ux)=d ==> Lp=d ==>p and p[k]=d[k]
- # x <== Ux=p <== p and know U
-
- # Back Substitution
- xk[-1] = dk[-1]/bk[-1]
-
- for k in range(n-2, -1, -1):
- xk[k] = (dk[k]-ck[k]*xk[k+1])/bk[k]
-
- del bk, ck, dk # Delete variables from memory
-
- return xk
-
- def check_exercise(self, V, eex):
- # ratio= the strike price / the par value
- return V > eex # eex = ratio*np.exp(-R*tau)
-
- def vasicek_policy_diagonals(self, subdiagonal, diagonal,
- superdiagonal, v_old,
- v_new, eex):
- has_early_exercise = self.check_exercise(v_new, eex)
- subdiagonal[has_early_exercise] = 0
- superdiagonal[has_early_exercise] = 0
- policy = v_old/eex
- policy_values = policy[has_early_exercise]
- diagonal[has_early_exercise] = policy_values
- return (subdiagonal, diagonal, superdiagonal)
-
- def iterate( self, subdiagonal, diagonal, superdiagonal,
- v_old, eex, max_policy_iter=10
- ):
- v_m_plus1 = v_old
- v_m = v_old
- change = np.zeros(len(v_old))
- prev_changes = np.zeros(len(v_old))
-
- iterations = 0
- while iterations <= max_policy_iter:
- iterations += 1
-
- v_m_plus1 = self.tridiagonal_solve(subdiagonal,
- diagonal, superdiagonal, v_old
- )
- subdiagonal, diagonal, superdiagonal = \
- self.vasicek_policy_diagonals(
- subdiagonal, diagonal, superdiagonal,
- v_old,
- v_m_plus1, eex)
-
- is_eex = self.check_exercise(v_m_plus1, eex)
- change[is_eex] = 1
-
- if iterations > 1:
- change[v_m_plus1 != v_m] = 1
-
- is_no_more_eex = False if True in is_eex else True
- if is_no_more_eex:
- break
-
- v_m_plus1[is_eex] = eex[is_eex]
- changes = (change == prev_changes)
-
- is_no_further_changes = all((x == 1)
- for x in changes
- )
- if is_no_further_changes:
- break
-
- prev_changes = change
- v_m = v_m_plus1
-
- return v_m_plus1, iterations-1
-
- # This method returns a list of evenly-spaced short-rates
- # and a list of option prices,
- def vasicek_czcb_values( self, r0, R, ratio, T, sigma, Kappa, theta,
- M, prob=1e-6, max_policy_iter=10,
- grid_struct_const=0.25, r_list=None
- ):
- '''
- r0 : the short-rate at time t=0;
- R : the strike zero rate for the bond price;
- ratio : the strike price per par value of the bond;
- T : the time to maturity;
- sigma : the volatility of the short-rate, r;
- Kappa : the rate of mean reversion;
- theta : the mean of the short-rate process;
- M : the number of steps in the finite differences scheme used by
- the vasicek_limits method later to determine short-rates;
- max_policy_iter : the maximum number of policy iterations used to
- find early-exercise nodes;
- grid_struct_const : the maximum threshold of dt movement that
- determines N in the calculate_N() method;
- r_list : the list of interest rates from which the short-rate process follows
- '''
- (r_min, dr, N, dtau) = self.vasicek_params( r0, M, sigma, kappa, theta,
- T, prob,
- grid_struct_const, r_list
- )
- # N : N rate levels
- # np.r_[0:N] : time step index [0:N-1]
- r_list = np.r_[0:N]*dr + r_min
- v_m_plus1 = np.ones(N)
-
- # M is the number of steps in the finite differences scheme
- for i in range(1, M+1):
- # K:at step i, the discounted value of the strike price to
- # the par value as a ratio
- K = self.exercise_call_price(R, ratio, i*dtau)
- eex = np.ones(N)*K # par value=1
- (subdiagonal, diagonal, superdiagonal) = self.vasicek_diagonals(
- sigma, Kappa, theta, r_min, dr, N, dtau
- )
- (v_m_plus1, iterations) = \
- self.iterate(subdiagonal, diagonal, superdiagonal,
- v_m_plus1, eex, max_policy_iter)
- return r_list, v_m_plus1
With these methods defined, we can now run our code and price a callable zero-coupon bond with the Vasicek model.
Assume that we run this model with the following parameters:
r0 is 0.05 (The r0 variable is the short-rate at time t=0;),
R is 0.05 (R is the strike zero rate for the bond price) ,
ratio is 0.95 (ratio is the strike price per par value of the bond;),
sigma is 0.03(the volatility of the short-rate, r),
kappa is 0.15(Kappa : the rate of mean reversion) ,
theta is 0.05 (the mean of the short-rate process),
prob is 1e-6(to get r_min or r_max with st.distributions.norm.ppf( prob or 1-prob, E_rt, stddev )
Compute the inverse of cdf values evaluated at the probability values in 'prob'),
M is 250 (the number of steps in the finite differences scheme used by
the vasicek_limits method later to determine short-rates),
max_policy_iter is 10 (the maximum number of policy iterations used to find early-exercise nodes),
grid_struc_interval is 0.25 ,
and we are interested in the values of the interest rates between 0% and 2%.
- # The r0 variable is the short-rate at time t=0;
- r0 = 0.05
- # R is the strike zero rate for the bond price;
- R = 0.05
- # ratio is the strike price per par value of the bond;
- ratio = 0.95
- #
- sigma = 0.03
- kappa = 0.15
- theta = 0.05
- prob = 1e-6
- M = 250
- max_policy_iter=10
- grid_struct_interval = 0.25
- rs = np.r_[0.0:2.0:0.1]
-
- vasicek = VasicekCZCB()
- r, vals = vasicek.vasicek_czcb_values(
- r0, R, ratio, 1., sigma, kappa, theta,
- M, prob, max_policy_iter, grid_struct_interval, rs)
The following Python code demonstrates this model for maturities of 1 year, 5 years, 7 years, 10 years, and 20 years:
- %pylab inline
-
- fig = plt.figure(figsize=(12, 8))
- plt.title("Callable Zero Coupon Bond Values by r")
- plt.plot(r, vals, label='1 yr')
-
- # T is the time to maturity;
- for T in [5., 7., 10., 20.]:
- r, vals = vasicek.vasicek_czcb_values(
- r0, R, ratio, T, sigma, kappa, theta,
- M, prob, max_policy_iter, grid_struct_interval, rs)
- plt.plot(r, vals, label=str(T)+' yr', linestyle="--", marker=".")
-
- plt.ylabel("Value ($)")
- plt.xlabel("r")
- plt.legend()
- plt.grid(True)
- plt.show()
We obtained the theoretical values of pricing callable zero-coupon bonds for various maturities for various interest rates.
In pricing callable zero-coupon bonds, we used the Vasicek interest-rate process to model interest-rate movement with the aid of a normal distribution process. In The Vasicek model section, we demonstrated that the Vasicek model can produce negative interest rates, which may not be practical for most economic cycles. Quantitative analysts often use more than one model in derivative pricing to obtain realistic results. The CIR and Hull-White models are some of the commonly-discussed models in financial studies. The limitation on these models is that they involve only one factor, or a single source of uncertainty.
We also looked at the implicit scheme of finite differences for the policy iteration of the early exercise. Another method of consideration is the Crank-Nicolson method of finite differences. Other methods include the Monte Carlo simulation for calibration of this model.
Finally, we obtained a final list of short-rates and callable bond prices. To infer a fair value for the callable bond for a particular short-rate, interpolation of the list of bond prices is required. Often, the linear interpolation method is used. Other interpolation methods of consideration are the cubic and spline interpolation methods.
In this chapter, we focused on interest-rate and related derivative pricing with Python. Most bonds, such as US Treasury bonds, pay a fixed amount of interest semi-annually, while other bonds may pay quarterly or annually. It is a characteristic of bonds that their prices are closely related to current interest-rate levels in an inverse manner. The normal or positive yield curve, where long-term interest rates are higher than short-term interest rates, is said to be upward sloping. In certain economic conditions, the yield curve can be inverted and is said to be downward sloping(During periods of high inflation, where the inflation rate exceeds the rate of coupon interests, negative interest rates may be observed. Investors are willing to pay in the short-term just to secure their long-term wealth.).
A zero-coupon bond is a bond that pays no coupons during its lifetime, except upon maturity when the principal or face value is repaid. We implemented a simple zero-coupon bond calculator in Python.
The yield curve can be derived from the short-term zero or spot rates of securities, such as zero-coupon bonds, T-bills, notes, and eurodollar deposits using a bootstrapping process. With Python, we used a lot of bond information to plot a yield curve, and derived forward rates, yield-to-maturity, and bond prices from the yield curve.
Two important metrics to bond traders are duration and convexity. Duration(
PV(Ct)=Ct(1+YTM)t OR PV(Ct)=Ct+FV(1+YTM)t==>PV(Ct)BP==>
==>) is the sensitivity measure of bond prices to yield changes(Change in interest rates).
Convexity(think of convexity as the second derivative of the relationship between the price and yield) is the sensitivity measure of the duration of a bond to yield changes. We implemented calculations using the modified duration(Effective Duration
VS
) model and convexity calculator in Python.
Short-rate models are frequently used in the evaluation of interest-rate derivatives. Interest-rate modeling is a fairly complex topic, since they are affected by a multitude of factors, such as economic states, political decisions, government intervention, and the laws of supply and demand. A number of interest-rate models have been proposed to account for the various characteristics of interest rates. Some of the interest-rate models we discussed include the Vasicek(), CIR, and Rendleman and Bartter models.
Bond issuers may embed options within a bond to allow them the right, but not the obligation, to buy or sell the issued bond at a predetermined price during a specified period of time. The price of a callable bond can be thought of as the price difference of a bond
without an option and the price of the embedded call option.
Price of bond( Callable bonds holders'cost ) = price of bond with no option - price of embedded option
Price of puttable bond = price of bond with no option + price of put option
Using Python, we took a look at pricing a callable zero-coupon bond by applying the Vasicek model to the implicit method of finite differences.==>==>
(2.3)
B(t,T)=1−e−K(T−t)K OR
OR
From (2.3), we are easy to obtain the following partial differential equation (PDE):
with terminal condition .
This method is, however, just one of the many methods that quantitative analysts use in bond-options modeling.
In the next chapter, we will discuss the statistical analysis of time-series data.
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。