Interest calculation conventions for money market instruments

Yes, you read the title right. This is a blog about programming, then why is this post about interest calculation? Perfect question. I will use this topic to demonstrate a business case, explain it and then we will take a look on a possible implementation in Java.

What is in it for me?

If you are a beginner Java developer, then you will be finding the value in learning a bit about the use of Spring Boot and the Java Date & Time API, plus some basic knowledge about the jargon in the financial domain.
If you are a seasoned Java veteran, then you can still pick up some of the basic finance concepts we will discuss.
If you are a business analyst or project manager, then you are already familiar with the wonders of the quotation of annual interest rates according to their maturity, but you might want to pick up some coding skills to better understand how your software engineers think.

So, what is the topic?

Our domain is the money market, a market which is used for short term borrowing and lending. Short term means less than one year or as people in finance say: the maturity is generally less than one year.

There are two actors in our domain:
1. Issuer (or Borrower): looking for funding for less than a year
2. Investor (or Lender): looking to lend money for less than a year

Let’s imagine a scenario: I am looking for some money for my startup, I just need it for a month until I could set up my infrastructure and I promise to return it after this month.
In the money market that is called 1M (meaning one month), the funds must be transferred to me (the borrower) on the spot date (in this case in t+2 days) and the loan must be repaid after one calendar month after the spot date (or sometimes called value date of the loan).

The different maturities are these:

NameAbbreviationSpot DateMaturity Date
OvernightONtt+1
Tom-Next (Tomorrow next)TNt+1t+2
Spot NextSNt+2t+3
1 week1Wt+2(t+2) + 7 calendar days
2 weeks2Wt+2(t+2) + 14 calendar days
............
1 month1Mt+2(t+2) + 1 calendar month
2 months2Mt+2(t+2) + 2 calendar months
............

Example:
Take our scenario from before, I ask for a 1 month loan quoted on the 4th of April. Because the spot date is t+2 for 1M, the actual date when I get the money is the 6th of April. The maturity is (t+2) + 1 calendar month, so I have to repay you on the 6th of May.

Getting our hands dirty

GitHub Project: You can grab the project from here. Download it and you can import it into Eclipse with File -> Import… -> Existing Maven Projects. To run the application, simply open MaturityServiceApplication and run it as Spring Boot App.

What we want to implement is a microservice which could be used to get the maturity date of a money market financial instrument. So our specification to this service looks like this:
Input: Quote date and the selected quote (for example 2M)
Output: The spot date, maturity date and the duration in calendar days

First of all if you are not familiar with Spring Boot, then look for a tutorial like this and get a basic understanding of it, I’ll wait for you here. Okay, welcome back and well done for checking out Spring Boot! I will use Eclipse, but feel free to use whatever IDE you feel like. Run the application, put in a few breakpoints, play with it to understand how it works 😉

The 3 domain objects we have are the following:
1. Quote: each instance of the Quote class is a line in the table above. So for example the Overnight quote or the Tom/Next quote will be an instance
2. DateAddition: this is the class which we will use to model the different date additions in the table, for example the t+2 in the spot date column or the (t+2)  + 1 calendar month in the maturity date column
3. LoanMaturity: this will be the output of our microservice, based on the quote date and the selected quote it will tell us the spot date, the maturity date and the duration of the loan in calendar days

Let’s take a look at Quote:

public class Quote extends ResourceSupport{
  @NotNull
  private String name;
  
  @NotNull
  private String abbreviation;
  
  @NotNull
  private DateAddition spotDateDayAddition;
  
  @NotNull
  private DateAddition maturityDateAddition;
  
  public Quote(final String name, final String abbreviation, 
               final DateAddition spotDateDayAddition, 
               final DateAddition maturityDateAddition){
    this.name = name;
    this.abbreviation = abbreviation;
    this.spotDateDayAddition = spotDateDayAddition;
    this.maturityDateAddition = maturityDateAddition;
  }
  
  public LocalDate getSpotDate(final LocalDate quoteDate){
    return quoteDate.plusDays(spotDateDayAddition.getAmount());
  }
  
