Tuesday 28th January, 2020 # Interest Rate Swap Pricing

In this series we are going to demonstrate fixed income derivatives pricing and risk calculation, first looking at necessary market data and trade modelling for swap pricing. We will then venture to price more complicated instruments, and finally, we will see how to run these calculations for large books efficiently using a cluster of computers.

• Part I: Interest Rate Swap Pricing
• Part II: Bermudan Swaption Pricing with Monte Carlo
• Part III: VaR Calculation with Monte Carlo
• Part III: Portfolio Level Pricing With Distributed Systems

The name of the project, hubris (ancient Greek: ὕβρις) relates to a personality quality of extreme or foolish pride or dangerous overconfidence. It is a recurring theme in ancient Greek mythology, for example, can be found in the myth of Icarus, the son of Daedalus. The father built two pairs of wings out of wax and feathers to be able to fly. Daedalus warned his son not to fly too close to the sun, nor too close to the sea, but to follow his path of flight. Eventually, Icarus didn’t listen to his father; he ended up flying too close to the sun, which due to the heat melted the wax. Consequentially Icarus lost his wings and fell into the sea. This can be an analogy when taking too much risk, after some time one might end up paying the price for his hubris. Bringing that picture back to capital markets, it is always important to know how much our assets worth, and what are the associated risks which need to be managed.

This series of articles will slowly walk through the technology behind a hypothetical scenario for a trading and risk team who owns such assets and wants to calculate those present value and market risk.

The simplest asset we have in this example is an interest rate swap. A vanilla swap can be modelled with two underlying assets a fixed rate bond and a floating rate note. A bond is a security which has coupons from inception until maturity. The number of these coupons are determined by the coupon frequency. For the fixed rate bond the coupons are simple, values can be calculated with the help of the fixed rate. The floating rate note is also a bond, however, it has variable coupon rates, which are tied to market rates. These are money market reference rates, like LIBOR or federal funds rates. This requires that we model market data in our project, in this example we will have a LIBOR based yield curve, which will help us to determine the discount factors and zero rates required to calculate the present value of our floating rate coupons. The above diagram shows the classes we used in this project. The key modules for modelling the swap and to calculate the PV are the trade, market data and business calendar. These represent our objects, we also use utilities to load data from files, bootstrap the curve and to interpolate values.

The project uses Java 11 (OpenJDK) as the primary language, lombok for syntactic sugar, JUnit for defining tests and verifying behaviour and sbt for compilation. sbt can be a strange choice for some when using a Java project, though in subsequent parts in these series we will introduce Apache Spark for distributed computing, which is written in Scala and uses sbt natively. More on that later.

## Fixed Leg

``````public interface Instrument {
LocalDate effectiveDate();
}

@SuperBuilder
@Getter
public abstract class Bond implements Instrument {
protected LocalDate effectiveDate;
protected LocalDate maturity;
protected BigDecimal notional;
protected CouponFrequency couponFrequency;
protected DayCountConvention dayCountCon;

public abstract List<Coupon> coupons();

@Override
}

@Override
public LocalDate effectiveDate() {
return effectiveDate;
}

public List<Tuple3<LocalDate, LocalDate, BigDecimal>> couponSchedule() {
return BusinessCalendar.couponSchedule(effectiveDate, maturity, couponFrequency, dayCon, dayCountCon);
}
}

@Getter
@SuperBuilder
public class FixedRateBond extends Bond {
protected BigDecimal rate;

public List<Coupon> coupons() {
var couponSchedule = couponSchedule();
return couponSchedule.stream().map(x -> {
var amount = rate.multiply(notional).multiply(x._3());
return new Coupon(x._1(), x._2(), x._3(), amount);
}).collect(Collectors.toList());
}
}
``````

The above code demonstrates how we represent a fixed rate bond. If we want to test this code, we can create a bond with these example values.

• effective date: 14 November 2011
• notional: \$1,000,000
• coupon frequency: Semi-Annual
• day convention: Modified Following
• day count convention: 30/360
• fixed rate: 1.24%
• maturing on 14 November, 2016
``````public static FixedRateBond createTestFixedRateBond() {
return FixedRateBond.builder()
.effectiveDate(LocalDate.of(2011, 11, 14))
.notional(new BigDecimal(1000000.0))
.couponFrequency(new SemiAnnualCouponFrequency())
.dayCon(new ModifiedFollowing())
.dayCountCon(new DayCountConvention30360())
.rate(new BigDecimal(0.0124))
.maturity(LocalDate.of(2016, 11, 14))
.build();
}

@Test
public void FixedRateBond_shouldHaveCorrectCoupons() {
var frb = createTestFixedRateBond();
var coupons = frb.coupons();
var expected = List.of(6200.0, 6200.0, 6200.0, 6200.0, 6200.0, 6200.0, 6200.0, 6268.888888, 6200.0, 6131.11111);
var ci = coupons.iterator();
expected.forEach(x -> assertEqualsBD(x, ci.next().getAmount()));
}
``````

