Introduction

These study notes are based on learning objective B3 of the Exam 9 syllabus, containing source material from chapters 16 and 22 of Investments by Bodie, Kane, and Marcus. These notes explain measures of interest rate sensitivity, such as duration and convexity, and methods for managing interest rate risk..

easypackages::packages("dplyr", "ggplot2", "DT", "data.table")
options(scipen = 999)

In this notebook, I will re-use the function for calculating the price of a bond, from the notebook on objectives B1-B2.

flat.yield.bond.price = function(par.value, maturity, coupon.rate, interest.rate, semiannual = TRUE) {
  payment.count = ifelse(semiannual, 2 * maturity, maturity)
  discount.factor = ifelse(semiannual, 1 / (1 + interest.rate / 2), 1 / (1 + interest.rate))
  coupon.payment = ifelse(semiannual, coupon.rate * par.value / 2, coupon.rate * par.value)
  PV.coupons = coupon.payment * (discount.factor - discount.factor^(payment.count + 1)) / (1 - discount.factor)
  PV.par.value = par.value * discount.factor^payment.count
  return(PV.coupons + PV.par.value)
}

Interest Rate Sensitivity

Duration

Given an asset or liability with present value \(P\) under interest rate \(y\), the modified duration is \[ D^* = -\frac{1}{P}\frac{dP}{dy} \] The Macaulay duration is \[ D = (1 + y)D^* \]

If we have multiple assets / liabilities \(P_i\) with modified duration \(D^*_i\), then the duration of their sum \(P = \sum_{1\leq i \leq n}P_i\) is: \[ -\frac{1}{P} \frac{d}{dy} \sum_{1 \leq i \leq n} P_i = -\frac{1}{P} \sum_{1\leq i \leq n} \frac{dP_i}{dy} = \frac{1}{P}\sum_{1\leq i \leq n} P_i D_i^* \] In other words, the duration of the sum is the present-value weighted sume of the durations. The same result holds for Macaulay duration.

Consider a zero-coupon bond with maturity \(m\). Its present value is \(\frac{1}{(1+y)^m}\), so its modified duration is \[ (1+y)^m \frac{m}{(1+y)^{m+1}} = \frac{m}{1+y} \] In particular, its Macaulay duration is the same as the maturity \(m\).

The two observations made above can be used to provide an alternate interpretation of the Macaulay duration. Suppose we have cash flow \(C_t\) at time \(t\). We can treat this as a zero-coupon bond with maturity \(t\), so the duration of this set of cash flows will be the weighted average of \(t\), where the weights are the present value of the cash flow: \[ D = \frac{1}{P} \sum_{1\leq t \leq n} \frac{tC_t}{(1+y)^t} \] where \[ P = \sum_{1\leq t \leq n} \frac{C_t}{(1+y)^t} \] This formulation assumes that if we have a semi-annual coupon bond, then \(t\) will be expressed as fractions of a year (as opposed to changing the unit of time to half-years).

This provides a method for calculating the Macaulay duration of a bond:

  1. Calculate the present value of each cash flow

  2. Sum the results from Step 1 to get the total present value

  3. Multiply each of the present values in step 1 by the time at which the cash flow is received

  4. Sum the results of Step 3, then divide by the total present value

  5. If modified duration is needed, divide by \((1+y)\).

The textbook contains an example of calculating the duration of a 2-year $1000 par value bond with 8% semiannual coupons, and that the annual interest rate is 9%. The duration can be calculated as follows:

