4. Using Monoids to compute interest rate risk
The Cats library provides abstractions that allow standard types to be manipulated using algebraic operations. This turns out to be highly useful as it produces more understandable code with higher levels of transparency. This is evident in the code below for computing the sensitivity of a bond to changes in interest rates. To understand this implementation, we first need to recall that a bond's price is equal to the sum of it's discounted payments:
p = sum(payment * exp(-rt))
... where r is the interest rate and t the time in years
From basic calculus, the sensitivity of price (p) to a change in interest rate (r) is then:
dp/dr = sum(-t*payment * exp(-rt))
The overall sensitivity is found by combining the individual results for each payment. This is where Monoids and Semigroups prove to be useful. With both of these, an associative combine operation is an integral part of the definition. In our use case, the combine operation is quite straightforward: it's performed by summing two sensitivities. Now, if we want to extend this to combine all the values across a list, we need to introduce an "identity" operator. Such an operation (see "empty" in code below) is only available in Monoid (not in SemiGroup) and it's presence is essential for our use case; without it, the value of an empty list would be undefined.
implicit val sensitivityMonoid: Monoid[Double] = new Monoid[Double] {
def empty: Double = 0.0
def combine(sensitivity1: Double, sensitivity2: Double): Double =
sensitivity1 + sensitivity2
}
def combineAll(sensitivities: List[Double]): Double =
sensitivities.foldLeft(Monoid[Double].empty)(Monoid[Double].combine)
The definition of SensitivityMonoid is provided to make the working clearer. In actual fact, we could have just used the definition of Monoid{Double] which is already available in Cats. Our code which uses our new Monoid to compute the bond sensitivity looks like this:
case class Payment(date: LocalDate, amount: Double)
def periodInYears(from:LocalDate, to:LocalDate) : Double = {
from.until(to, ChronoUnit.DAYS)/365.0
}
def sensitivity(valuationDate: LocalDate, rate: Double, payment: Payment) = {
val time = periodInYears(valuationDate, payment.date)
payment.amount*time*Math.exp(-rate * time)
}
def sensitivity(payments:List[Payment], rate:Double, valuationDate:LocalDate) :
Double = {
val sensitivities = payments.map(cf => sensitivity(valuationDate, rate, cf))
combineAll(sensitivities)
}
def main(args: Array[String]) = {
val payments = List(
Payment(LocalDate.of(2025, 6, 7), 5),
Payment(LocalDate.of(2025, 12, 7), 105))
println(totalSensitivity(payments, 0.055, LocalDate.of(2024, 6, 7)))
}
Last updated