From 9a8b3bd1efb5ad55c4c7f676b31306d6c32cf26f Mon Sep 17 00:00:00 2001 From: Andrius Dagys Date: Mon, 11 Apr 2016 16:15:48 +0100 Subject: [PATCH] Added interpolation functionality for the rates oracle. The oracle can be set up with different interpolation mechanisms, currently supported are: linear and cubic spline --- contracts/src/main/kotlin/contracts/IRS.kt | 2 +- core/src/main/kotlin/core/FinanceTypes.kt | 72 +++++-- .../main/kotlin/core/math/Interpolators.kt | 49 ++++- core/src/test/kotlin/core/FinanceTypesTest.kt | 13 ++ .../kotlin/core/math/InterpolatorsTest.kt | 41 +++- .../core/node/services/NodeInterestRates.kt | 179 +++++++++++------- .../node/services/NodeInterestRatesTest.kt | 40 ++-- 7 files changed, 286 insertions(+), 110 deletions(-) diff --git a/contracts/src/main/kotlin/contracts/IRS.kt b/contracts/src/main/kotlin/contracts/IRS.kt index 17636a1655..6eea3daeda 100644 --- a/contracts/src/main/kotlin/contracts/IRS.kt +++ b/contracts/src/main/kotlin/contracts/IRS.kt @@ -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) diff --git a/core/src/main/kotlin/core/FinanceTypes.kt b/core/src/main/kotlin/core/FinanceTypes.kt index e3e1b6a208..e10e735c96 100644 --- a/core/src/main/kotlin/core/FinanceTypes.kt +++ b/core/src/main/kotlin/core/FinanceTypes.kt @@ -58,8 +58,7 @@ data class Amount(val pennies: Long, val currency: Currency) : Comparable.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() { } } -/** - * 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 } } -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") } diff --git a/core/src/main/kotlin/core/math/Interpolators.kt b/core/src/main/kotlin/core/math/Interpolators.kt index 14e6b2bd45..30e431e51f 100644 --- a/core/src/main/kotlin/core/math/Interpolators.kt +++ b/core/src/main/kotlin/core/math/Interpolators.kt @@ -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) } diff --git a/core/src/test/kotlin/core/FinanceTypesTest.kt b/core/src/test/kotlin/core/FinanceTypesTest.kt index a79bb65386..418d12eaa9 100644 --- a/core/src/test/kotlin/core/FinanceTypesTest.kt +++ b/core/src/test/kotlin/core/FinanceTypesTest.kt @@ -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) diff --git a/core/src/test/kotlin/core/math/InterpolatorsTest.kt b/core/src/test/kotlin/core/math/InterpolatorsTest.kt index 0e050baf7e..3fc6ac2ef8 100644 --- a/core/src/test/kotlin/core/math/InterpolatorsTest.kt +++ b/core/src/test/kotlin/core/math/InterpolatorsTest.kt @@ -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 { interpolator.interpolate(0.0) } + assertFailsWith { interpolator.interpolate(6.0) } + } + + @Test + fun `linear interpolator throws when data set is less than 2 points`() { + val xs = doubleArrayOf(1.0) + assertFailsWith { 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 { 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 { 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) diff --git a/src/main/kotlin/core/node/services/NodeInterestRates.kt b/src/main/kotlin/core/node/services/NodeInterestRates.kt index cf583d4f0d..e1ed54ad25 100644 --- a/src/main/kotlin/core/node/services/NodeInterestRates.kt +++ b/src/main/kotlin/core/node/services/NodeInterestRates.kt @@ -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 { - 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> { - val results = HashMap>() - 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> = 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>() + @Volatile var knownFixes = FixContainer(emptyList()) set(value) { - require(value.isNotEmpty()) + require(value.size > 0) field = value } fun query(queries: List): List { require(queries.isNotEmpty()) - val knownFixes = knownFixes // Snapshot - - val answers: List = queries.map { getKnownFix(knownFixes, it) } + val knownFixes = knownFixes // Snapshot + val answers: List = 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: 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 = 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, 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): Map, InterpolatingRateMap> { + val tempContainer = HashMap, HashMap>() + for (fix in fixes) { + val fixOf = fix.of + val rates = tempContainer.getOrPut(fixOf.name to fixOf.forDay) { HashMap() } + 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, + 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)) + } } \ No newline at end of file diff --git a/src/test/kotlin/core/node/services/NodeInterestRatesTest.kt b/src/test/kotlin/core/node/services/NodeInterestRatesTest.kt index 9f962aea06..d1ff044b0c 100644 --- a/src/test/kotlin/core/node/services/NodeInterestRatesTest.kt +++ b/src/test/kotlin/core/node/services/NodeInterestRatesTest.kt @@ -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 { service.query(listOf(q1, q2)) } + val e = assertFailsWith { 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 { oracle.query(listOf(q)) } + } + @Test fun `empty query`() { - assertFailsWith { service.query(emptyList()) } + assertFailsWith { oracle.query(emptyList()) } } @Test fun `refuse to sign with no relevant commands`() { val tx = makeTX() - assertFailsWith { service.sign(tx.toWireTransaction()) } + assertFailsWith { oracle.sign(tx.toWireTransaction()) } tx.addCommand(Cash.Commands.Move(), ALICE) - assertFailsWith { service.sign(tx.toWireTransaction()) } + assertFailsWith { 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 { service.sign(tx.toWireTransaction()) } + val e1 = assertFailsWith { oracle.sign(tx.toWireTransaction()) } assertEquals(fixOf, e1.fix) }