  public LocalDate getMaturityDate(final LocalDate quoteDate) throws IllegalStateException{
    switch(maturityDateAddition.getUnit()){
      case DAYS:
        return quoteDate.plusDays(maturityDateAddition.getAmount());
      case WEEKS:
        return quoteDate.plusDays(spotDateDayAddition.getAmount()).plusWeeks(maturityDateAddition.getAmount());
      case MONTHS:
        return quoteDate.plusDays(spotDateDayAddition.getAmount()).plusMonths(maturityDateAddition.getAmount());
      default:
        throw new IllegalStateException("Maturity date addition is not a day, week or month!");
    }
  }

  public String getName() {
    return name;
  }

  public String getAbbreviation() {
    return abbreviation;
  }

  @JsonIgnore
  public DateAddition getSpotDateDayAddition() {
    return spotDateDayAddition;
  }

  @JsonIgnore
  public DateAddition getMaturityDateAddition() {
    return maturityDateAddition;
  }
}

As you can notice, the Quote class extends the ResourceSupport class. This is used for the HATEOAS support. HATEOAS stands for Hypermedia as the Engine of Application State and what it means is to use a subset of REST to create a dynamic API which can guide the client. As you can see, the annotation @JsonIgnore was added to the spotDateDayAddition and maturityDateAddition fields, instead of sending these to the client, what we will do is to provide a link to the client where it can access this data.

If you take a look at the constructor parameters, you will notice that all parameters are final. The reason for this is to avoid any kind of accidental reassignation.
In the getSpotDate and getMaturityDate methods you can see the implementation for date manipulations based on the rules in the table above. In these methods the plusDays, plusWeeks and plusMonths methods are used. As the naming of these methods suggests, they are used to add either days, weeks or months to a LocalDate. Keep in mind that the actual LocalDate is not changed, these methods create a new LocalDate with the updated date and return it.
Let’s take a quick look at the DateAddition class:

public class DateAddition extends ResourceSupport{
  public static final int DAY_LIMIT = 3;
  public static final int WEEK_LIMIT = 52;
  public static final int MONTH_LIMIT = 12;
  
  @NotNull
  private final ChronoUnit unit;
  
  @NotNull
  @Min(value = 0)
  private final Integer amount;
  
  public DateAddition(final ChronoUnit unit, final Integer amount){
    checkParameters(unit, amount);
    this.unit = unit;
    this.amount = amount;
  }
  
  private void checkParameters(final ChronoUnit unit, final Integer amount) throws IllegalArgumentException {
    switch (unit){
      case DAYS:
        checkAmountParamter(DAY_LIMIT, amount);
        break;
      case WEEKS:
        checkAmountParamter(WEEK_LIMIT, amount);
        break;
      case MONTHS:
        checkAmountParamter(MONTH_LIMIT, amount);
        break;
      default:
        throw new IllegalArgumentException("Invalid time unit parameter!");
    }
  }
  
  private void checkAmountParamter(final Integer limit, final Integer amount){
    if(amount > limit || amount < 0){
      throw new IllegalArgumentException("Invalid amount parameter!");
    }
  }

  public ChronoUnit getUnit() {
    return unit;
  }

  public Integer getAmount() {
    return amount;
  }
}

We define a few constants in this class to model the different limits we have for the different time units (eg.: Days, Weeks and Months).
To model the Overnight, Tom/Next and Spot Next quotes we set the limit for days to 3 and because we talk about Money Market instruments, their maturity is less than a year, so the limits for he weeks is 52 and for the months it is 12.
Luckily we don’t have to model the concepts of the days, weeks and months, since the ChronoUnit already defines these as a thread safe enum.
Finally let’s check the LoanMaturity class, which will be telling us the spot date, maturity date and the duration of the loan:

public class LoanMaturity {
  @NotNull
  private final LocalDate quoteDate;
  
  @NotNull
  private final LocalDate spotDate;
  
  @NotNull
  private final LocalDate maturityDate;
  
  @NotNull
  private final Long durationInCalendarDays;
  
  public LoanMaturity(final LocalDate quoteDate, final LocalDate spotDate, final LocalDate maturityDate){
    this.quoteDate = quoteDate;
    this.spotDate = spotDate;
    this.maturityDate = maturityDate;
    this.durationInCalendarDays = ChronoUnit.DAYS.between(spotDate, maturityDate);
  }
  
