Skip to content

Python methods to create a Ho-Lee binomial interest rate model for fixed income security pricing: caps, swaps, bonds, etc.

Notifications You must be signed in to change notification settings

wrcarpenter/Interest-Rate-Models

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

71 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Interest Rate Models

Fixed income bonds and derivatives are complicated financial instruments whose pricing is often conditional on the path of interest rates in the future. The assumption that current rates will remain 'static' is overly simplistic, especially when one needs to price a security with embedded options (ex: swaption, callable bond, etc.).

This repository focuses on the implementation of the acclaimed Ho-Lee interest rate model in a binomial lattic framework, which is somewhat similar to valuation methods in the equity derivative market. The fixed income industry has become more complex since this model was initially created but it has been used in practice at some point to develop pricing models for banks and other market participants. Once a model is created, one can price various securities traded daily in fixed income markets via tree discount pricing or via monte carlo simluation from rate paths generated from a given tree, such as:

Image

Table of Contents

Introduction

Objectives

Ho-Lee Rate Model Construction

Cap Pricing

Swap Pricing

Coupon Bond Pricing

Objectives

  • Use market pricing data to calibrate a Ho-Lee binomial interest tree model
  • Price various interest rate securities and derivatives with a tree model: caps, floors, swaps, bonds, etc.
  • Create Monte Carlo simluations from a given tree and compare pricing results to binomial pricing
  • Use Monte Carlo interest rate simulations to price path-dependent securities, like mortgages

Ho-Lee Rate Model Construction

The Ho-Lee model was introduced in 1986 by Thomas Ho and Sang Bin Lee. Generally, it defines a short rate to follow a stochastic process:

$$dr^* = \theta(t)dt + \sigma dZ^*$$

The drift term, $\theta(t)dt$, is time-varying which allows the model to be calibrated to match a given term structure of interest rates (by varying theta each period). Thus, the model will produce rates that can price a zero coupon bond in any given month to match market prices. One potential downside of this model is that it allows interest rates to become negative, which many practictioners sought to avoid because it seemed unlikely that would ever occur in real markets. However, recent negative interest rate environmetns in Japan and Europe over the past decade could make this model more plausible moving forward.

Model Dynamics

In order to use the Ho-Lee model to build a tree, the dynamics must be discretized so the short rate in the tree follows:

Move up the tree:

$$r^{*}_{t+\Delta t} = r^{*}_t + \theta (t) \Delta t + \sigma \sqrt{\Delta t}$$

Move down the tree:

$$r^{*}_{t+\Delta t} = r^{*}_t + \theta (t) \Delta t - \sigma \sqrt{\Delta t}$$

This will produce a tree model that can then be used for pricing via backwards discounting (martingale condition) or with Monte Carlo simulation where paths are sampled from the rates in the tree. The following code is what implements construction of a tree once all relevant variables are provided:

def rateTree(r0, theta, sigma, delta):

    tree = np.zeros([len(theta), len(theta)])
    
    tree[0,0] = r0
       
    for col in range(1, len(tree)):
        
        tree[0, col] = tree[0, col-1] + theta[col]*delta+sigma*math.sqrt(delta)
   
    for col in range(1, len(tree)):
        for row in range(1, col+1):
            tree[row, col] = tree[row-1, col] - 2*sigma*math.sqrt(delta)

    return tree

Determining Interest Rate Volatility $\sigma$

The Ho-Lee model assumes a constant volatility which means it cannot match a given term structure of volatility in the market. This is certainly one downside of the model because options typically have different volatilities at different maturities. Other models (such as the Black-Derman-Toy model) were subsequently created to handle a term structure of volatility.

Zero Coupon Bond Prices

Zero Coupon Bond (ZCB) prices are an input used for calibrating the model to market data. ZCBs for Treasury securities can be bootstrapped from market data. See this project that goes into detail on how to implement a bootstapping method in python and subsequently use ZCBs to determine the yield-spreads of various bonds.

