Expanded the verify() function for the IRS Contract

This commit is contained in:
Richard Green 2016-05-09 10:45:55 +01:00
parent c6fab1c642
commit 62e7dc583e
9 changed files with 646 additions and 131 deletions

View File

@ -19,15 +19,24 @@ open class UnknownType() {
return (other is UnknownType)
}
override fun hashCode(): Int {
return 1
}
override fun hashCode() = 1
}
/**
* Event superclass - everything happens on a date.
*/
open class Event(val date: LocalDate)
open class Event(val date: LocalDate) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Event) return false
if (date != other.date) return false
return true
}
override fun hashCode() = Objects.hash(date)
}
/**
* Top level PaymentEvent class - represents an obligation to pay an amount on a given date, which may be either in the past or the future.
@ -58,14 +67,30 @@ abstract class RatePaymentEvent(date: LocalDate,
abstract val flow: Amount
val days: Int get() =
calculateDaysBetween(accrualStartDate, accrualEndDate, dayCountBasisYear, dayCountBasisDay)
val days: Int get() = calculateDaysBetween(accrualStartDate, accrualEndDate, dayCountBasisYear, dayCountBasisDay)
val dayCountFactor: BigDecimal get() =
// TODO : Fix below (use daycount convention for division)
(BigDecimal(days).divide(BigDecimal(360.0), 8, RoundingMode.HALF_UP)).setScale(4, RoundingMode.HALF_UP)
// TODO : Fix below (use daycount convention for division, not hardcoded 360 etc)
val dayCountFactor: BigDecimal get() = (BigDecimal(days).divide(BigDecimal(360.0), 8, RoundingMode.HALF_UP)).setScale(4, RoundingMode.HALF_UP)
open fun asCSV(): String = "$accrualStartDate,$accrualEndDate,$dayCountFactor,$days,$date,${notional.currency},${notional},$rate,$flow"
open fun asCSV() = "$accrualStartDate,$accrualEndDate,$dayCountFactor,$days,$date,${notional.currency},${notional},$rate,$flow"
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is RatePaymentEvent) return false
if (accrualStartDate != other.accrualStartDate) return false
if (accrualEndDate != other.accrualEndDate) return false
if (dayCountBasisDay != other.dayCountBasisDay) return false
if (dayCountBasisYear != other.dayCountBasisYear) return false
if (notional != other.notional) return false
if (rate != other.rate) return false
// if (flow != other.flow) return false // Flow is derived
return super.equals(other)
}
override fun hashCode() = super.hashCode() + 31 * Objects.hash(accrualStartDate, accrualEndDate, dayCountBasisDay,
dayCountBasisYear, notional, rate)
}
/**
@ -114,9 +139,7 @@ class FloatingRatePaymentEvent(date: LocalDate,
return Amount(dayCountFactor.times(BigDecimal(notional.pennies)).times(v).toLong(), notional.currency)
}
override fun toString(): String {
return "FloatingPaymentEvent $accrualStartDate -> $accrualEndDate : $dayCountFactor : $days : $date : $notional : $rate (fix on $fixingDate): $flow"
}
override fun toString(): String = "FloatingPaymentEvent $accrualStartDate -> $accrualEndDate : $dayCountFactor : $days : $date : $notional : $rate (fix on $fixingDate): $flow"
override fun asCSV(): String = "$accrualStartDate,$accrualEndDate,$dayCountFactor,$days,$date,${notional.currency},${notional},$fixingDate,$rate,$flow"
@ -126,6 +149,26 @@ class FloatingRatePaymentEvent(date: LocalDate,
fun withNewRate(newRate: Rate): FloatingRatePaymentEvent =
FloatingRatePaymentEvent(date, accrualStartDate, accrualEndDate, dayCountBasisDay,
dayCountBasisYear, fixingDate, notional, newRate)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other?.javaClass != javaClass) return false
other as FloatingRatePaymentEvent
if (fixingDate != other.fixingDate) return false
return super.equals(other)
}
override fun hashCode() = super.hashCode() + 31 * Objects.hash(fixingDate)
// Can't autogenerate as not a data class :-(
fun copy(date: LocalDate = this.date,
accrualStartDate: LocalDate = this.accrualStartDate,
accrualEndDate: LocalDate = this.accrualEndDate,
dayCountBasisDay: DayCountBasisDay = this.dayCountBasisDay,
dayCountBasisYear: DayCountBasisYear = this.dayCountBasisYear,
fixingDate: LocalDate = this.fixingDate,
notional: Amount = this.notional,
rate: Rate = this.rate) = FloatingRatePaymentEvent(date, accrualStartDate, accrualEndDate, dayCountBasisDay, dayCountBasisYear, fixingDate, notional, rate)
}
@ -191,18 +234,13 @@ class InterestRateSwap() : Contract {
/**
* Returns a copy after modifying (applying) the fixing for that date.
*/
fun applyFixing(date: LocalDate, newRate: Rate): Calculation {
fun applyFixing(date: LocalDate, newRate: FixedRate): Calculation {
val paymentEvent = getFixing(date)
val newFloatingLPS = floatingLegPaymentSchedule + (paymentEvent.date to paymentEvent.withNewRate(newRate))
return Calculation(expression = expression,
floatingLegPaymentSchedule = newFloatingLPS,
fixedLegPaymentSchedule = fixedLegPaymentSchedule)
}
fun exportSchedule() {
}
}
abstract class CommonLeg(
@ -212,13 +250,13 @@ class InterestRateSwap() : Contract {
val effectiveDateAdjustment: DateRollConvention?,
val terminationDate: LocalDate,
val terminationDateAdjustment: DateRollConvention?,
var dayCountBasisDay: DayCountBasisDay,
var dayCountBasisYear: DayCountBasisYear,
var dayInMonth: Int,
var paymentRule: PaymentRule,
var paymentDelay: Int,
var paymentCalendar: BusinessCalendar,
var interestPeriodAdjustment: AccrualAdjustment
val dayCountBasisDay: DayCountBasisDay,
val dayCountBasisYear: DayCountBasisYear,
val dayInMonth: Int,
val paymentRule: PaymentRule,
val paymentDelay: Int,
val paymentCalendar: BusinessCalendar,
val interestPeriodAdjustment: AccrualAdjustment
) {
override fun toString(): String {
return "Notional=$notional,PaymentFrequency=$paymentFrequency,EffectiveDate=$effectiveDate,EffectiveDateAdjustment:$effectiveDateAdjustment,TerminatationDate=$terminationDate," +
@ -249,24 +287,9 @@ class InterestRateSwap() : Contract {
return true
}
override fun hashCode(): Int {
var result = notional.hashCode()
result += 31 * result + paymentFrequency.hashCode()
result += 31 * result + effectiveDate.hashCode()
result += 31 * result + (effectiveDateAdjustment?.hashCode() ?: 0)
result += 31 * result + terminationDate.hashCode()
result += 31 * result + (terminationDateAdjustment?.hashCode() ?: 0)
result += 31 * result + dayCountBasisDay.hashCode()
result += 31 * result + dayCountBasisYear.hashCode()
result += 31 * result + dayInMonth
result += 31 * result + paymentRule.hashCode()
result += 31 * result + paymentDelay
result += 31 * result + paymentCalendar.hashCode()
result += 31 * result + interestPeriodAdjustment.hashCode()
return result
}
override fun hashCode() = super.hashCode() + 31 * Objects.hash(notional, paymentFrequency, effectiveDate,
effectiveDateAdjustment, terminationDate, effectiveDateAdjustment, terminationDate, terminationDateAdjustment,
dayCountBasisDay, dayCountBasisYear, dayInMonth, paymentRule, paymentDelay, paymentCalendar, interestPeriodAdjustment)
}
open class FixedLeg(
@ -306,13 +329,27 @@ class InterestRateSwap() : Contract {
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result += 31 * result + fixedRatePayer.hashCode()
result += 31 * result + fixedRate.hashCode()
result += 31 * result + rollConvention.hashCode()
return result
}
override fun hashCode() = super.hashCode() + 31 * Objects.hash(fixedRatePayer, fixedRate, rollConvention)
// Can't autogenerate as not a data class :-(
fun copy(fixedRatePayer: Party = this.fixedRatePayer,
notional: Amount = this.notional,
paymentFrequency: Frequency = this.paymentFrequency,
effectiveDate: LocalDate = this.effectiveDate,
effectiveDateAdjustment: DateRollConvention? = this.effectiveDateAdjustment,
terminationDate: LocalDate = this.terminationDate,
terminationDateAdjustment: DateRollConvention? = this.terminationDateAdjustment,
dayCountBasisDay: DayCountBasisDay = this.dayCountBasisDay,
dayCountBasisYear: DayCountBasisYear = this.dayCountBasisYear,
dayInMonth: Int = this.dayInMonth,
paymentRule: PaymentRule = this.paymentRule,
paymentDelay: Int = this.paymentDelay,
paymentCalendar: BusinessCalendar = this.paymentCalendar,
interestPeriodAdjustment: AccrualAdjustment = this.interestPeriodAdjustment,
fixedRate: FixedRate = this.fixedRate) = FixedLeg(
fixedRatePayer, notional, paymentFrequency, effectiveDate, effectiveDateAdjustment, terminationDate,
terminationDateAdjustment, dayCountBasisDay, dayCountBasisYear, dayInMonth, paymentRule, paymentDelay,
paymentCalendar, interestPeriodAdjustment, fixedRate, rollConvention)
}
@ -370,62 +407,168 @@ class InterestRateSwap() : Contract {
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result += 31 * result + floatingRatePayer.hashCode()
result += 31 * result + rollConvention.hashCode()
result += 31 * result + fixingRollConvention.hashCode()
result += 31 * result + resetDayInMonth
result += 31 * result + fixingPeriod.hashCode()
result += 31 * result + resetRule.hashCode()
result += 31 * result + fixingsPerPayment.hashCode()
result += 31 * result + fixingCalendar.hashCode()
result += 31 * result + index.hashCode()
result += 31 * result + indexSource.hashCode()
result += 31 * result + indexTenor.hashCode()
return result
override fun hashCode() = super.hashCode() + 31 * Objects.hash(floatingRatePayer, rollConvention,
fixingRollConvention, resetDayInMonth, fixingPeriod, resetRule, fixingsPerPayment, fixingCalendar,
index, indexSource, indexTenor)
fun copy(floatingRatePayer: Party = this.floatingRatePayer,
notional: Amount = this.notional,
paymentFrequency: Frequency = this.paymentFrequency,
effectiveDate: LocalDate = this.effectiveDate,
effectiveDateAdjustment: DateRollConvention? = this.effectiveDateAdjustment,
terminationDate: LocalDate = this.terminationDate,
terminationDateAdjustment: DateRollConvention? = this.terminationDateAdjustment,
dayCountBasisDay: DayCountBasisDay = this.dayCountBasisDay,
dayCountBasisYear: DayCountBasisYear = this.dayCountBasisYear,
dayInMonth: Int = this.dayInMonth,
paymentRule: PaymentRule = this.paymentRule,
paymentDelay: Int = this.paymentDelay,
paymentCalendar: BusinessCalendar = this.paymentCalendar,
interestPeriodAdjustment: AccrualAdjustment = this.interestPeriodAdjustment,
rollConvention: DateRollConvention = this.rollConvention,
fixingRollConvention: DateRollConvention = this.fixingRollConvention,
resetDayInMonth: Int = this.resetDayInMonth,
fixingPeriod: DateOffset = this.fixingPeriod,
resetRule: PaymentRule = this.resetRule,
fixingsPerPayment: Frequency = this.fixingsPerPayment,
fixingCalendar: BusinessCalendar = this.fixingCalendar,
index: String = this.index,
indexSource: String = this.indexSource,
indexTenor: Tenor = this.indexTenor
) = FloatingLeg(floatingRatePayer, notional, paymentFrequency, effectiveDate, effectiveDateAdjustment,
terminationDate, terminationDateAdjustment, dayCountBasisDay, dayCountBasisYear, dayInMonth,
paymentRule, paymentDelay, paymentCalendar, interestPeriodAdjustment, rollConvention,
fixingRollConvention, resetDayInMonth, fixingPeriod, resetRule, fixingsPerPayment,
fixingCalendar, index, indexSource, indexTenor)
}
// These functions may make more sense to use for basket types, but for now let's leave them here
fun checkLegDates(legs: Array<CommonLeg>) {
requireThat {
"Effective date is before termination date" by legs.all { it.effectiveDate < it.terminationDate }
"Effective dates are in alignment" by legs.all { it.effectiveDate == legs[0].effectiveDate }
"Termination dates are in alignment" by legs.all { it.terminationDate == legs[0].terminationDate }
}
}
fun checkLegAmounts(legs: Array<CommonLeg>) {
requireThat {
"The notional is non zero" by legs.any { it.notional.pennies > (0).toLong() }
"The notional for all legs must be the same" by legs.all { it.notional == legs[0].notional }
}
for (leg: CommonLeg in legs) {
if (leg is FixedLeg) {
requireThat {
// TODO: Confirm: would someone really enter a swap with a negative fixed rate?
"Fixed leg rate must be positive" by leg.fixedRate.isPositive()
}
}
}
}
// TODO: After business rules discussion, add further checks to the schedules and rates
fun checkSchedules(@Suppress("UNUSED_PARAMETER") legs: Array<CommonLeg>): Boolean = true
fun checkRates(@Suppress("UNUSED_PARAMETER") legs: Array<CommonLeg>): Boolean = true
/**
* verify() with a few examples of what needs to be checked. TODO: Lots more to add.
* Compares two schedules of Floating Leg Payments, returns the difference (i.e. omissions in either leg or changes to the values).
*/
fun getFloatingLegPaymentsDifferences(payments1: Map<LocalDate, Event>, payments2: Map<LocalDate, Event>): List<Pair<LocalDate, Pair<FloatingRatePaymentEvent, FloatingRatePaymentEvent>>> {
val diff1 = payments1.filter { payments1[it.key] != payments2[it.key] }
val diff2 = payments2.filter { payments1[it.key] != payments2[it.key] }
val ret = (diff1.keys + diff2.keys).map { it to Pair(diff1.get(it) as FloatingRatePaymentEvent, diff2.get(it) as FloatingRatePaymentEvent) }
return ret
}
/**
* verify() with some examples of what needs to be checked.
*/
override fun verify(tx: TransactionForVerification) {
// Group by Trade ID for in / out states
val groups = tx.groupStates() { state: InterestRateSwap.State -> state.common.tradeID }
val command = tx.commands.requireSingleCommand<InterestRateSwap.Commands>()
val time = tx.commands.getTimestampByName("Mock Company 0", "Timestamping Service", "Bank A")?.midpoint
if (time == null) throw IllegalArgumentException("must be timestamped")
val irs = tx.outStates.filterIsInstance<InterestRateSwap.State>().single()
when (command.value) {
is Commands.Agree -> {
requireThat {
"There are no in states for an agreement" by tx.inStates.isEmpty()
"The fixed rate is non zero" by (irs.fixedLeg.fixedRate != FixedRate(PercentageRatioUnit("0.0")))
"There are events in the fix schedule" by (irs.calculation.fixedLegPaymentSchedule.size > 0)
"There are events in the float schedule" by (irs.calculation.floatingLegPaymentSchedule.size > 0)
// "There are fixes in the schedule" by (irs.calculation.floatingLegPaymentSchedule!!.size > 0)
// TODO: shortlist of other tests
}
}
is Commands.Fix -> {
requireThat {
// TODO: see previous block
// "There is a fixing supplied" by false // TODO
// "The fixing has been signed by an appropriate oracle" by false // TODO
// "The fixing has arrived at the right time" by false
// "The net payment has been calculated" by false // TODO : Not sure if this is the right place
for ((inputs, outputs, key) in groups) {
when (command.value) {
is Commands.Agree -> {
val irs = outputs.filterIsInstance<InterestRateSwap.State>().single()
requireThat {
"There are no in states for an agreement" by inputs.isEmpty()
"There are events in the fix schedule" by (irs.calculation.fixedLegPaymentSchedule.size > 0)
"There are events in the float schedule" by (irs.calculation.floatingLegPaymentSchedule.size > 0)
"All notionals must be non zero" by ( irs.fixedLeg.notional.pennies > 0 && irs.floatingLeg.notional.pennies > 0)
"The fixed leg rate must be positive" by ( irs.fixedLeg.fixedRate.isPositive() )
"The currency of the notionals must be the same" by (irs.fixedLeg.notional.currency == irs.floatingLeg.notional.currency)
"All leg notionals must be the same" by (irs.fixedLeg.notional == irs.floatingLeg.notional)
"The effective date is before the termination date for the fixed leg" by (irs.fixedLeg.effectiveDate < irs.fixedLeg.terminationDate)
"The effective date is before the termination date for the floating leg" by (irs.floatingLeg.effectiveDate < irs.floatingLeg.terminationDate)
"The effective dates are aligned" by (irs.floatingLeg.effectiveDate == irs.fixedLeg.effectiveDate)
"The termination dates are aligned" by (irs.floatingLeg.terminationDate == irs.fixedLeg.terminationDate)
"The rates are valid" by checkRates(arrayOf(irs.fixedLeg, irs.floatingLeg))
"The schedules are valid" by checkSchedules(arrayOf(irs.fixedLeg, irs.floatingLeg))
// TODO: further tests
}
checkLegAmounts(arrayOf(irs.fixedLeg, irs.floatingLeg))
checkLegDates(arrayOf(irs.fixedLeg, irs.floatingLeg))
}
}
is Commands.Pay -> {
requireThat {
// TODO: see previous block
//"A counterparty must be making a payment" by false // TODO
// "The right counterparty must be receiving the payment" by false // TODO
is Commands.Fix -> {
val irs = outputs.filterIsInstance<InterestRateSwap.State>().single()
val prevIrs = inputs.filterIsInstance<InterestRateSwap.State>().single()
val paymentDifferences = getFloatingLegPaymentsDifferences(prevIrs.calculation.floatingLegPaymentSchedule, irs.calculation.floatingLegPaymentSchedule)
// Having both of these tests are "redundant" as far as verify() goes, however, by performing both
// we can relay more information back to the user in the case of failure.
requireThat {
"There is at least one difference in the IRS floating leg payment schedules" by !paymentDifferences.isEmpty()
"There is only one change in the IRS floating leg payment schedule" by (paymentDifferences.size == 1)
}
val changedRates = paymentDifferences.single().second // Ignore the date of the changed rate (we checked that earlier).
val (oldFloatingRatePaymentEvent, newFixedRatePaymentEvent) = changedRates
val fixCommand = tx.commands.requireSingleCommand<Fix>()
val fixValue = fixCommand.value
// Need to check that everything is the same apart from the new fixed rate entry.
requireThat {
"The fixed leg parties are constant" by ( irs.fixedLeg.fixedRatePayer == prevIrs.fixedLeg.fixedRatePayer) // Although superseded by the below test, this is included for a regression issue
"The fixed leg is constant" by (irs.fixedLeg == prevIrs.fixedLeg)
"The floating leg is constant" by (irs.floatingLeg == prevIrs.floatingLeg)
"The common values are constant" by (irs.common == prevIrs.common)
"The fixed leg payment schedule is constant" by (irs.calculation.fixedLegPaymentSchedule == prevIrs.calculation.fixedLegPaymentSchedule)
"The expression is unchanged" by (irs.calculation.expression == prevIrs.calculation.expression)
"There is only one changed payment in the floating leg" by (paymentDifferences.size == 1)
"There changed payment is a floating payment" by (oldFloatingRatePaymentEvent.rate is ReferenceRate)
"The new payment is a fixed payment" by (newFixedRatePaymentEvent.rate is FixedRate)
"The changed payments dates are aligned" by ( oldFloatingRatePaymentEvent.date == newFixedRatePaymentEvent.date)
"The new payment has the correct rate" by (newFixedRatePaymentEvent.rate.ratioUnit!!.value == fixValue.value)
"The fixing is for the next required date" by (prevIrs.calculation.nextFixingDate() == fixValue.of.forDay)
"The fix payment has the same currency as the notional" by (newFixedRatePaymentEvent.flow.currency == irs.floatingLeg.notional.currency)
// "The fixing is not in the future " by (fixCommand) // The oracle should not have signed this .
}
}
is Commands.Pay -> {
requireThat {
"Payments not supported / verifiable yet" by false
}
}
is Commands.Mature -> {
val irs = inputs.filterIsInstance<InterestRateSwap.State>().single()
requireThat {
"No more fixings to be applied" by (irs.calculation.nextFixingDate() == null)
}
}
else -> throw IllegalArgumentException("Unrecognised verifiable command: ${command.value}")
}
else -> throw IllegalArgumentException("Unrecognised verifiable command: ${command.value}")
}
}
@ -510,6 +653,7 @@ class InterestRateSwap() : Contract {
* Just makes printing it out a bit better for those who don't have 80000 column wide monitors.
*/
fun prettyPrint(): String = toString().replace(",", "\n")
}
/**
@ -580,7 +724,7 @@ class InterestRateSwap() : Contract {
// TODO: Replace with rates oracle
fun generateFix(tx: TransactionBuilder, irs: StateAndRef<State>, fixing: Pair<LocalDate, Rate>) {
tx.addInputState(irs.ref)
tx.addOutputState(irs.state.copy(calculation = irs.state.calculation.applyFixing(fixing.first, fixing.second)))
tx.addOutputState(irs.state.copy(calculation = irs.state.calculation.applyFixing(fixing.first, FixedRate(fixing.second))))
tx.addCommand(Commands.Fix(), listOf(irs.state.floatingLeg.floatingRatePayer.owningKey, irs.state.fixedLeg.fixedRatePayer.owningKey))
}
}

View File

@ -24,17 +24,14 @@ open class RatioUnit(value: BigDecimal) { // TODO: Discuss this type
return true
}
override fun hashCode(): Int {
return value.hashCode()
}
override fun hashCode() = value.hashCode()
}
/**
* A class to reprecent a percentage in an unambiguous way.
*/
open class PercentageRatioUnit(percentageAsString: String) : RatioUnit(BigDecimal(percentageAsString).divide(BigDecimal("100"))) {
override fun toString(): String = value.times(BigDecimal(100)).toString() + "%"
override fun toString() = value.times(BigDecimal(100)).toString() + "%"
}
/**
@ -111,16 +108,23 @@ open class Rate(val ratioUnit: RatioUnit? = null) {
* that have not yet happened. Yet-to-be fixed floating rates need to be equal such that schedules can be tested
* for equality.
*/
override fun hashCode(): Int {
return ratioUnit?.hashCode() ?: 0
}
override fun hashCode() = ratioUnit?.hashCode() ?: 0
}
/**
* A very basic subclass to represent a fixed rate.
*/
class FixedRate(ratioUnit: RatioUnit) : Rate(ratioUnit) {
constructor(otherRate: Rate) : this(ratioUnit = otherRate.ratioUnit!!)
override fun toString(): String = "$ratioUnit"
fun isPositive(): Boolean = ratioUnit!!.value > BigDecimal("0.0")
override fun equals(other: Any?) = other?.javaClass == javaClass && super.equals(other)
override fun hashCode() = super.hashCode()
}
/**

View File

@ -6,9 +6,9 @@
"currency": "USD"
},
"paymentFrequency": "SemiAnnual",
"effectiveDate": "2016-03-16",
"effectiveDate": "2016-03-11",
"effectiveDateAdjustment": null,
"terminationDate": "2026-03-16",
"terminationDate": "2026-03-11",
"terminationDateAdjustment": null,
"fixedRate": {
"ratioUnit": {
@ -31,9 +31,9 @@
"currency": "USD"
},
"paymentFrequency": "Quarterly",
"effectiveDate": "2016-03-12",
"effectiveDate": "2016-03-11",
"effectiveDateAdjustment": null,
"terminationDate": "2026-03-12",
"terminationDate": "2026-03-11",
"terminationDateAdjustment": null,
"dayCountBasisDay": "D30",
"dayCountBasisYear": "Y360",

View File

@ -5,7 +5,8 @@ ICE LIBOR 2016-03-16 2M = 0.655
EURIBOR 2016-03-15 1M = 0.123
EURIBOR 2016-03-15 2M = 0.111
ICE LIBOR 2016-03-06 3M = 0.0063515
# Previous fixings
ICE LIBOR 2016-03-07 3M = 0.0063516
ICE LIBOR 2016-03-07 3M = 0.0063516
ICE LIBOR 2016-03-08 3M = 0.0063517
ICE LIBOR 2016-03-09 3M = 0.0063518

View File

@ -113,6 +113,8 @@ interface KeyManagementService {
fun toPrivate(publicKey: PublicKey) = keys[publicKey] ?: throw IllegalStateException("No private key known for requested public key")
fun toKeyPair(publicKey: PublicKey) = KeyPair(publicKey, toPrivate(publicKey))
/** Generates a new random key and adds it to the exposed map. */
fun freshKey(): KeyPair
}

View File

@ -15,6 +15,7 @@ import core.utilities.ANSIProgressRenderer
import core.utilities.ProgressTracker
import demos.DemoClock
import protocols.TwoPartyDealProtocol
import java.security.KeyPair
import java.time.LocalDate
/**
@ -98,7 +99,13 @@ object UpdateBusinessDayProtocol {
progressTracker.childrenFor[FIXING] = TwoPartyDealProtocol.Primary.tracker()
progressTracker.currentStep = FIXING
val participant = TwoPartyDealProtocol.Floater(party.address, sessionID, serviceHub.networkMapCache.timestampingNodes[0], dealStateAndRef, serviceHub.keyManagementService.freshKey(), sessionID, progressTracker.childrenFor[FIXING]!!)
val myName = serviceHub.storageService.myLegalIdentity.name
val deal: InterestRateSwap.State = dealStateAndRef.state
val myOldParty = deal.parties.single { it.name == myName }
val keyPair = serviceHub.keyManagementService.toKeyPair(myOldParty.owningKey)
val participant = TwoPartyDealProtocol.Floater(party.address, sessionID, serviceHub.networkMapCache.timestampingNodes[0], dealStateAndRef,
keyPair,
sessionID, progressTracker.childrenFor[FIXING]!!)
val result = subProtocol(participant)
return result.tx.outRef(0)
}

View File

@ -367,10 +367,9 @@ object TwoPartyDealProtocol {
val deal: T = dealToFix.state
val myOldParty = deal.parties.single { it.name == myName }
val theirOldParty = deal.parties.single { it.name != myName }
val myNewKey = serviceHub.keyManagementService.freshKey().public
@Suppress("UNCHECKED_CAST")
val newDeal = deal.withPublicKey(myOldParty, myNewKey).withPublicKey(theirOldParty, handshake.publicKey) as T
val newDeal = deal
val oldRef = dealToFix.ref
val ptx = TransactionBuilder()
@ -386,7 +385,7 @@ object TwoPartyDealProtocol {
}
subProtocol(addFixing)
return Pair(ptx, arrayListOf(myNewKey))
return Pair(ptx, arrayListOf(myOldParty.owningKey))
}
}

View File

@ -4,6 +4,7 @@ import core.*
import core.node.services.DummyTimestampingAuthority
import core.testutils.*
import org.junit.Test
import java.math.BigDecimal
import java.time.LocalDate
import java.util.*
@ -15,9 +16,9 @@ fun createDummyIRS(irsSelect: Int): InterestRateSwap.State {
fixedRatePayer = MEGA_CORP,
notional = 15900000.DOLLARS,
paymentFrequency = Frequency.SemiAnnual,
effectiveDate = LocalDate.of(2016, 3, 16),
effectiveDate = LocalDate.of(2016, 3, 10),
effectiveDateAdjustment = null,
terminationDate = LocalDate.of(2026, 3, 16),
terminationDate = LocalDate.of(2026, 3, 10),
terminationDateAdjustment = null,
fixedRate = FixedRate(PercentageRatioUnit("1.677")),
dayCountBasisDay = DayCountBasisDay.D30,
@ -98,7 +99,6 @@ fun createDummyIRS(irsSelect: Int): InterestRateSwap.State {
InterestRateSwap.State(fixedLeg = fixedLeg, floatingLeg = floatingLeg, calculation = calculation, common = common)
}
2 -> {
// 10y swap, we pay 1.3% fixed 30/360 semi, rec 3m usd libor act/360 Q on 25m notional (mod foll/adj on both sides)
// I did a mock up start date 10/03/2015 10/03/2025 so you have 5 cashflows on float side that have been preset the rest are unknown
@ -181,7 +181,7 @@ fun createDummyIRS(irsSelect: Int): InterestRateSwap.State {
addressForTransfers = "",
exposure = UnknownType(),
localBusinessDay = BusinessCalendar.getInstance("London"),
tradeID = "trade1",
tradeID = "trade2",
hashLegalDocs = "put hash here",
dailyInterestAmount = Expression("(CashAmount * InterestRate ) / (fixedLeg.notional.currency.currencyCode.equals('GBP')) ? 365 : 360")
)
@ -197,10 +197,25 @@ class IRSTests {
val attachments = MockStorageService().attachments
val exampleIRS = createDummyIRS(1)
val inState = InterestRateSwap.State(
exampleIRS.fixedLeg,
exampleIRS.floatingLeg,
exampleIRS.calculation,
exampleIRS.common
)
val outState = inState.copy()
@Test
fun ok() {
val t = trade()
t.verify()
trade().verify()
}
@Test
fun `ok with groups`() {
tradegroups().verify()
}
/**
@ -243,7 +258,7 @@ class IRSTests {
}
/**
* Test the generate
* Test the generate. No explicit exception as if something goes wrong, we'll find out anyway.
*/
@Test
fun generateIRS() {
@ -251,6 +266,9 @@ class IRSTests {
generateIRSTxn(1)
}
/**
* Testing a simple IRS, add a few fixings and then display as CSV
*/
@Test
fun `IRS Export test`() {
// No transactions etc required - we're just checking simple maths and export functionallity
@ -351,7 +369,14 @@ class IRSTests {
}
/**
* Generates a typical transactional history for an IRS.
*/
fun trade(): TransactionGroupDSL<InterestRateSwap.State> {
val ld = LocalDate.of(2016, 3, 8)
val bd = BigDecimal("0.0063518")
val txgroup: TransactionGroupDSL<InterestRateSwap.State> = transactionGroupFor() {
transaction("Agreement") {
output("irs post agreement") { singleIRS() }
@ -361,18 +386,349 @@ class IRSTests {
transaction("Fix") {
input("irs post agreement")
output("irs post first fixing") { "irs post agreement".output }
arg(ORACLE_PUBKEY) { InterestRateSwap.Commands.Fix() }
timestamp(TEST_TX_TIME)
}
transaction("Pay") {
input("irs post first fixing")
output("irs post first payment") { "irs post first fixing".output }
arg(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY) { InterestRateSwap.Commands.Pay() }
output("irs post first fixing") {
"irs post agreement".output.copy(
"irs post agreement".output.fixedLeg,
"irs post agreement".output.floatingLeg,
"irs post agreement".output.calculation.applyFixing(ld, FixedRate(RatioUnit(bd))),
"irs post agreement".output.common
)
}
arg(ORACLE_PUBKEY) {
InterestRateSwap.Commands.Fix()
}
arg(ORACLE_PUBKEY) {
Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)
}
timestamp(TEST_TX_TIME)
}
}
return txgroup
}
}
@Test
fun `ensure failure occurs when there are inbound states for an agreement command`() {
transaction {
input() { singleIRS() }
output("irs post agreement") { singleIRS() }
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
this `fails requirement` "There are no in states for an agreement"
}
}
@Test
fun `ensure failure occurs when no events in fix schedule`() {
val irs = singleIRS()
val emptySchedule = HashMap<LocalDate, FixedRatePaymentEvent>()
transaction {
output() {
irs.copy(calculation = irs.calculation.copy(fixedLegPaymentSchedule = emptySchedule))
}
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
this `fails requirement` "There are events in the fix schedule"
}
}
@Test
fun `ensure failure occurs when no events in floating schedule`() {
val irs = singleIRS()
val emptySchedule = HashMap<LocalDate, FloatingRatePaymentEvent>()
transaction {
output() {
irs.copy(calculation = irs.calculation.copy(floatingLegPaymentSchedule = emptySchedule))
}
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
this `fails requirement` "There are events in the float schedule"
}
}
@Test
fun `ensure notionals are non zero`() {
val irs = singleIRS()
transaction {
output() {
irs.copy(irs.fixedLeg.copy(notional = irs.fixedLeg.notional.copy(pennies = 0)))
}
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
this `fails requirement` "All notionals must be non zero"
}
transaction {
output() {
irs.copy(irs.fixedLeg.copy(notional = irs.floatingLeg.notional.copy(pennies = 0)))
}
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
this `fails requirement` "All notionals must be non zero"
}
}
@Test
fun `ensure positive rate on fixed leg`() {
val irs = singleIRS()
val modifiedIRS = irs.copy(fixedLeg = irs.fixedLeg.copy(fixedRate = FixedRate(PercentageRatioUnit("-0.1"))))
transaction {
output() {
modifiedIRS
}
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
this `fails requirement` "The fixed leg rate must be positive"
}
}
/**
* This will be modified once we adapt the IRS to be cross currency
*/
@Test
fun `ensure same currency notionals`() {
val irs = singleIRS()
val modifiedIRS = irs.copy(fixedLeg = irs.fixedLeg.copy(notional = Amount(irs.fixedLeg.notional.pennies, Currency.getInstance("JPY"))))
transaction {
output() {
modifiedIRS
}
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
this `fails requirement` "The currency of the notionals must be the same"
}
}
@Test
fun `ensure notional amounts are equal`() {
val irs = singleIRS()
val modifiedIRS = irs.copy(fixedLeg = irs.fixedLeg.copy(notional = Amount(irs.floatingLeg.notional.pennies + 1, irs.floatingLeg.notional.currency)))
transaction {
output() {
modifiedIRS
}
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
this `fails requirement` "All leg notionals must be the same"
}
}
@Test
fun `ensure trade date and termination date checks are done pt1`() {
val irs = singleIRS()
val modifiedIRS1 = irs.copy(fixedLeg = irs.fixedLeg.copy(terminationDate = irs.fixedLeg.effectiveDate.minusDays(1)))
transaction {
output() {
modifiedIRS1
}
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
this `fails requirement` "The effective date is before the termination date for the fixed leg"
}
val modifiedIRS2 = irs.copy(floatingLeg = irs.floatingLeg.copy(terminationDate = irs.floatingLeg.effectiveDate.minusDays(1)))
transaction {
output() {
modifiedIRS2
}
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
this `fails requirement` "The effective date is before the termination date for the floating leg"
}
}
@Test
fun `ensure trade date and termination date checks are done pt2`() {
val irs = singleIRS()
val modifiedIRS3 = irs.copy(floatingLeg = irs.floatingLeg.copy(terminationDate = irs.fixedLeg.terminationDate.minusDays(1)))
transaction {
output() {
modifiedIRS3
}
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
this `fails requirement` "The termination dates are aligned"
}
val modifiedIRS4 = irs.copy(floatingLeg = irs.floatingLeg.copy(effectiveDate = irs.fixedLeg.effectiveDate.minusDays(1)))
transaction {
output() {
modifiedIRS4
}
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
this `fails requirement` "The effective dates are aligned"
}
}
@Test
fun `various fixing tests`() {
val ld = LocalDate.of(2016, 3, 8)
val bd = BigDecimal("0.0063518")
transaction {
output("irs post agreement") { singleIRS() }
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
}
val oldIRS = singleIRS(1)
val newIRS = oldIRS.copy(oldIRS.fixedLeg,
oldIRS.floatingLeg,
oldIRS.calculation.applyFixing(ld, FixedRate(RatioUnit(bd))),
oldIRS.common)
transaction {
input() {
oldIRS
}
// Templated tweak for reference. A corrent fixing applied should be ok
tweak {
arg(ORACLE_PUBKEY) {
InterestRateSwap.Commands.Fix()
}
timestamp(TEST_TX_TIME)
arg(ORACLE_PUBKEY) {
Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)
}
output() { newIRS }
this.accepts()
}
// This test makes sure that verify confirms the fixing was applied and there is a difference in the old and new
tweak {
arg(ORACLE_PUBKEY) { InterestRateSwap.Commands.Fix() }
timestamp(TEST_TX_TIME)
arg(ORACLE_PUBKEY) { Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd) }
output() { oldIRS }
this`fails requirement` "There is at least one difference in the IRS floating leg payment schedules"
}
// This tests tries to sneak in a change to another fixing (which may or may not be the latest one)
tweak {
arg(ORACLE_PUBKEY) { InterestRateSwap.Commands.Fix() }
timestamp(TEST_TX_TIME)
arg(ORACLE_PUBKEY) {
Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)
}
val firstResetKey = newIRS.calculation.floatingLegPaymentSchedule.keys.first()
val firstResetValue = newIRS.calculation.floatingLegPaymentSchedule[firstResetKey]
var modifiedFirstResetValue = firstResetValue!!.copy(notional = Amount(firstResetValue.notional.pennies, Currency.getInstance("JPY")))
output() {
newIRS.copy(
newIRS.fixedLeg,
newIRS.floatingLeg,
newIRS.calculation.copy(floatingLegPaymentSchedule = newIRS.calculation.floatingLegPaymentSchedule.plus(
Pair(firstResetKey, modifiedFirstResetValue))),
newIRS.common
)
}
this`fails requirement` "There is only one change in the IRS floating leg payment schedule"
}
// This tests modifies the payment currency for the fixing
tweak {
arg(ORACLE_PUBKEY) { InterestRateSwap.Commands.Fix() }
timestamp(TEST_TX_TIME)
arg(ORACLE_PUBKEY) { Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd) }
val latestReset = newIRS.calculation.floatingLegPaymentSchedule.filter { it.value.rate is FixedRate }.maxBy { it.key }
var modifiedLatestResetValue = latestReset!!.value.copy(notional = Amount(latestReset.value.notional.pennies, Currency.getInstance("JPY")))
output() {
newIRS.copy(
newIRS.fixedLeg,
newIRS.floatingLeg,
newIRS.calculation.copy(floatingLegPaymentSchedule = newIRS.calculation.floatingLegPaymentSchedule.plus(
Pair(latestReset.key, modifiedLatestResetValue))),
newIRS.common
)
}
this`fails requirement` "The fix payment has the same currency as the notional"
}
}
}
/**
* This returns an example of transactions that are grouped by TradeId and then a fixing applied.
* It's important to make the tradeID different for two reasons, the hashes will be the same and all sorts of confusion will
* result and the grouping won't work either.
* In reality, the only fields that should be in common will be the next fixing date and the reference rate.
*/
fun tradegroups(): TransactionGroupDSL<InterestRateSwap.State> {
val ld1 = LocalDate.of(2016, 3, 8)
val bd1 = BigDecimal("0.0063518")
val irs = singleIRS()
val txgroup: TransactionGroupDSL<InterestRateSwap.State> = transactionGroupFor() {
transaction("Agreement") {
output("irs post agreement1") {
irs.copy(
irs.fixedLeg,
irs.floatingLeg,
irs.calculation,
irs.common.copy(tradeID = "t1")
)
}
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
}
transaction("Agreement") {
output("irs post agreement2") {
irs.copy(
irs.fixedLeg,
irs.floatingLeg,
irs.calculation,
irs.common.copy(tradeID = "t2")
)
}
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME)
}
transaction("Fix") {
input("irs post agreement1")
input("irs post agreement2")
output("irs post first fixing1") {
"irs post agreement1".output.copy(
"irs post agreement1".output.fixedLeg,
"irs post agreement1".output.floatingLeg,
"irs post agreement1".output.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))),
"irs post agreement1".output.common.copy(tradeID = "t1")
)
}
output("irs post first fixing2") {
"irs post agreement2".output.copy(
"irs post agreement2".output.fixedLeg,
"irs post agreement2".output.floatingLeg,
"irs post agreement2".output.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))),
"irs post agreement2".output.common.copy(tradeID = "t2")
)
}
arg(ORACLE_PUBKEY) {
InterestRateSwap.Commands.Fix()
}
arg(ORACLE_PUBKEY) {
Fix(FixOf("ICE LIBOR", ld1, Tenor("3M")), bd1)
}
timestamp(TEST_TX_TIME)
}
}
return txgroup
}
}

View File

@ -157,16 +157,16 @@ open class TransactionForTest : AbstractTransactionForTest() {
private val inStates = arrayListOf<ContractState>()
fun input(s: () -> ContractState) = inStates.add(s())
protected fun run(time: Instant) {
protected fun runCommandsAndVerify(time: Instant) {
val cmds = commandsToAuthenticatedObjects()
val tx = TransactionForVerification(inStates, outStates.map { it.state }, emptyList(), cmds, SecureHash.randomSHA256())
tx.verify()
}
fun accepts(time: Instant = TEST_TX_TIME) = run(time)
fun accepts(time: Instant = TEST_TX_TIME) = runCommandsAndVerify(time)
fun rejects(withMessage: String? = null, time: Instant = TEST_TX_TIME) {
val r = try {
run(time)
runCommandsAndVerify(time)
false
} catch (e: Exception) {
val m = e.message
@ -179,7 +179,9 @@ open class TransactionForTest : AbstractTransactionForTest() {
if (!r) throw AssertionError("Expected exception but didn't get one")
}
// which is uglier?? :)
/**
* Used to confirm that the test, when (implicitly) run against the .verify() method, fails with the text of the message
*/
infix fun `fails requirement`(msg: String) = rejects(msg)
fun fails_requirement(msg: String) = this.`fails requirement`(msg)