duration.example = data.frame(time = c(0.5, 1, 1.5, 2), cash_flow = c(40, 40, 40, 1040))
duration.example = duration.example %>% mutate(pv_cash_flow = cash_flow / 1.09^time, weighted_time = time * pv_cash_flow)
duration.example
##   time cash_flow pv_cash_flow weighted_time
## 1  0.5        40     38.31305      19.15653
## 2  1.0        40     36.69725      36.69725
## 3  1.5        40     35.14959      52.72438
## 4  2.0      1040    875.34719    1750.69439
macaulay.duration = sum(duration.example$weighted_time) / sum(duration.example$pv_cash_flow)
modified.duration = macaulay.duration / 1.09
print(paste0("The Macaulay duration is ", macaulay.duration, " and the modified duration is ", modified.duration))
## [1] "The Macaulay duration is 1.88661510277498 and the modified duration is 1.73083954383026"

It is also useful to know the duration of a level perpetuity. Recall that the present value of a $1 perpetuity, with the first payment one year from present, is \[ \sum_{i\geq 1}\frac{1}{(1+y)^i} = \frac{\frac{1}{1+y}}{1 - \frac{1}{1+y}} = \frac{1}{y} \] The modified duration is \[ - y\frac{d}{dy}\frac{1}{y} = \frac{1}{y} \] and so the Macaulay duration is \[ \frac{y+1}{y} = 1 + \frac{1}{y} \] Note that despite the “infinite maturity” of the perpetuity, it has a finite duration, providing a clear distinction between the two concepts.

Factors affecting duration of a bond include:
Factor Impact on Duration Rationale
Higher coupon rate Lower Higher proportion of bond value attributed to earlier payments
Higher time to maturity Higher for par or premium bonds; usually higher for discount bonds Greater proportion of payments made at later times
Higher yield to maturity Lower High yield reduces value of more-distant payments

The impact of the above can be illustrated by comparing the duration of bonds with different maturities, coupon rates, and yields to maturity.

bond.duration.macaulay = function(par.value, coupon.rate, maturity, yield) {
  cash.flow.pattern = rep(coupon.rate * par.value, maturity) + c(rep(0, maturity - 1), par.value)
  PV.cash.flow = 1 / (1+yield)^(1:maturity) * cash.flow.pattern
  weighted.time = (1:maturity) * PV.cash.flow
  return(sum(weighted.time) / sum(PV.cash.flow))
}

Compare these four bonds:

bond.duration.scenarios = data.frame(scenario_name = c("A", "B", "C", "D"), coupon_rate = c(0, 0.15, 0.03, 0.15), yield_to_maturity = c(0.06, 0.06, 0.15, 0.15))
bond.duration.scenarios
##   scenario_name coupon_rate yield_to_maturity
## 1             A        0.00              0.06
## 2             B        0.15              0.06
## 3             C        0.03              0.15
## 4             D        0.15              0.15
bond.duration.comparison = merge(bond.duration.scenarios, data.frame(maturity = 1:50))
bond.duration.comparison$macaulay_duration = apply(bond.duration.comparison %>% select(coupon_rate, maturity, yield_to_maturity), 1, function(y) {bond.duration.macaulay(par.value = 1000, coupon.rate = y['coupon_rate'], maturity = y['maturity'], yield = y['yield_to_maturity'])})
ggplot(data = bond.duration.comparison, aes(x = maturity, y = macaulay_duration, col = scenario_name)) + geom_line()

Note that:

  • B and D have the same coupon rate, but D has a higher yield so it has a consistently lower duration.

  • C and D have the same yield, but D has a higher coupon rate, so it has a consistently lower duration.

  • B and D are premium and par value bonds, respectively, so duration monotonically increaseses. C is a discount bond, so its duration starts to decrease for longer maturities. It approaches D asymptotically.

Effective Duration

A callable bond has a provision allowing the issuer the option of re-purchasing the bond at a specified price before the maturity date. The presence of this option can make it challenging to assess interest rate sensitivity, since the cash flows are not known. In this case, the effective duration is used instead. This is defined as: \[ D_E = - \frac{\Delta P}{P} \frac{1}{\Delta r} \] The key distinction here is that the market interest rate is used, rather than the bond’s yield to maturity. The price of the bond needs to be calculated using a pricing model for bonds with embedded options. As an example, suppose we are given the price of the bond at three different interest rates:

effective.duration.example = data.frame(interest_rate = c(0.045, 0.05, 0.055), price = c(1010, 980, 930))
effective.duration.example
##   interest_rate price
## 1         0.045  1010
## 2         0.050   980
## 3         0.055   930

Effective duration is calculated as follows:

effective.duration = -(effective.duration.example[3, 'price'] - effective.duration.example[1, 'price']) / effective.duration.example[2, 'price'] / (effective.duration.example[3, 'interest_rate'] - effective.duration.example[1, 'interest_rate'])
print(paste0("The effective duration is ", effective.duration))
## [1] "The effective duration is 8.16326530612245"

Convexity

The convexity of a bond is defined as \[ \frac{1}{P}\frac{d^2P}{dy^2} \] Applying the second derivative to the present value of cash flows, we obtain: \[ \frac{1}{P} \sum_{1\leq i \leq n} \frac{C_i}{(1+y)^{i+2}}i(1+i) \]

Convexity is an attractive feature of bonds because bonds with greater convexity gain more in value when yields fall than they lose in value when yields rise. As a result, investors may be willing to pay more for bonds with higher convexity.

As an example, consider calculating the convexity for a 30-year $1000 par value bond with an 8% annual coupon and an intiial yield to maturity of 8%.

bond.convexity = function(par.value, coupon.rate, maturity, yield) {
  cash.flow.pattern = rep(coupon.rate * par.value, maturity) + c(rep(0, maturity - 1), par.value)
  PV.cash.flow = 1 / (1+yield)^(1:maturity) * cash.flow.pattern
  weighted.time = (1:maturity) * PV.cash.flow * (1 + 1:maturity)
  return(sum(weighted.time) / sum(PV.cash.flow) / (1 + yield)^2)
}
example.convexity = bond.convexity(1000, 0.08, 30, 0.08)
print(paste0("The convexity of the bond is ", example.convexity))
## [1] "The convexity of the bond is 212.432547085269"
example.duration = bond.duration.macaulay(1000, 0.08, 30, 0.08) / 1.08
print(paste0("The modified duration of the bond is ", example.duration))
## [1] "The modified duration of the bond is 11.2577833431275"

Impact of Interest Changes on Bond Prices

Direct Calculation

The most straightforward way to calculate the impact of a change in interest rates on bond prices is an explicit calculation of the bond price under two different yields. Consider the following four bonds, all of which have a par value of $1000 and have annual coupons:

bond.price.scenarios = data.frame(bond_name = c("A", "B", "C", "D"), coupon_rate = c(0.12, 0.12, 0.03, 0.03), maturity = c(5, 30, 30, 30), initial_ytm = c(0.1, 0.1, 0.1, 0.06))
bond.price.scenarios$original_price = apply(bond.price.scenarios %>% select(coupon_rate, maturity, initial_ytm), 1, function(y){flat.yield.bond.price(1000, y['maturity'], y['coupon_rate'], y['initial_ytm'], semiannual = FALSE)})
bond.price.scenarios
##   bond_name coupon_rate maturity initial_ytm original_price
## 1         A        0.12        5        0.10      1075.8157
## 2         B        0.12       30        0.10      1188.5383
## 3         C        0.03       30        0.10       340.1160
## 4         D        0.03       30        0.06       587.0551

Test the sensitivity of the bond price over a range of changes to the yield-to-maturity by calculating its percentage change in price as the yield-to-maturity changes:

bond.price.comparison = merge(bond.price.scenarios, data.frame(ytm_change = -50:50/1000))
bond.price.comparison = bond.price.comparison %>% mutate(current_ytm = initial_ytm + ytm_change)
bond.price.comparison$price = apply(bond.price.comparison %>% select(coupon_rate, maturity, current_ytm), 1, function(y) {flat.yield.bond.price(1000, y['maturity'], y['coupon_rate'], y['current_ytm'], semiannual = FALSE)})
bond.price.comparison = bond.price.comparison %>% mutate(pct_change_price = price / original_price - 1)
ggplot(data = bond.price.comparison, aes(x = ytm_change, y = pct_change_price, col = bond_name)) + geom_line()

Comparing scenarios pairwise gives some insight into the impact of bond characteristics on sensitivity:

  • Comparing A and B, bond A has a shorter maturity, so it is less sensitive to changes in interest rates.

  • Comparing B and C, bond B has a larger coupon rate. It is less sensitive to interest rate changes because a greater proportion of its value is paid sooner.

  • Comparing C and D, bond C has a larger initial yield to maturity, and it is less sensitive to additive changes in the interest rate.

Continuing the earlier example of the 30-year 8% coupon bond with an initial yield of 8%, we can calculate the change in price exactly under the assumptions that the yield increases to 10% or decreases to 6%:

example.initial.price = flat.yield.bond.price(1000, 30, 0.08, 0.08, semiannual = FALSE)
example.ytm.increase.price = flat.yield.bond.price(1000, 30, 0.08, 0.10, semiannual = FALSE)
example.ytm.decrease.price = flat.yield.bond.price(1000, 30, 0.08, 0.06, semiannual = FALSE)
print(paste0("When the yield increases to 10%, the price changes by ", example.ytm.increase.price / example.initial.price - 1, " and when the yield decreases to 6% the price changes by ", example.ytm.decrease.price / example.initial.price - 1))
## [1] "When the yield increases to 10%, the price changes by -0.188538289339766 and when the yield decreases to 6% the price changes by 0.275296623029788"

Using Duration

The linear approximation to the price of a bond can be expressed in terms of modified duration as follows: \[ \Delta P = -PD^*\Delta y \] Continuing the earlier example, we can approximate the change in price using duration as follows:

example.ytm.decrease.approx.1.pct = 0.02 * example.duration * 100
example.ytm.increase.approx.1.pct = -0.02 * example.duration * 100
print(paste0("The duration approximation gives price changes of ", example.ytm.increase.approx.1.pct, " percent and ", example.ytm.decrease.approx.1.pct, " for a yield incresae / decrease of 2%, respectively."))
## [1] "The duration approximation gives price changes of -22.515566686255 percent and 22.515566686255 for a yield incresae / decrease of 2%, respectively."

Note that in either case, the price estimate produced by the duration approximation is lower than the actual price.

Using Duration and Convexity

The duration approximation can be improved by using the second-order Taylor series approximation to the price. Let \(V\) denote convexity. Then this approximation becomes: \[ \Delta P = -PD^*\Delta y + \frac12 VP (\Delta y)^2 \] Note that as long as duration is positive, this will always produce a price that is higher than the duration approximation. Since it is a better approximation to the true value than the linear approximation, this explains why the duration approximation consistently underestimates the price.

example.ytm.decrease.approx.2.pct = (0.02 * example.duration + 0.5 * example.convexity * 0.02^2) * 100
example.ytm.increase.approx.2.pct = (-0.02 * example.duration + 0.5 * example.convexity * 0.02^2) * 100
print(paste0("The convexity approximation gives price changes of ", example.ytm.increase.approx.2.pct, " percent and ", example.ytm.decrease.approx.2.pct, " for a yield incresae / decrease of 2%, respectively."))
## [1] "The convexity approximation gives price changes of -18.2669157445496 percent and 26.7642176279603 for a yield incresae / decrease of 2%, respectively."

Managing Interest Rate Risk

Immunization

An immunization technique aims to shield an investor from interest rate risk. In a duration matching strategy, the assets are structured so that they have the same Macaulay duration as the liabilities. An example of this approach involves creating an asset portfolio that consists of zero coupon bonds along with perpetutities:

  1. Calculate the duration of the liabilities, \(D_L\) and the present value of the liability.

  2. Calculate the duration of the perputity, \(D_P = 1 + \frac{1}{y}\)

  3. Let \(w\) be the weight of the zero-coupon bonds in the portfolio, assuming a maturity of \(D_B\). Then the duration of the asset portfoliod is \(D_A = wm + (1-w)D_P\)

  4. Solve the equation \(D_L = D_A\) for \(w\). This gives \(w = \frac{D_L - D_P}{D_B - D_P}\).

  5. Multiply \(w\) and \((1-w)\) by the present value of the liabilities to get the amount to invest in bonds and perpetuities, respectievly.

The textbook illustrates this approach by considering a situation in which a company must pay $19,487 in 7 years, with a current market interest rate of 10%. First, calculate the present value of the liability:

PV.liability = 19487 / 1.10^7
duration.liability = 7

The duration of a perputity is:

duration.perpetuity = 1 + 1/0.1

The example assumes that the company is investing in 3-year zero-coupon bonds:

duration.bond = 3

Using the formula above, this gives the weight on the bond as follows;

bond.weight = (duration.liability - duration.perpetuity) / (duration.bond - duration.perpetuity)

The company’s investments should be:

print(paste0("The company should invest ", bond.weight * PV.liability, " in bonds and ", (1 - bond.weight) * PV.liability, " in perpetuities."))
## [1] "The company should invest 4999.95612498089 in bonds and 4999.95612498089 in perpetuities."

The portfolio must be rebalanced for two reasons:

  • As time passes, the duration of the assets and liabilities will change.

  • Interest rates may change.

Consider what happens to the above portfolio one year later even when interest rates stay fixed. In this case, the liabilites now have a present value and duration of:

PV.liability = 19487 / 1.10^6
duration.liability = 6

The duration and present value of the perpetuity remain the same. The bonds are now 2-year bonds:

duration.bond = 2

The weight of bonds in the portfolio is now:

bond.weight = (duration.liability - duration.perpetuity) / (duration.bond - duration.perpetuity)
bond.weight
## [1] 0.5555556

The company’s investments should be:

print(paste0("The company should invest ", bond.weight * PV.liability, " in bonds and ", (1 - bond.weight) * PV.liability, " in perpetuities."))
## [1] "The company should invest 6111.05748608775 in bonds and 4888.8459888702 in perpetuities."

Suppose instead that interest rates fall to 8% in the second year. Now, the liabilities have a present value and duration of:

PV.liability = 19487 / 1.08^6
PV.liability
## [1] 12280.12

The duration of the bond is still 2, but the duration of the perpetuity now changes:

duration.perpetuity = 1 + 1/0.08
duration.perpetuity
## [1] 13.5

The weight of bonds in the portfolio is now:

bond.weight = (duration.liability - duration.perpetuity) / (duration.bond - duration.perpetuity)
bond.weight
## [1] 0.6521739

The company’s investments should be:

print(paste0("The company should invest ", bond.weight * PV.liability, " in bonds and ", (1 - bond.weight) * PV.liability, " in perpetuities."))
## [1] "The company should invest 8008.77099069852 in bonds and 4271.34452837254 in perpetuities."

To illustrate how immunization has protected against the interest rate risk, compare the total amount invested in the second year to the amount available to invest:

print(paste0("The company needs to invest ", bond.weight * PV.liability + (1 - bond.weight) * PV.liability, " in total"))
## [1] "The company needs to invest 12280.1155190711 in total"

The value of the $5000 perpetuity is now:

perpetutity.new.value = 5000 * 0.1 /0.08
perpetutity.new.value
## [1] 6250

The value of the bond is now:

bond.new.value = 5000 * 1.10^3 / 1.08^2
bond.new.value
## [1] 5705.59

Adding in the $500 coupon from the perpetuity,

print(paste0("The company has ", perpetutity.new.value + bond.new.value + 500, " available to invest."))
## [1] "The company has 12455.5898491084 available to invest."

Note that the amount available exceeds the amount needed.