Calibrating for Theta $\theta$ to Price Zero Coupon Bonds

The Ho-Lee model has a time-varying $\theta$ that gives it the flexibilty to be calibrated to match a given term structure of interest rates. To accomplish this, market data of ZCB prices must be provided and then the tree is iteratively constructed, choosing a $\theta_i$ at each point to fit all given zero coupon bond prices. The following code is part of the functions that executes this calibration process:

def build(zcb, sigma, delta):
    
    # empty rates tree
    tree  = np.zeros([zcb.shape[1], zcb.shape[1]])
    # empty theta tree
    theta = np.zeros([zcb.shape[1]]) 
    
    # Initial Zero Coupon rate
    tree[0,0] = np.log(zcb[0,0])*-1/delta

    r0 = tree[0,0]
    
    for i in range(1, len(theta)):
        
        solved   = calibrate(tree, zcb[0,i], i, sigma, delta)
        
        # update theta array
        theta[i] = solved[0]
        tree     = solved[1]
    
    return [r0, tree, theta]

The calibration process is arguably the most computationally intensive process of the tree creation process but it produces a $\theta$ array and a fully constructed tree that can immediately be used and subsequently reused for security pricing. Below is example of a calibrated model where prices of ZCBs generated from the tree are compared to the ZCB market data. The fit is extremely precise:

Image

Binomial Tree Pricing Method

There are a few important conditions that must be met for tree pricing via the Ho-Lee model to be possible: risk neautral dymanics, martingale assumption, etc. The Ho-Lee trees created satisfy these conditions and allows backward discounting in the tree to provide valid prices for bonds and derivatives. The below algorithm is what is implemented in code that is essentially taking a weighted average of cash flows at each node in the tree and discounting them back to the present.

def priceTree(rates, prob, cf, delta, typ, notion):
        
    # include extra column for payoff         
    tree = np.zeros([len(rates)+1, len(rates)+1])
    
    # assign security payoff
    tree[:,len(tree)-1] = payoff(notion, typ)
    
    # interate through the price tree    
    for col in reversed(range(0,len(tree)-1)):  
        
        for row in range(0, col+1):
            
            rate = rates[row,col]
            pu = pd = 1/2 
            tree[row, col] = np.exp(-1*rate*delta)* \
                             (pu*(tree[row, col+1]+cf[row,col+1]) + pd*(tree[row+1, col+1]+cf[row+1, col+1]))      
    
    return (tree[0,0], tree) 

This algorithm enables one to price securities like bonds, swaps, caps, floors, etc and can be expanded quickly to handle more complicated derivatives like swaptions or callable bonds with coupon payments.

Monte Carlo Pricing Method

Monte Carlo simulations are a popular methodology for pricing fixed income securities, especially when payoffs can be "path dependent." One common example of this can be found in mortgages, a multi-billion dollar market traded by all of Wall Street's largest banks. Assumptions about borrower prepayment often depend on interest rates they have seen in the past, thus the entire path of rates is important to simluation for future cash flows.

With a Ho-Lee model, simulation can be introduced by randomly moving up or down the branches assuming an "up" or "down" probability of 50%. The following code performs this simulation for an arbitrary number of paths when given a rate tree:

# Monte carlo simluation 
def tree_monte_carlo(tree, paths):    

    monte = np.zeros([len(tree)-1, paths])

    monte[0,:] = tree[0,0] # assign initial interest rate 
    
    for col in range(0,monte.shape[1]):
        
        r = 0
        c = 0
        p = 0
        
        for row in range(1, monte.shape[0]):
        
            p = random.rand()
            
            if p > 0.5:
                # move through rate tree
                monte[row, col] = tree[r, c+1]
                # update position on rate tree
                r = r 
                c = c+1
            else:
                # move through rate tree
                monte[row, col] = tree[r+1, c+1]
                # update position on rate tree
                r = r + 1
                c = c + 1

    periods = np.arange(1, len(tree), 1)
    monte   = pd.DataFrame(monte)
    monte.insert(0, 'Period', periods)
    
    return monte