  public LoanMaturity(final LocalDate quoteDate, final Quote quote){
    this(quoteDate, quote.getSpotDate(quoteDate), quote.getMaturityDate(quoteDate));
  }
  
  public LocalDate getQuoteDate() {
    return quoteDate;
  }
  
  public LocalDate getSpotDate() {
    return spotDate;
  }
  
  public LocalDate getMaturityDate() {
    return maturityDate;
  }
  
  public Long getDurationInCalendarDays() {
    return durationInCalendarDays;
  }
}

To calculate the duration of the loan in calendar days, we use the between method which is defined on the ChronoUnit.
Now we have our domain modelled, it is time to create the possible quotes. To do this, we will create them dynamically in a service (alternatively we could also store them in a database for example):

@Service
public class MaturityService {
  private Map<String, Quote> quotesMap = new ConcurrentHashMap<>();
  
  @PostConstruct
  private void init(){
    addToMap(new Quote("Overnight", "ON", new DateAddition(ChronoUnit.DAYS, 0), new DateAddition(ChronoUnit.DAYS, 1)));
    addToMap(new Quote("Tom-Next", "TN", new DateAddition(ChronoUnit.DAYS, 1), new DateAddition(ChronoUnit.DAYS, 2)));
    addToMap(new Quote("Spot Next", "SN", new DateAddition(ChronoUnit.DAYS, 2), new DateAddition(ChronoUnit.DAYS, 3)));
    
    for(int i = 1; i <= DateAddition.WEEK_LIMIT; i++){
      String name = i + "Week" + (i != 1 ? "s" : "");
      addToMap(new Quote(name, i + "W", new DateAddition(ChronoUnit.DAYS, 2), new DateAddition(ChronoUnit.WEEKS, i)));
    }
    
    for(int i = 1; i <= DateAddition.MONTH_LIMIT; i++){
      String name = i + "Month" + (i != 1 ? "s" : "");
      addToMap(new Quote(name, i + "M", new DateAddition(ChronoUnit.DAYS, 2), new DateAddition(ChronoUnit.MONTHS, i)));
    }
  }
  
  private void addToMap(final Quote quote){
    quotesMap.put(quote.getAbbreviation(), quote);
  }
  
  public LoanMaturity getLoanMaturity(final LocalDate quoteDate, final Quote quote){
    return new LoanMaturity(quoteDate, quote);
  }
  
  public Map<String, Quote> getQuotesMap(){
    return this.quotesMap;
  }
}

As you can see, the class is annotated with the @Service annotation. This is a specialisation of the @Component annotation and it shows Spring that this class is a service.
The initialization of our quotes will be done in the init() method, which is marked by a @PostConstruct annotation. This is a lifecycle callback method which will be called by the Spring framework. As you can see we create the different quotes and put them in a map for easier access, using their abbreviation as key, since the most likely lookup will be based on this field. You can read more about the concurrent maps here.
Now our service is ready, time to expose it via a controller:

@RestController
public class MaturityServiceController {

  @Autowired
  private MaturityService service;
  
  @RequestMapping(method=GET, value="/quote/{abbreviation}/maturitydateaddition")
  public DateAddition maturityDateAddtion(@PathVariable final String abbreviation){
    return service.getQuotesMap().get(abbreviation).getMaturityDateAddition();
  }
  
  @RequestMapping(method=GET, value="/quote/{abbreviation}/spotdatedayaddition")
  public DateAddition spotdatedayaddition(@PathVariable final String abbreviation){
    return service.getQuotesMap().get(abbreviation).getSpotDateDayAddition();
  }
  
  @RequestMapping(method=GET, value="/quote/{abbreviation}")
  public Quote quote(@PathVariable final String abbreviation){
    Quote q = service.getQuotesMap().get(abbreviation);
    
    q.removeLinks();
    
    DateAddition maturityDateAddition = methodOn(MaturityServiceController.class).maturityDateAddtion(q.getAbbreviation());
    q.add(linkTo(maturityDateAddition).withRel("maturityDateAddition"));
    
    DateAddition spotDateDayAddition = methodOn(MaturityServiceController.class).spotdatedayaddition(q.getAbbreviation());
    q.add(linkTo(spotDateDayAddition).withRel("spotDateDayAddition"));
    
    return q;
  }
  