An alternate approach, cash flow matching, involves buying a single zero-coupon bond with face value equal to the expected cash outlay, for each cash flow. This immunizes the portfolio, since the cash flow from the bond and liability offset each other regardless of the interest rate. This is referred to as a dedication strategy. Advantages / disadvantages of each method include:

  • The need for rebalancing is a weakness of the duration matching approach, because it will incur transaction costs every time rebalancing is needed. On the other hand, a dedication strategy does not require rebalancing.

  • A disadvantage of cash flow matching is that it imposes constraints on bond selection; it prevents portfolio managers from taking advantage of “underpriced” bonds.

  • For liabilities with a very long time horizon (e.g. pension payments), bonds of large enough maturity to match the cash flows may not exist.

  • Duration calculations are only valid for a flat yield curve, though it can be generalized by varying the amount used to discount each cash flow. In this case, immunization is only achieved for parallel shifts in the yield curve.

  • Immunization does not work well in an inflationary environment; it only makes sense for notional liabilities.

T-Bond Futures Contracts

Interest rate risk can be assessed through a concept, closely related to duration, called the price value of a basis point (PVBP): \[ \mathrm{PVBP} = \frac{\text{Change in portfolio value}}{\text{Predicted change in yield in BP}} \] This quantity can be used to determine a hedging strategy as follows:

  1. Calculate PVBP for the portfolio, using the duration approximation.

  2. Calculate PVBP for one contract of the hedge vehicle. This is typically a short position in a T-bond future; an assumption is made that the price of the futures contract moves in proportion to the price of the bond. The contract multiplier is the number of futures contracts needed to form one of the hedge contracts.

  3. Calculate the ratio \(H\) of the quantity calculated in step 1 to the quantity calculated in step 2

  4. Buy or short \(H\) of the hedge contracts depending on whether the value of the contracts move in opposite or the same directions, respectively.

Suppose that a bond portfolio of $20M has a modified duration of 4.5. Then its PVBP, based on a 10-basis point change, is:

PVBP.portfolio = 4.5 * 0.001 * 20000000 / 10
PVBP.portfolio
## [1] 9000

This number is the decline in value per basis point.

The hedging vehicle is a $100,000 T-bond futures contract, with a futures price of $90 per $100 par value. Assume the bond to be delivered has a modified duration of 10 years. Assuming that the futures price declines at the same percentage of the bond, then the PVBP of the hedging vehicle is

PVBP.hedge = (10 * 0.001) * 90 * 1000 / 10
PVBP.hedge
## [1] 90

Therefore, the hedge ratio is

PVBP.portfolio / PVBP.hedge
## [1] 100

Therefore, 100 contracts should be shorted to hedge the interest rate risk of this portfolio.

Cross-hedging is the concept that most hedging activity uses a different asset than the one being hedged. There is a risk of “slippage” between the prices of yields of the two asset classes, so to the extent that slippage exists the hedge will not be perfect. In this example, the underlying assumption is that the prices bond portfolio and the T-bond futures contract move perfectly in unison.

Interest Rate Swaps

An interest rate swap is an agreement to exchange a series of cash flows proportional to a fixed interest rate for a series of cash flows proportional to a floating interest rate. The cash flows are the interest on a specified notional principal; it is “notional” in the sense that the parties do not actually trade the principal; they only exchange the interest on the principal. The structure of an interest rate swap is:

  1. Party A wants to convert a fixed interest rate to LIBOR; they pay LIBOR to the swap dealer. The dealer gives Party A the fixed interest rate minus half the bid-ask spread.

  2. Party B wants to convert a LIBOR rate to a fixed interest rate; they pay the fixed rate plus half the bid-ask spread to the swap dealer. The swap dealer pays LIBOR to party B.

  3. The swap dealer bears no interest rate risk since the LIBOR payments cancel out, but earns the bid-ask spread on the notional principal.

A swap can be priced as follows:

  1. Given a variable future cash flow \(F_1, F_2, \ldots, F_n\), discount each to present value using the current spot rate according to its maturity. Denote this present value by \(F\).

  2. Calculate a discount factor \(D\) for the fixed cash flow \(F^*\) as the present value of a $1 payment at each payment time, using the spot rate according to its maturity.

  3. Select \(F^*\) such that \(DF^* = F\).

Suppose that we enter into a contract in which the variable payments and spot rates are as follows:

swap.data = data.frame(time = c(1, 2), payment = c(1.992, 1.955), yield = c(0.05, 0.05))
swap.data
##   time payment yield
## 1    1   1.992  0.05
## 2    2   1.955  0.05

Calculate the present value of the payment, as well as present value of $1 as follows:

swap.data = swap.data %>% mutate(PV_payment = payment / (1 + yield)^time, PV_1 = 1 / (1 + yield)^time)
swap.data
##   time payment yield PV_payment      PV_1
## 1    1   1.992  0.05   1.897143 0.9523810
## 2    2   1.955  0.05   1.773243 0.9070295

Calculate \(F\), \(D\), and \(F^*\) as follows:

swap.F = sum(swap.data$PV_payment)
swap.D = sum(swap.data$PV_1)
swap.F.star = swap.F / swap.D
print(paste0("The fair level price for the swap is ", swap.F.star))
## [1] "The fair level price for the swap is 1.97395121951219"

There is credit risk associated with an interest rate swap, but it is significantly mitigated by two factors:

  • There is no exchange of principal, only interest

  • If either party defaults on the interest portion, then the other party will keep the amount that they had agreed to pay. Therefore, only the difference between the fixed rate and floating rate interest is at risk, not the entire interest payment.

Currency Swaps

A foreign exchange swap involves an exchange of two currencies on several future dates. It is a multi-period extension of a forward contract. Therefore, the swap involves first pricing a series of forward currency contracts.

Suppose we want to create a contract to deliver 1000 units of currency B in \(n\) years, in exchange for \(x\) units of currency \(A\). Let \(r_A\) and \(r_B\) be the risk-free rates in the respective currencies for an \(n\)-year maturity. A synthetic forward contract can be created as follows:

  1. Sell a 1000-par value zero coupon bond in currency B. This generates \(1000 / (1+r_B)^n\).

  2. Exchange the above amount, at today’s spot exchange rate \(E_0\), for \(E_0 1000/(1 + r_B)^n\) units of currency \(A\).

  3. Invest this amount in zero coupon bonds in currency A. At maturity, this will be worth \(1000 E_0(1 + r_A)^n/(1+r_B)^n\).

  4. There is no net investment today, but at maturity, the bond from step 1 is due and the seller owes 1000 in currency B. At the same time, the bond from step 3 matures and the seller receives \(1000 E_0(1 + r_A)^n/(1+r_B)^n\) in currency A.

Therefore, the forward rate must be \[ E_0 \frac{(1+r_A)^n}{(1 + r_B)^n} \]

The example in the textbook uses the following parameters:

risk.free.B = 0.07
risk.free.A = 0.05
spot.A.per.B = 2.03
forward.1 = spot.A.per.B * (1 + risk.free.A) / (1 + risk.free.B)
forward.2 = spot.A.per.B * (1 + risk.free.A)^2 / (1 + risk.free.B)^2
print(paste0("The 1-year forward exchange rate is ", forward.1, " and the 2-year forward exchange rate is ", forward.2))
## [1] "The 1-year forward exchange rate is 1.99205607476635 and the 2-year forward exchange rate is 1.95482138178007"

Pricing a currency swap uses the same algorithm as presented in the section on interest rate swaps: determining the value of a level annuity that has the same present value as the variable cash flow. The discount rate used should be the one of the currency being paid by the swap purchaser (i.e. if we are working with the forward rate to pay in currency A and receive curency B, then the discount rate should be the interest rate on currency A).