This gives us the following coupon schedule:

Start Date End Date Time Coupon Amount
2011-11-142012-05-140.5\$6200.00
2012-05-142012-11-140.5\$6200.00
2012-11-142013-05-140.5\$6200.00
2013-05-142013-11-140.5\$6200.00
2013-11-142014-05-140.5\$6200.00
2014-05-142014-11-140.5\$6200.00
2014-11-142015-05-140.5\$6200.00
2015-05-142015-11-160.5055556\$6268.89
2015-11-162016-05-160.5\$6200.00
2016-05-162016-11-140.4944444\$6131.11

## Floating Leg

As mentioned before, floating legs coupon values are dependent on market values. To demonstrate an example, we will use LIBOR rates to construct a zero curve. The first part of the curve can be modelled with cash market rates (overnight to 11 months), the middle of the curve is sometimes modelled with forward rate agreements (FRAs), and the tail of the curve is constructed with swap rates. Please note that this was common around 2010, since then OIS (overnight index swap) curves became the market standard for discounting collateralised cashflows.

Type Term Rate
CashON0.1410%
CashT/N0.1410%
Cash1W0.1910%
Cash2W0.2090%
Cash1M0.2490%
Cash2M0.3450%
Cash3M0.4570%
Cash4M0.5230%
Cash5M0.5860%
Cash6M0.6540%
Cash7M0.7080%
Cash8M0.7540%
Cash9M0.8080%
Cash10M0.8570%
Cash11M0.9130%
Swap1Y0.58%
Swap2Y0.60%
Swap3Y0.72%
Swap4Y0.96%
Swap5Y1.24%
Swap7Y1.73%
Swap10Y2.19%
Swap30Y2.83%

A swap represents a set of potential cashflow exchanges in the future, which are determined with the future market rates. On the other hand, we want to calculate the present value of the swap for a given date. This requires to discount the future values to present values, and to do that we will calculate the discount factors for those dates. This in a nutshell means constructing the swap curve, which will be a zero curve in this example.

To construct the curve, we are going to use bootstrapping, by calculating the discount factors for each market data points. This takes into account the rate for the given period, the time elapsed since the previous point and its discount factor. We can easily hop through the curve, calculating these values with cash values. Though once we use different instruments, like swaps we need to calculate the rates in different way. For the first swap rate we will use the idea that, the swap should be worth par if we receive the principal at maturity. Using this for the one year swap, we can use this formula with the 6 months discount factor (cash) to calculate the discount factor of the swap.

1 = (R1yrparswap × df6mo × TNov14-May14) + (R1yrparswap × df1yr × TMay14-Nov14) + df1yr

``````public class NWayBootstrap implements CurveBootstrap {

private final LocalDate cob;
private final DayCountConvention dayCountCon;

public BigDecimal df(BigDecimal bootstrap, BigDecimal rate, LocalDate start, LocalDate end) {
var value = rate
.multiply(dayCountCon.factor(start, end))
return bootstrap.divide(value, DECIMAL_PRECISION, ROUNDING_MODE);
}

public static LocalDate intervalMiddle(LocalDate start, LocalDate end) {
var days = ChronoUnit.DAYS.between(start, end);
return start.plusDays((days / 2) - 1);
}

public BigDecimal dfSwap(BigDecimal swapRate, BigDecimal dfMiddle, LocalDate start, LocalDate middle, LocalDate end) {
var x1 = BigDecimal.ONE.subtract(swapRate.multiply(dfMiddle.multiply(dayCountCon.factor(start, middle))));
return x1.divide(x2, DECIMAL_PRECISION, ROUNDING_MODE);
}

public BigDecimal dfSwap(BigDecimal bootstrap, BigDecimal swapRate, LocalDate start, LocalDate end) {
var x1 = BigDecimal.ONE.subtract(swapRate.multiply(bootstrap));
var x2 = new BigDecimal(1).add(swapRate.multiply(dayCountCon.factor(start, end)));
return x1.divide(x2, DECIMAL_PRECISION, ROUNDING_MODE);
}

public Map<Rate, BigDecimal> bootstrap(List<Rate> points) {
var dfMap = new HashMap<Rate, BigDecimal>();
var bootstrapMap = new HashMap<LocalDate, BigDecimal>();
var swapBootstrapValue = new BigDecimal(0);
var lastPoint = points.get(0);
for(int i = 0; i < points.size(); i++) {
var p = points.get(i);
BigDecimal dfValue;
if (p instanceof MoneyMarketRate) {
var bootstrap = p.getStartDate().equals(cob) ? new BigDecimal(1) : bootstrapMap.get(p.getStartDate());
dfValue = df(bootstrap, p.getRate(), p.getStartDate(), p.getEndDate());
} else if (p instanceof ParSwapRate) {
if (swapBootstrapValue.compareTo(BigDecimal.ZERO) == 0) {
// first time
var middleDate = intervalMiddle(p.getStartDate(), p.getEndDate());
var dfMiddle = bootstrapMap.get(middleDate);
dfValue = dfSwap(p.getRate(), dfMiddle, p.getStartDate(), middleDate, p.getEndDate());
if (points.get(0).getStartDate().isBefore(p.getStartDate())) {
// the first point is before the swap start date, need to discount that
var dfFirstDate = bootstrapMap.get(p.getStartDate());
dfValue = dfValue.multiply(dfFirstDate);
}
} else {
dfValue = dfSwap(swapBootstrapValue, p.getRate(), lastPoint.getEndDate(), p.getEndDate());
}
swapBootstrapValue = dfValue;
} else {
throw new IllegalArgumentException("Unsupported rate type: " + p);
}
bootstrapMap.put(p.getEndDate(), dfValue);
lastPoint = p;
dfMap.put(p, dfValue);
}
return dfMap;
}

}
``````