  @RequestMapping(method=GET, value="/quotes")
  public List<Quote> quotes(){
    List<Quote> quotes = new ArrayList<Quote>(service.getQuotesMap().values());
    Collections.sort(quotes, (Quote q1, Quote q2) -> q1.getName().compareTo(q2.getName()));
    for(Quote q : quotes){
      q.removeLinks();
      q.add(linkTo(methodOn(MaturityServiceController.class).quote(q.getAbbreviation())).withSelfRel());
    }
    return quotes;
  }
  
  @RequestMapping(method=GET, value="/loanmaturity/{abbreviation}/{quoteDate}")
  public LoanMaturity loanMaturity(@PathVariable final String abbreviation, 
         @PathVariable @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) final LocalDate quoteDate){
    return service.getLoanMaturity(quoteDate, service.getQuotesMap().get(abbreviation));
  }
}

The controller has a @RestController annotation and uses the service with the help of the @Autowired annotation. The @RequestMapping annotation is used to map the web requests to our methods in the controller. We only provide GET methods to the clients. In these methods you can see how variable parameters could be passed via the URL path with the help of @PathVariable and how we can format these parameters into objects, such as a LocalDate with the help of @DateTimeFormat.
You can see HATEOAS at work here with the use of methods suchs as methodOn, withRel and linkTo. Since our Quote extends the ResourceSupport class, we have access to the add method which we can use to store links. In the quotes method which we use to list of all quotes in the microservice, we use a lambda to implement the necessary Comparator.
After starting up the application by running the MaturityServiceApplication class, the application is available on the 8080 port.
If you open the URL http://localhost:8080/quote/TN in your browser, you will be able to see the response in JSON format:

{ 
   "name":"Tom-Next",
   "abbreviation":"TN",
   "_links":{ 
      "maturityDateAddition":{
         "href":"http://localhost:8080/quote/TN/maturitydateaddition"
      },
      "spotDateDayAddition":{
         "href":"http://localhost:8080/quote/TN/spotdatedayaddition"
      }
   }
}

To get the details of a specific loan, you can use this URL for example: http://localhost:8080/loanmaturity/1M/2018-10-25. The response should be:

{
   "quoteDate":{
      "year":2018,
      "month":"OCTOBER",
      "dayOfMonth":25,
      "dayOfWeek":"THURSDAY",
      "era":"CE",
      "dayOfYear":298,
      "leapYear":false,
      "monthValue":10,
      "chronology":{
         "id":"ISO",
         "calendarType":"iso8601"
      }
   },
   "spotDate":{
      "year":2018,
      "month":"OCTOBER",
      "dayOfMonth":27,
      "dayOfWeek":"SATURDAY",
      "era":"CE",
      "dayOfYear":300,
      "leapYear":false,
      "monthValue":10,
      "chronology":{
         "id":"ISO",
         "calendarType":"iso8601"
      }
   },
   "maturityDate":{
      "year":2018,
      "month":"NOVEMBER",
      "dayOfMonth":27,
      "dayOfWeek":"TUESDAY",
      "era":"CE",
      "dayOfYear":331,
      "leapYear":false,
      "monthValue":11,
      "chronology":{
         "id":"ISO",
         "calendarType":"iso8601"
      }
   },
   "durationInCalendarDays":31
}

To test the controller, a few unit tests were written:

@RunWith(SpringRunner.class)
@SpringBootTest
public class MaturityServiceControllerTest {

  @Autowired
  private MaturityServiceController controller;
  
  @Test
  public void loanMaturityTestForON() throws Exception{    
    LoanMaturity loan = controller.loanMaturity("ON", LocalDate.of(2017, 4, 4));
    
    assertEquals(new Long(1), loan.getDurationInCalendarDays());
    assertEquals(LocalDate.of(2017, 4, 4), loan.getQuoteDate());
    assertEquals(LocalDate.of(2017, 4, 4), loan.getSpotDate());
    assertEquals(LocalDate.of(2017, 4, 5), loan.getMaturityDate());
  }
  
