mirror of
https://github.com/corda/corda.git
synced 2025-01-20 03:36:29 +00:00
Merged in interest-rate-interpolation (pull request #58)
Rates oracle - missing value interpolation
This commit is contained in:
commit
27a244e89c
@ -59,7 +59,7 @@ abstract class RatePaymentEvent(date: LocalDate,
|
||||
abstract val flow: Amount
|
||||
|
||||
val days: Int get() =
|
||||
dayCountCalculator(accrualStartDate, accrualEndDate, dayCountBasisYear, dayCountBasisDay)
|
||||
calculateDaysBetween(accrualStartDate, accrualEndDate, dayCountBasisYear, dayCountBasisDay)
|
||||
|
||||
val dayCountFactor: BigDecimal get() =
|
||||
// TODO : Fix below (use daycount convention for division)
|
||||
|
@ -58,8 +58,7 @@ data class Amount(val pennies: Long, val currency: Currency) : Comparable<Amount
|
||||
operator fun times(other: Long): Amount = Amount(Math.multiplyExact(pennies, other), currency)
|
||||
operator fun div(other: Int): Amount = Amount(pennies / other, currency)
|
||||
operator fun times(other: Int): Amount = Amount(Math.multiplyExact(pennies, other.toLong()), currency)
|
||||
|
||||
// override fun toString(): String = currency.currencyCode + " " + (BigDecimal(pennies).divide(BigDecimal(100))).setScale(2).toPlainString()
|
||||
|
||||
override fun toString(): String = (BigDecimal(pennies).divide(BigDecimal(100))).setScale(2).toPlainString()
|
||||
|
||||
override fun compareTo(other: Amount): Int {
|
||||
@ -76,6 +75,7 @@ fun Iterable<Amount>.sumOrZero(currency: Currency) = if (iterator().hasNext()) s
|
||||
//
|
||||
// Interest rate fixes
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/** A [FixOf] identifies the question side of a fix: what day, tenor and type of fix ("LIBOR", "EURIBOR" etc) */
|
||||
data class FixOf(val name: String, val forDay: LocalDate, val ofTenor: Tenor)
|
||||
@ -83,10 +83,7 @@ data class FixOf(val name: String, val forDay: LocalDate, val ofTenor: Tenor)
|
||||
/** A [Fix] represents a named interest rate, on a given day, for a given duration. It can be embedded in a tx. */
|
||||
data class Fix(val of: FixOf, val value: BigDecimal) : CommandData
|
||||
|
||||
/**
|
||||
* Represents a textual expression of e.g. a formula
|
||||
*
|
||||
*/
|
||||
/** Represents a textual expression of e.g. a formula */
|
||||
@JsonDeserialize(using = ExpressionDeserializer::class)
|
||||
@JsonSerialize(using = ExpressionSerializer::class)
|
||||
data class Expression(val expr: String)
|
||||
@ -103,32 +100,63 @@ object ExpressionDeserializer : JsonDeserializer<Expression>() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder class for the Tenor datatype - which is a standardised duration of time until maturity */
|
||||
/** Placeholder class for the Tenor datatype - which is a standardised duration of time until maturity */
|
||||
data class Tenor(val name: String) {
|
||||
private val amount: Int
|
||||
private val unit: TimeUnit
|
||||
|
||||
init {
|
||||
val verifier = Regex("([0-9])+([DMYW])") // Only doing Overnight, Day, Week, Month, Year for now.
|
||||
if (!(name == "ON" || verifier.containsMatchIn(name))) {
|
||||
throw IllegalArgumentException("Unrecognized tenor : $name")
|
||||
if (name == "ON") {
|
||||
// Overnight
|
||||
amount = 1
|
||||
unit = TimeUnit.Day
|
||||
} else {
|
||||
val regex = """(\d+)([DMYW])""".toRegex()
|
||||
val match = regex.matchEntire(name)?.groupValues ?: throw IllegalArgumentException("Unrecognised tenor name: $name")
|
||||
|
||||
amount = match[1].toInt()
|
||||
unit = TimeUnit.values().first { it.code == match[2] }
|
||||
}
|
||||
}
|
||||
|
||||
fun daysToMaturity(startDate: LocalDate, calendar: BusinessCalendar): Int {
|
||||
val maturityDate = when (unit) {
|
||||
TimeUnit.Day -> startDate.plusDays(amount.toLong())
|
||||
TimeUnit.Week -> startDate.plusWeeks(amount.toLong())
|
||||
TimeUnit.Month -> startDate.plusMonths(amount.toLong())
|
||||
TimeUnit.Year -> startDate.plusYears(amount.toLong())
|
||||
else -> throw IllegalStateException("Invalid tenor time unit: $unit")
|
||||
}
|
||||
// Move date to the closest business day when it falls on a weekend/holiday
|
||||
val adjustedMaturityDate = calendar.applyRollConvention(maturityDate, DateRollConvention.ModifiedFollowing)
|
||||
val daysToMaturity = calculateDaysBetween(startDate, adjustedMaturityDate, DayCountBasisYear.Y360, DayCountBasisDay.DActual)
|
||||
|
||||
return daysToMaturity.toInt()
|
||||
}
|
||||
|
||||
override fun toString(): String = "$name"
|
||||
|
||||
enum class TimeUnit(val code: String) {
|
||||
Day("D"), Week("W"), Month("M"), Year("Y")
|
||||
}
|
||||
}
|
||||
|
||||
/** Simple enum for returning accurals adjusted or unadjusted.
|
||||
/**
|
||||
* Simple enum for returning accurals adjusted or unadjusted.
|
||||
* We don't actually do anything with this yet though, so it's ignored for now.
|
||||
*/
|
||||
enum class AccrualAdjustment {
|
||||
Adjusted, Unadjusted
|
||||
}
|
||||
|
||||
/** This is utilised in the [DateRollConvention] class to determine which way we should initially step when
|
||||
/**
|
||||
* This is utilised in the [DateRollConvention] class to determine which way we should initially step when
|
||||
* finding a business day
|
||||
*/
|
||||
enum class DateRollDirection(val value: Long) { FORWARD(1), BACKWARD(-1) }
|
||||
|
||||
/** This reflects what happens if a date on which a business event is supposed to happen actually falls upon a non-working day
|
||||
/**
|
||||
* This reflects what happens if a date on which a business event is supposed to happen actually falls upon a non-working day
|
||||
* Depending on the accounting requirement, we can move forward until we get to a business day, or backwards
|
||||
* There are some additional rules which are explained in the individual cases below
|
||||
*/
|
||||
@ -173,9 +201,10 @@ enum class DateRollConvention {
|
||||
|
||||
|
||||
/**
|
||||
* This forms the day part of the "Day Count Basis" used for interest calculation.
|
||||
* Note that the first character cannot be a number (enum naming constraints), so we drop that
|
||||
* in the toString lest some people get confused. */
|
||||
* This forms the day part of the "Day Count Basis" used for interest calculation.
|
||||
* Note that the first character cannot be a number (enum naming constraints), so we drop that
|
||||
* in the toString lest some people get confused.
|
||||
*/
|
||||
enum class DayCountBasisDay {
|
||||
// We have to prefix 30 etc with a letter due to enum naming constraints.
|
||||
D30,
|
||||
@ -358,13 +387,14 @@ open class BusinessCalendar private constructor(val calendars: Array<out String>
|
||||
}
|
||||
}
|
||||
|
||||
fun dayCountCalculator(startDate: LocalDate, endDate: LocalDate,
|
||||
dcbYear: DayCountBasisYear,
|
||||
dcbDay: DayCountBasisDay): Int {
|
||||
fun calculateDaysBetween(startDate: LocalDate,
|
||||
endDate: LocalDate,
|
||||
dcbYear: DayCountBasisYear,
|
||||
dcbDay: DayCountBasisDay): Int {
|
||||
// Right now we are only considering Actual/360 and 30/360 .. We'll do the rest later.
|
||||
// TODO: The rest.
|
||||
return when {
|
||||
dcbDay == DayCountBasisDay.DActual && dcbYear == DayCountBasisYear.Y360 -> (endDate.toEpochDay() - startDate.toEpochDay()).toInt()
|
||||
dcbDay == DayCountBasisDay.DActual -> (endDate.toEpochDay() - startDate.toEpochDay()).toInt()
|
||||
dcbDay == DayCountBasisDay.D30 && dcbYear == DayCountBasisYear.Y360 -> ((endDate.year - startDate.year) * 360.0 + (endDate.monthValue - startDate.monthValue) * 30.0 + endDate.dayOfMonth - startDate.dayOfMonth).toInt()
|
||||
else -> TODO("Can't calculate days using convention $dcbDay / $dcbYear")
|
||||
}
|
||||
|
@ -2,21 +2,64 @@ package core.math
|
||||
|
||||
import java.util.*
|
||||
|
||||
interface Interpolator {
|
||||
fun interpolate(x: Double): Double
|
||||
}
|
||||
|
||||
interface InterpolatorFactory {
|
||||
fun create(xs: DoubleArray, ys: DoubleArray): Interpolator
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolates values between the given data points using straight lines
|
||||
*/
|
||||
class LinearInterpolator(private val xs: DoubleArray, private val ys: DoubleArray) : Interpolator {
|
||||
init {
|
||||
require(xs.size == ys.size) { "x and y dimensions should match: ${xs.size} != ${ys.size}" }
|
||||
require(xs.size >= 2) { "At least 2 data points are required for linear interpolation, received: ${xs.size}" }
|
||||
}
|
||||
|
||||
companion object Factory : InterpolatorFactory {
|
||||
override fun create(xs: DoubleArray, ys: DoubleArray) = LinearInterpolator(xs, ys)
|
||||
}
|
||||
|
||||
override fun interpolate(x: Double): Double {
|
||||
val x0 = xs.first()
|
||||
if (x0 == x) return x0
|
||||
|
||||
require(x > x0) { "Can't interpolate below $x0" }
|
||||
|
||||
for (i in 1..xs.size - 1) {
|
||||
if (xs[i] == x) return xs[i]
|
||||
else if (xs[i] > x) return interpolateBetween(x, xs[i - 1], xs[i], ys[i - 1], ys[i])
|
||||
}
|
||||
throw IllegalArgumentException("Can't interpolate above ${xs.last()}")
|
||||
}
|
||||
|
||||
private fun interpolateBetween(x: Double, x1: Double, x2: Double, y1: Double, y2: Double): Double {
|
||||
return y1 + (y2 - y1) * (x - x1) / (x2 - x1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolates values between the given data points using a [SplineFunction].
|
||||
*
|
||||
* Implementation uses the Natural Cubic Spline algorithm as described in
|
||||
* R. L. Burden and J. D. Faires (2011), *Numerical Analysis*. 9th ed. Boston, MA: Brooks/Cole, Cengage Learning. p149-150.
|
||||
*/
|
||||
class CubicSplineInterpolator(private val xs: DoubleArray, private val ys: DoubleArray) {
|
||||
class CubicSplineInterpolator(private val xs: DoubleArray, private val ys: DoubleArray) : Interpolator {
|
||||
init {
|
||||
require(xs.size == ys.size) { "x and y dimensions should match: ${xs.size} != ${ys.size}" }
|
||||
require(xs.size >= 3) { "At least 3 data points are required for interpolation, received: ${xs.size}" }
|
||||
require(xs.size >= 3) { "At least 3 data points are required for cubic interpolation, received: ${xs.size}" }
|
||||
}
|
||||
|
||||
companion object Factory : InterpolatorFactory {
|
||||
override fun create(xs: DoubleArray, ys: DoubleArray) = CubicSplineInterpolator(xs, ys)
|
||||
}
|
||||
|
||||
private val splineFunction by lazy { computeSplineFunction() }
|
||||
|
||||
fun interpolate(x: Double): Double {
|
||||
override fun interpolate(x: Double): Double {
|
||||
require(x >= xs.first() && x <= xs.last()) { "Can't interpolate below ${xs.first()} or above ${xs.last()}" }
|
||||
return splineFunction.getValue(x)
|
||||
}
|
||||
|
@ -28,6 +28,19 @@ class FinanceTypesTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `tenor days to maturity adjusted for holiday`() {
|
||||
val tenor = Tenor("1M")
|
||||
val calendar = BusinessCalendar.getInstance("London")
|
||||
val currentDay = LocalDate.of(2016, 2, 27)
|
||||
val maturityDate = currentDay.plusMonths(1).plusDays(2) // 2016-3-27 is a Sunday, next day is a holiday
|
||||
val expectedDaysToMaturity = (maturityDate.toEpochDay() - currentDay.toEpochDay()).toInt()
|
||||
|
||||
val actualDaysToMaturity = tenor.daysToMaturity(currentDay, calendar)
|
||||
|
||||
assertEquals(actualDaysToMaturity, expectedDaysToMaturity)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `schedule generator 1`() {
|
||||
var ret = BusinessCalendar.createGenericSchedule(startDate = LocalDate.of(2014, 11, 25), period = Frequency.Monthly, noOfAdditionalPeriods = 3)
|
||||
|
@ -8,7 +8,40 @@ import kotlin.test.assertFailsWith
|
||||
class InterpolatorsTest {
|
||||
|
||||
@Test
|
||||
fun `throws when key to interpolate is outside the data set`() {
|
||||
fun `linear interpolator throws when key to interpolate is outside the data set`() {
|
||||
val xs = doubleArrayOf(1.0, 2.0, 4.0, 5.0)
|
||||
val interpolator = LinearInterpolator(xs, ys = xs)
|
||||
assertFailsWith<IllegalArgumentException> { interpolator.interpolate(0.0) }
|
||||
assertFailsWith<IllegalArgumentException> { interpolator.interpolate(6.0) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `linear interpolator throws when data set is less than 2 points`() {
|
||||
val xs = doubleArrayOf(1.0)
|
||||
assertFailsWith<IllegalArgumentException> { LinearInterpolator(xs, ys = xs) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `linear interpolator returns existing value when key is in data set`() {
|
||||
val xs = doubleArrayOf(1.0, 2.0, 4.0, 5.0)
|
||||
val interpolatedValue = LinearInterpolator(xs, ys = xs).interpolate(2.0)
|
||||
assertEquals(2.0, interpolatedValue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `linear interpolator interpolates missing values correctly`() {
|
||||
val xs = doubleArrayOf(1.0, 2.0, 3.0, 4.0, 5.0)
|
||||
val ys = xs
|
||||
val toInterpolate = doubleArrayOf(1.5, 2.5, 2.8, 3.3, 3.7, 4.3, 4.7)
|
||||
val expected = toInterpolate
|
||||
|
||||
val interpolator = LinearInterpolator(xs, ys)
|
||||
val actual = toInterpolate.map { interpolator.interpolate(it) }.toDoubleArray()
|
||||
Assert.assertArrayEquals(expected, actual, 0.01)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cubic interpolator throws when key to interpolate is outside the data set`() {
|
||||
val xs = doubleArrayOf(1.0, 2.0, 4.0, 5.0)
|
||||
val interpolator = CubicSplineInterpolator(xs, ys = xs)
|
||||
assertFailsWith<IllegalArgumentException> { interpolator.interpolate(0.0) }
|
||||
@ -16,20 +49,20 @@ class InterpolatorsTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `throws when data set is less than 3 points`() {
|
||||
fun `cubic interpolator throws when data set is less than 3 points`() {
|
||||
val xs = doubleArrayOf(1.0, 2.0)
|
||||
assertFailsWith<IllegalArgumentException> { CubicSplineInterpolator(xs, ys = xs) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns existing value when key is in data set`() {
|
||||
fun `cubic interpolator returns existing value when key is in data set`() {
|
||||
val xs = doubleArrayOf(1.0, 2.0, 4.0, 5.0)
|
||||
val interpolatedValue = CubicSplineInterpolator(xs, ys = xs).interpolate(2.0)
|
||||
assertEquals(2.0, interpolatedValue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `interpolates missing values correctly`() {
|
||||
fun `cubic interpolator interpolates missing values correctly`() {
|
||||
val xs = doubleArrayOf(1.0, 2.0, 3.0, 4.0, 5.0)
|
||||
val ys = doubleArrayOf(2.0, 4.0, 5.0, 11.0, 10.0)
|
||||
val toInterpolate = doubleArrayOf(1.5, 2.5, 2.8, 3.3, 3.7, 4.3, 4.7)
|
||||
|
@ -3,6 +3,9 @@ package core.node.services
|
||||
import core.*
|
||||
import core.crypto.DigitalSignature
|
||||
import core.crypto.signWithECDSA
|
||||
import core.math.CubicSplineInterpolator
|
||||
import core.math.Interpolator
|
||||
import core.math.InterpolatorFactory
|
||||
import core.messaging.send
|
||||
import core.node.AbstractNode
|
||||
import core.node.AcceptsFileUpload
|
||||
@ -26,35 +29,6 @@ import javax.annotation.concurrent.ThreadSafe
|
||||
*/
|
||||
object NodeInterestRates {
|
||||
object Type : ServiceType("corda.interest_rates")
|
||||
/** Parses a string of the form "LIBOR 16-March-2016 1M = 0.678" into a [FixOf] and [Fix] */
|
||||
fun parseOneRate(s: String): Pair<FixOf, Fix> {
|
||||
val (key, value) = s.split('=').map { it.trim() }
|
||||
val of = parseFixOf(key)
|
||||
val rate = BigDecimal(value)
|
||||
return of to Fix(of, rate)
|
||||
}
|
||||
|
||||
/** Parses a string of the form "LIBOR 16-March-2016 1M" into a [FixOf] */
|
||||
fun parseFixOf(key: String): FixOf {
|
||||
val words = key.split(' ')
|
||||
val tenorString = words.last()
|
||||
val date = words.dropLast(1).last()
|
||||
val name = words.dropLast(2).joinToString(" ")
|
||||
return FixOf(name, LocalDate.parse(date), Tenor(tenorString))
|
||||
}
|
||||
|
||||
/** Parses lines containing fixes */
|
||||
fun parseFile(s: String): Map<FixOf, TreeMap<LocalDate, Fix>> {
|
||||
val results = HashMap<FixOf, TreeMap<LocalDate, Fix>>()
|
||||
for (line in s.lines()) {
|
||||
val (fixOf, fix) = parseOneRate(line)
|
||||
val genericKey = FixOf(fixOf.name, LocalDate.MIN, fixOf.ofTenor)
|
||||
val existingMap = results.computeIfAbsent(genericKey, { TreeMap() })
|
||||
existingMap[fixOf.forDay] = fix
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* The Service that wraps [Oracle] and handles messages/network interaction/request scrubbing.
|
||||
*/
|
||||
@ -89,30 +63,21 @@ object NodeInterestRates {
|
||||
override val acceptableFileExtensions = listOf(".rates", ".txt")
|
||||
|
||||
override fun upload(data: InputStream): String {
|
||||
val fixes: Map<FixOf, TreeMap<LocalDate, Fix>> = parseFile(data.
|
||||
bufferedReader().
|
||||
readLines().
|
||||
map { it.trim() }.
|
||||
// Filter out comment and empty lines.
|
||||
filterNot { it.startsWith("#") || it.isBlank() }.
|
||||
joinToString("\n"))
|
||||
|
||||
val fixes = parseFile(data.bufferedReader().readText())
|
||||
// TODO: Save the uploaded fixes to the storage service and reload on construction.
|
||||
|
||||
// This assignment is thread safe because knownFixes is volatile and the oracle code always snapshots
|
||||
// the pointer to the stack before working with the map.
|
||||
oracle.knownFixes = fixes
|
||||
|
||||
val sumOfFixes = fixes.map { it.value.size }.sum()
|
||||
return "Accepted $sumOfFixes new interest rate fixes"
|
||||
return "Accepted $fixes.size new interest rate fixes"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An implementation of an interest rate fix oracle which is given data in a simple string format.
|
||||
*
|
||||
* NOTE the implementation has changed such that it will find the nearest dated entry on OR BEFORE the date
|
||||
* requested so as not to need to populate every possible date in the flat file that (typically) feeds this service
|
||||
* The oracle will try to interpolate the missing value of a tenor for the given fix name and date.
|
||||
*/
|
||||
@ThreadSafe
|
||||
class Oracle(val identity: Party, private val signingKey: KeyPair) {
|
||||
@ -120,40 +85,22 @@ object NodeInterestRates {
|
||||
require(signingKey.public == identity.owningKey)
|
||||
}
|
||||
|
||||
/** The fix data being served by this oracle.
|
||||
*
|
||||
* This is now a map of FixOf (but with date set to LocalDate.MIN, so just index name and tenor)
|
||||
* to a sorted map of LocalDate to Fix, allowing for approximate date finding so that we do not need
|
||||
* to populate the file with a rate for every day.
|
||||
*/
|
||||
@Volatile var knownFixes = emptyMap<FixOf, TreeMap<LocalDate, Fix>>()
|
||||
@Volatile var knownFixes = FixContainer(emptyList<Fix>())
|
||||
set(value) {
|
||||
require(value.isNotEmpty())
|
||||
require(value.size > 0)
|
||||
field = value
|
||||
}
|
||||
|
||||
fun query(queries: List<FixOf>): List<Fix> {
|
||||
require(queries.isNotEmpty())
|
||||
val knownFixes = knownFixes // Snapshot
|
||||
|
||||
val answers: List<Fix?> = queries.map { getKnownFix(knownFixes, it) }
|
||||
val knownFixes = knownFixes // Snapshot
|
||||
val answers: List<Fix?> = queries.map { knownFixes[it] }
|
||||
val firstNull = answers.indexOf(null)
|
||||
if (firstNull != -1)
|
||||
throw UnknownFix(queries[firstNull])
|
||||
return answers.filterNotNull()
|
||||
}
|
||||
|
||||
private fun getKnownFix(knownFixes: Map<FixOf, TreeMap<LocalDate, Fix>>, fixOf: FixOf): Fix? {
|
||||
val rates = knownFixes[FixOf(fixOf.name, LocalDate.MIN, fixOf.ofTenor)]
|
||||
// Greatest key less than or equal to the date we're looking for
|
||||
val floor = rates?.floorEntry(fixOf.forDay)?.value
|
||||
return if (floor != null) {
|
||||
Fix(fixOf, floor.value)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun sign(wtx: WireTransaction): DigitalSignature.LegallyIdentifiable {
|
||||
// Extract the fix commands marked as being signable by us.
|
||||
val fixes: List<Fix> = wtx.commands.
|
||||
@ -165,9 +112,9 @@ object NodeInterestRates {
|
||||
throw IllegalArgumentException()
|
||||
|
||||
// For each fix, verify that the data is correct.
|
||||
val knownFixes = knownFixes // Snapshot
|
||||
val knownFixes = knownFixes // Snapshot
|
||||
for (fix in fixes) {
|
||||
val known = getKnownFix(knownFixes, fix.of)
|
||||
val known = knownFixes[fix.of]
|
||||
if (known == null || known != fix)
|
||||
throw UnknownFix(fix.of)
|
||||
}
|
||||
@ -183,4 +130,106 @@ object NodeInterestRates {
|
||||
class UnknownFix(val fix: FixOf) : Exception() {
|
||||
override fun toString() = "Unknown fix: $fix"
|
||||
}
|
||||
|
||||
/** Fix container, for every fix name & date pair stores a tenor to interest rate map - [InterpolatingRateMap] */
|
||||
class FixContainer(val fixes: List<Fix>, val factory: InterpolatorFactory = CubicSplineInterpolator.Factory) {
|
||||
private val container = buildContainer(fixes)
|
||||
val size = fixes.size
|
||||
|
||||
operator fun get(fixOf: FixOf): Fix? {
|
||||
val rates = container[fixOf.name to fixOf.forDay]
|
||||
val fixValue = rates?.getRate(fixOf.ofTenor) ?: return null
|
||||
return Fix(fixOf, fixValue)
|
||||
}
|
||||
|
||||
private fun buildContainer(fixes: List<Fix>): Map<Pair<String, LocalDate>, InterpolatingRateMap> {
|
||||
val tempContainer = HashMap<Pair<String, LocalDate>, HashMap<Tenor, BigDecimal>>()
|
||||
for (fix in fixes) {
|
||||
val fixOf = fix.of
|
||||
val rates = tempContainer.getOrPut(fixOf.name to fixOf.forDay) { HashMap<Tenor, BigDecimal>() }
|
||||
rates[fixOf.ofTenor] = fix.value
|
||||
}
|
||||
|
||||
// TODO: the calendar data needs to be specified for every fix type in the input string
|
||||
val calendar = BusinessCalendar.getInstance("London", "NewYork")
|
||||
|
||||
return tempContainer.mapValues { InterpolatingRateMap(it.key.second, it.value, calendar, factory) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a mapping between tenors and interest rates.
|
||||
* Interpolates missing values using the provided interpolation mechanism.
|
||||
*/
|
||||
class InterpolatingRateMap(val date: LocalDate,
|
||||
val inputRates: Map<Tenor, BigDecimal>,
|
||||
val calendar: BusinessCalendar,
|
||||
val factory: InterpolatorFactory) {
|
||||
|
||||
/** Snapshot of the input */
|
||||
private val rates = HashMap(inputRates)
|
||||
|
||||
/** Number of rates excluding the interpolated ones */
|
||||
val size = inputRates.size
|
||||
|
||||
private val interpolator: Interpolator? by lazy {
|
||||
// Need to convert tenors to doubles for interpolation
|
||||
val numericMap = rates.mapKeys { daysToMaturity(it.key) }.toSortedMap()
|
||||
val keys = numericMap.keys.map { it.toDouble() }.toDoubleArray()
|
||||
val values = numericMap.values.map { it.toDouble() }.toDoubleArray()
|
||||
|
||||
try {
|
||||
factory.create(keys, values)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
null // Not enough data points for interpolation
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the interest rate for a given [Tenor],
|
||||
* or _null_ if the rate is not found and cannot be interpolated
|
||||
*/
|
||||
fun getRate(tenor: Tenor): BigDecimal? {
|
||||
return rates.getOrElse(tenor) {
|
||||
val rate = interpolate(tenor)
|
||||
if (rate != null) rates.put(tenor, rate)
|
||||
return rate
|
||||
}
|
||||
}
|
||||
|
||||
private fun daysToMaturity(tenor: Tenor) = tenor.daysToMaturity(date, calendar)
|
||||
|
||||
private fun interpolate(tenor: Tenor): BigDecimal? {
|
||||
val key = daysToMaturity(tenor).toDouble()
|
||||
val value = interpolator?.interpolate(key) ?: return null
|
||||
return BigDecimal(value)
|
||||
}
|
||||
}
|
||||
|
||||
/** Parses lines containing fixes */
|
||||
fun parseFile(s: String): FixContainer {
|
||||
val fixes = s.lines().
|
||||
map { it.trim() }.
|
||||
// Filter out comment and empty lines.
|
||||
filterNot { it.startsWith("#") || it.isBlank() }.
|
||||
map { parseFix(it) }
|
||||
return FixContainer(fixes)
|
||||
}
|
||||
|
||||
/** Parses a string of the form "LIBOR 16-March-2016 1M = 0.678" into a [Fix] */
|
||||
fun parseFix(s: String): Fix {
|
||||
val (key, value) = s.split('=').map { it.trim() }
|
||||
val of = parseFixOf(key)
|
||||
val rate = BigDecimal(value)
|
||||
return Fix(of, rate)
|
||||
}
|
||||
|
||||
/** Parses a string of the form "LIBOR 16-March-2016 1M" into a [FixOf] */
|
||||
fun parseFixOf(key: String): FixOf {
|
||||
val words = key.split(' ')
|
||||
val tenorString = words.last()
|
||||
val date = words.dropLast(1).last()
|
||||
val name = words.dropLast(2).joinToString(" ")
|
||||
return FixOf(name, LocalDate.parse(date), Tenor(tenorString))
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ import core.bd
|
||||
import core.testing.MockNetwork
|
||||
import core.testutils.*
|
||||
import core.utilities.BriefLogFormatter
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import protocols.RatesFixProtocol
|
||||
import kotlin.test.assertEquals
|
||||
@ -16,16 +17,18 @@ import kotlin.test.assertFailsWith
|
||||
class NodeInterestRatesTest {
|
||||
val TEST_DATA = NodeInterestRates.parseFile("""
|
||||
LIBOR 2016-03-16 1M = 0.678
|
||||
LIBOR 2016-03-16 2M = 0.655
|
||||
LIBOR 2016-03-16 2M = 0.685
|
||||
LIBOR 2016-03-16 1Y = 0.890
|
||||
LIBOR 2016-03-16 2Y = 0.962
|
||||
EURIBOR 2016-03-15 1M = 0.123
|
||||
EURIBOR 2016-03-15 2M = 0.111
|
||||
""".trimIndent())
|
||||
|
||||
val service = NodeInterestRates.Oracle(MEGA_CORP, MEGA_CORP_KEY).apply { knownFixes = TEST_DATA }
|
||||
val oracle = NodeInterestRates.Oracle(MEGA_CORP, MEGA_CORP_KEY).apply { knownFixes = TEST_DATA }
|
||||
|
||||
@Test fun `query successfully`() {
|
||||
val q = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")
|
||||
val res = service.query(listOf(q))
|
||||
val res = oracle.query(listOf(q))
|
||||
assertEquals(1, res.size)
|
||||
assertEquals("0.678".bd, res[0].value)
|
||||
assertEquals(q, res[0].of)
|
||||
@ -34,36 +37,41 @@ class NodeInterestRatesTest {
|
||||
@Test fun `query with one success and one missing`() {
|
||||
val q1 = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")
|
||||
val q2 = NodeInterestRates.parseFixOf("LIBOR 2016-03-15 1M")
|
||||
val e = assertFailsWith<NodeInterestRates.UnknownFix> { service.query(listOf(q1, q2)) }
|
||||
val e = assertFailsWith<NodeInterestRates.UnknownFix> { oracle.query(listOf(q1, q2)) }
|
||||
assertEquals(e.fix, q2)
|
||||
}
|
||||
|
||||
@Test fun `query successfully with one date beyond`() {
|
||||
val q = NodeInterestRates.parseFixOf("LIBOR 2016-03-19 1M")
|
||||
val res = service.query(listOf(q))
|
||||
@Test fun `query successfully with interpolated rate`() {
|
||||
val q = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 5M")
|
||||
val res = oracle.query(listOf(q))
|
||||
assertEquals(1, res.size)
|
||||
assertEquals("0.678".bd, res[0].value)
|
||||
Assert.assertEquals(0.7316228, res[0].value.toDouble(), 0.0000001)
|
||||
assertEquals(q, res[0].of)
|
||||
}
|
||||
|
||||
@Test fun `rate missing and unable to interpolate`() {
|
||||
val q = NodeInterestRates.parseFixOf("EURIBOR 2016-03-15 3M")
|
||||
assertFailsWith<NodeInterestRates.UnknownFix> { oracle.query(listOf(q)) }
|
||||
}
|
||||
|
||||
@Test fun `empty query`() {
|
||||
assertFailsWith<IllegalArgumentException> { service.query(emptyList()) }
|
||||
assertFailsWith<IllegalArgumentException> { oracle.query(emptyList()) }
|
||||
}
|
||||
|
||||
@Test fun `refuse to sign with no relevant commands`() {
|
||||
val tx = makeTX()
|
||||
assertFailsWith<IllegalArgumentException> { service.sign(tx.toWireTransaction()) }
|
||||
assertFailsWith<IllegalArgumentException> { oracle.sign(tx.toWireTransaction()) }
|
||||
tx.addCommand(Cash.Commands.Move(), ALICE)
|
||||
assertFailsWith<IllegalArgumentException> { service.sign(tx.toWireTransaction()) }
|
||||
assertFailsWith<IllegalArgumentException> { oracle.sign(tx.toWireTransaction()) }
|
||||
}
|
||||
|
||||
@Test fun `sign successfully`() {
|
||||
val tx = makeTX()
|
||||
val fix = service.query(listOf(NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M"))).first()
|
||||
tx.addCommand(fix, service.identity.owningKey)
|
||||
val fix = oracle.query(listOf(NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M"))).first()
|
||||
tx.addCommand(fix, oracle.identity.owningKey)
|
||||
|
||||
// Sign successfully.
|
||||
val signature = service.sign(tx.toWireTransaction())
|
||||
val signature = oracle.sign(tx.toWireTransaction())
|
||||
tx.checkAndAddSignature(signature)
|
||||
}
|
||||
|
||||
@ -71,9 +79,9 @@ class NodeInterestRatesTest {
|
||||
val tx = makeTX()
|
||||
val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")
|
||||
val badFix = Fix(fixOf, "0.6789".bd)
|
||||
tx.addCommand(badFix, service.identity.owningKey)
|
||||
tx.addCommand(badFix, oracle.identity.owningKey)
|
||||
|
||||
val e1 = assertFailsWith<NodeInterestRates.UnknownFix> { service.sign(tx.toWireTransaction()) }
|
||||
val e1 = assertFailsWith<NodeInterestRates.UnknownFix> { oracle.sign(tx.toWireTransaction()) }
|
||||
assertEquals(fixOf, e1.fix)
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user