Now we have calculated the discount factors for the curve, however, we don’t have discount factors for those coupon dates which are not defined on the swap curve. We are using linear interpolation to calculate these. With this, the floating leg coupons present values can be calculated.

The remaining step is to join the swap’s two legs coupons together, from which we can calculate the present value of each coupon pair and the swap itself. The below example illustrates it for a swap:

• Notional: \$1,000,000 USD
• Coupon Frequency: Semi-Annual
• Fixed Coupon Amount: 1.24%
• Floating Coupon Index: 6 month USD LIBOR
• Business Day Convention: Modified Following
• Fixed Coupon Daycount: 30/360
• Floating Coupon Daycount: Actual/360
• Effective Date: Nov 14, 2011
• Termination Date: Nov 14, 2016
• We will be valuing our swap as of November 10, 2011.
``````@Test
public void createAndPriceSwap_hasCouponsAndPVZeroAtInception() throws Exception {
final var notional = new BigDecimal(1000000);
final var couponFrequency = new SemiAnnualCouponFrequency();
final var fixedCouponAmount = new BigDecimal(0.0124); // 1.24%
final var floatingCouponIndex = "usdlibor_6m";
final var dayConvention = new ModifiedFollowing();
final var fixedCouponDaycount = new DayCountConvention30360();
final var floatingCouponDaycount = new DayCountConventionActual360();
final var effectiveDate = LocalDate.of(2011, 11, 14);
final var terminationDate = LocalDate.of(2016, 11, 14);
final var creationDate = LocalDate.of(2011, 11, 10);
var swap = IRSwap.create(tradeId, notional, couponFrequency, fixedCouponAmount,
dayConvention, fixedCouponDaycount, floatingCouponDaycount, effectiveDate, terminationDate);

// basic verification
assert(null != swap);
assertEquals(effectiveDate, swap.effectiveDate());

var floatingLeg = swap.getFloatingLeg();
assert(null != floatingLeg);
var mktDataCtx = new MarketDataContext(creationDate);
floatingLeg.setMarketData(floatingCouponIndex, mktDataCtx);

// coupons
var couponPairs = swap.couponPairs();
assert(null != couponPairs);
assertEquals(10, couponPairs.size());

// PV
assertEqualsBD(0, swap.price(creationDate));
}
``````
Coupon Date Fixed Coupon Floating Coupon Net Coupon Discount Factor Present Value
2012-05-14\$6200.00\$3306.33-\$2893.670.9966889-\$2884.09
2012-11-14\$6200.00\$2492.65-\$3707.350.9942107-\$3685.88
2013-05-14\$6200.00\$3049.19-\$3150.810.9911884-\$3123.04
2013-11-14\$6200.00\$3152.14-\$3047.860.9880738-\$3011.51
2014-05-14\$6200.00\$4498.44-\$1701.570.9836490-\$1673.75
2014-11-14\$6200.00\$5131.08-\$1068.920.9786276-\$1046.07
2015-05-14\$6200.00\$7807.48\$1607.480.9710461\$1560.94
2015-11-16\$6268.89\$9216.90\$2948.010.9621778\$2836.51
2016-05-16\$6200.00\$11294.90\$5094.900.9514315\$4847.45
2016-11-14\$6131.11\$12708.55\$6577.440.9394919\$6179.45

As per the swap definition, the net fair value of this swap is \$0 as of 10 November, 2011.

This tutorial covered the basics of modelling required for swap pricing, including swap terminology, fixed and floating leg coupon calculations, discounting and curve construction.

The project’s source code is available on our GitHub repository.

In the next article, we will look at a more exotic instrument for which the swap will be an underlying asset.

## Interested in finding out more?

If you are looking at ways to improve how you tackle the challenges of software delivery and would like some help, please get in touch. Or if you are working as a software engineer and you are interested in growing with us, we are always looking for talented people to work with.