Merged in interest-rate-interpolation (pull request #58)

Rates oracle - missing value interpolation
This commit is contained in:
Andrius Dagys 2016-04-13 12:35:17 +01:00
commit 27a244e89c
7 changed files with 286 additions and 110 deletions

View File

@ -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)

View File

@ -59,7 +59,6 @@ data class Amount(val pennies: Long, val currency: Currency) : Comparable<Amount
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
*/
@ -175,7 +203,8 @@ 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. */
* 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,
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")
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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)

View File

@ -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 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.
@ -167,7 +114,7 @@ object NodeInterestRates {
// For each fix, verify that the data is correct.
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))
}
}

View File

@ -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)
}