  @Test
  public void loanMaturityTestForTN() throws Exception{    
    LoanMaturity loan = controller.loanMaturity("TN", LocalDate.of(2017, 4, 4));
    
    assertEquals(new Long(1), loan.getDurationInCalendarDays());
    assertEquals(LocalDate.of(2017, 4, 4), loan.getQuoteDate());
    assertEquals(LocalDate.of(2017, 4, 5), loan.getSpotDate());
    assertEquals(LocalDate.of(2017, 4, 6), loan.getMaturityDate());
  }
  
  @Test
  public void loanMaturityTestForSN() throws Exception{    
    LoanMaturity loan = controller.loanMaturity("SN", LocalDate.of(2017, 4, 4));
    
    assertEquals(new Long(1), loan.getDurationInCalendarDays());
    assertEquals(LocalDate.of(2017, 4, 4), loan.getQuoteDate());
    assertEquals(LocalDate.of(2017, 4, 6), loan.getSpotDate());
    assertEquals(LocalDate.of(2017, 4, 7), loan.getMaturityDate());
  }
  
  @Test
  public void loanMaturityTestFor1W() throws Exception{    
    LoanMaturity loan = controller.loanMaturity("1W", LocalDate.of(2017, 4, 4));
    
    assertEquals(new Long(7), loan.getDurationInCalendarDays());
    assertEquals(LocalDate.of(2017, 4, 4), loan.getQuoteDate());
    assertEquals(LocalDate.of(2017, 4, 6), loan.getSpotDate());
    assertEquals(LocalDate.of(2017, 4, 13), loan.getMaturityDate());
  }
  
  @Test
  public void loanMaturityTestFor2W() throws Exception{    
    LoanMaturity loan = controller.loanMaturity("2W", LocalDate.of(2017, 4, 4));
    
    assertEquals(new Long(14), loan.getDurationInCalendarDays());
    assertEquals(LocalDate.of(2017, 4, 4), loan.getQuoteDate());
    assertEquals(LocalDate.of(2017, 4, 6), loan.getSpotDate());
    assertEquals(LocalDate.of(2017, 4, 20), loan.getMaturityDate());
  }
  
  @Test
  public void loanMaturityTestFor1MWith30DayCalendarMonth() throws Exception{    
    LoanMaturity loan = controller.loanMaturity("1M", LocalDate.of(2017, 4, 4));
    
    assertEquals(new Long(30), loan.getDurationInCalendarDays());
    assertEquals(LocalDate.of(2017, 4, 4), loan.getQuoteDate());
    assertEquals(LocalDate.of(2017, 4, 6), loan.getSpotDate());
    assertEquals(LocalDate.of(2017, 5, 6), loan.getMaturityDate());
  }
  
  @Test
  public void loanMaturityTestFor1MWith31DayCalendarMonth() throws Exception{    
    LoanMaturity loan = controller.loanMaturity("1M", LocalDate.of(2017, 10, 25));
    
    assertEquals(new Long(31), loan.getDurationInCalendarDays());
    assertEquals(LocalDate.of(2017, 10, 25), loan.getQuoteDate());
    assertEquals(LocalDate.of(2017, 10, 27), loan.getSpotDate());
    assertEquals(LocalDate.of(2017, 11, 27), loan.getMaturityDate());
  }
  
  @Test
  public void loanMaturityTestFor2M() throws Exception{    
    LoanMaturity loan = controller.loanMaturity("2M", LocalDate.of(2017, 4, 4));
    
    assertEquals(new Long(61), loan.getDurationInCalendarDays());
    assertEquals(LocalDate.of(2017, 4, 4), loan.getQuoteDate());
    assertEquals(LocalDate.of(2017, 4, 6), loan.getSpotDate());
    assertEquals(LocalDate.of(2017, 6, 6), loan.getMaturityDate());
  }
}

 

András Döbröntey

About the Author

András Döbröntey

Leave a Comment:

Bitnami