Blog

Tuesday 28th January, 2020

Hubris Part I

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.

Hubris UML class diagram

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 {
  String tradeId();
  LocalDate effectiveDate();
}

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

  public abstract List<Coupon> coupons();

  @Override
  public String tradeId() {
    return tradeId;
  }

  @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))
        .add(new BigDecimal(1));
    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))));
    var x2 = BigDecimal.ONE.add(swapRate.multiply(dayCountCon.factor(middle, end)));
    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 tradeId = "irswap1";
  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(tradeId, swap.tradeId());
  assertEquals(effectiveDate, swap.effectiveDate());

  // add market data
  var floatingLeg = swap.getFloatingLeg();
  assert(null != floatingLeg);
  var mktDataCtx = new MarketDataContext(creationDate);
  floatingLeg.setMarketData(floatingCouponIndex, mktDataCtx);
  mktDataCtx.loadYieldCurve(floatingCouponIndex, floatingLeg.getDayCon(), floatingLeg.getDayCountCon());

  // 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.

Rate Swaps