The following is how paths on simulated tree could appear and how they compare to provide ZCB pricing data used to calibrate the tree:

Image

Cap Pricing

An interest rate cap is a derivative that pays the cap-holder the difference between a reference rate ($rR) and a strike rate ($\bar{r}$), if and only if, the reference rate is higher than the strike rate. In markets, caps offer insurance against increasing interest rate for many borrowers, especially those who have sold floating rate debt. Selling a floating rate coupon and buying a cap effectively sets a ceiling on how much interest a borrower might have to pay on a bond. If rates increase substantially, the borrower can use the proceeds from the cap agreement to make higher payments on their floating rate obligation. A caps cash flows can be written as:

$$CF(t) = \Delta t * N * max(r(t-1)-\bar{r}, 0)$$

Implementing this in code below:

def cf_cap(rates, strike, delta, notion, cpn):

    cf  = np.zeros([len(rates)+1, len(rates)+1])

    for col in range(0, len(cf)-1):
        for row in range(0, col+1):
            rate = rates[row,col]
            cf[row, col] = delta*notion*max(rate-strike/100, 0)

    return cf

Using the model, here is one example of how a cap price varies based on a strike rate (holding volatility, cap notional amount, etc constant).

Image

Image

This is sensible considering a cap with a very low strike (ex: around 1%) should be "expensive" in the current rate environment, where the current short rate is about 5% and longer dated Treasury bonds are all 4% yield or higher. The same is also true for the extremely low cost of a high-strike cap, considering that the market is not pricing in future rates as high as 9-12%.

Swap Pricing

An interest rate swap is an agreement between two parties where one pays a fixed rate over a number of periods and the other pays a floating rate, connected to some reference rate in the market (ex: SOFR or previously LIBOR). At a given time, the actual cash exchanged between the two parties is:

$$CF(t) = \Delta t * N * (r(t-1) - \bar{r})$$

In the case above, that would be the cash flows for a 'payer swap' where a fixed rate is being paid, and a floating rate is being received. The code below implements a payer swap cash flow:

def cf_swap(rates, strike, delta, notion, cpn):
    
    f  = np.zeros([len(rates)+1, len(rates)+1])

    for col in range(0, len(cf)-1):
        for row in range(0, col+1):
            rate = rates[row,col]
            cf[row, col] = delta*notion*(rate-strike/100)    
            
    return cf

Using the model, here is one example of how a payer swap price varies based on a strike rate (holding volatility, cap notional amount, etc constant).

Image

Image

In the real world, swap rates (fixed leg) are quoted so that the cost of a swap at inception is actually zero. In some instances above, the swap price becomes negative because the strike is so high that the "payer leg" should actually be paid for entering the swap, rather than pay a price for it.

Coupon Bond Pricing

A coupon bond is a fixed income security that pays a certain coupon overtime to the bondholder, which is assumed to be constant for pricing below. In reality, bonds can have floating rate coupons indexed to SOFR, LIBOR, etc. A bonds cashflows can be written as:

$$CF(t) = \Delta t * N * r(t-1)$$

The code below implements an array of coupon payments for the bond cash flow:

def cf_bond(rates, strike, delta, notion, cpn):
    
    cf  = np.zeros([len(rates)+1, len(rates)+1])

    for col in range(0, len(cf)-1):
        for row in range(0, col+1):
            cf[row, col] = delta*notion*cpn/100  
    
    return cf

Using the model, here is one example of how a bond price varies based on a coupon (holding volatility, cap notional amount, etc constant).

Image

Image

It makes sense that the bond price would increase with a higher coupon rate; the investor is paying more for all the intermediate cash flows they will receive from the borrower.

About

Python methods to create a Ho-Lee binomial interest rate model for fixed income security pricing: caps, swaps, bonds, etc.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages