mirror of
https://github.com/corda/corda.git
synced 2024-12-21 22:07:55 +00:00
Expanded the verify() function for the IRS Contract
This commit is contained in:
parent
c6fab1c642
commit
62e7dc583e
@ -19,15 +19,24 @@ open class UnknownType() {
|
|||||||
return (other is UnknownType)
|
return (other is UnknownType)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode() = 1
|
||||||
return 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event superclass - everything happens on a date.
|
* 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.
|
* 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
|
abstract val flow: Amount
|
||||||
|
|
||||||
val days: Int get() =
|
val days: Int get() = calculateDaysBetween(accrualStartDate, accrualEndDate, dayCountBasisYear, dayCountBasisDay)
|
||||||
calculateDaysBetween(accrualStartDate, accrualEndDate, dayCountBasisYear, dayCountBasisDay)
|
|
||||||
|
|
||||||
val dayCountFactor: BigDecimal get() =
|
// TODO : Fix below (use daycount convention for division, not hardcoded 360 etc)
|
||||||
// TODO : Fix below (use daycount convention for division)
|
val dayCountFactor: BigDecimal get() = (BigDecimal(days).divide(BigDecimal(360.0), 8, RoundingMode.HALF_UP)).setScale(4, RoundingMode.HALF_UP)
|
||||||
(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)
|
return Amount(dayCountFactor.times(BigDecimal(notional.pennies)).times(v).toLong(), notional.currency)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String = "FloatingPaymentEvent $accrualStartDate -> $accrualEndDate : $dayCountFactor : $days : $date : $notional : $rate (fix on $fixingDate): $flow"
|
||||||
return "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"
|
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 =
|
fun withNewRate(newRate: Rate): FloatingRatePaymentEvent =
|
||||||
FloatingRatePaymentEvent(date, accrualStartDate, accrualEndDate, dayCountBasisDay,
|
FloatingRatePaymentEvent(date, accrualStartDate, accrualEndDate, dayCountBasisDay,
|
||||||
dayCountBasisYear, fixingDate, notional, newRate)
|
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.
|
* 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 paymentEvent = getFixing(date)
|
||||||
val newFloatingLPS = floatingLegPaymentSchedule + (paymentEvent.date to paymentEvent.withNewRate(newRate))
|
val newFloatingLPS = floatingLegPaymentSchedule + (paymentEvent.date to paymentEvent.withNewRate(newRate))
|
||||||
return Calculation(expression = expression,
|
return Calculation(expression = expression,
|
||||||
floatingLegPaymentSchedule = newFloatingLPS,
|
floatingLegPaymentSchedule = newFloatingLPS,
|
||||||
fixedLegPaymentSchedule = fixedLegPaymentSchedule)
|
fixedLegPaymentSchedule = fixedLegPaymentSchedule)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun exportSchedule() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class CommonLeg(
|
abstract class CommonLeg(
|
||||||
@ -212,13 +250,13 @@ class InterestRateSwap() : Contract {
|
|||||||
val effectiveDateAdjustment: DateRollConvention?,
|
val effectiveDateAdjustment: DateRollConvention?,
|
||||||
val terminationDate: LocalDate,
|
val terminationDate: LocalDate,
|
||||||
val terminationDateAdjustment: DateRollConvention?,
|
val terminationDateAdjustment: DateRollConvention?,
|
||||||
var dayCountBasisDay: DayCountBasisDay,
|
val dayCountBasisDay: DayCountBasisDay,
|
||||||
var dayCountBasisYear: DayCountBasisYear,
|
val dayCountBasisYear: DayCountBasisYear,
|
||||||
var dayInMonth: Int,
|
val dayInMonth: Int,
|
||||||
var paymentRule: PaymentRule,
|
val paymentRule: PaymentRule,
|
||||||
var paymentDelay: Int,
|
val paymentDelay: Int,
|
||||||
var paymentCalendar: BusinessCalendar,
|
val paymentCalendar: BusinessCalendar,
|
||||||
var interestPeriodAdjustment: AccrualAdjustment
|
val interestPeriodAdjustment: AccrualAdjustment
|
||||||
) {
|
) {
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return "Notional=$notional,PaymentFrequency=$paymentFrequency,EffectiveDate=$effectiveDate,EffectiveDateAdjustment:$effectiveDateAdjustment,TerminatationDate=$terminationDate," +
|
return "Notional=$notional,PaymentFrequency=$paymentFrequency,EffectiveDate=$effectiveDate,EffectiveDateAdjustment:$effectiveDateAdjustment,TerminatationDate=$terminationDate," +
|
||||||
@ -249,24 +287,9 @@ class InterestRateSwap() : Contract {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode() = super.hashCode() + 31 * Objects.hash(notional, paymentFrequency, effectiveDate,
|
||||||
var result = notional.hashCode()
|
effectiveDateAdjustment, terminationDate, effectiveDateAdjustment, terminationDate, terminationDateAdjustment,
|
||||||
result += 31 * result + paymentFrequency.hashCode()
|
dayCountBasisDay, dayCountBasisYear, dayInMonth, paymentRule, paymentDelay, paymentCalendar, interestPeriodAdjustment)
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open class FixedLeg(
|
open class FixedLeg(
|
||||||
@ -306,13 +329,27 @@ class InterestRateSwap() : Contract {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode() = super.hashCode() + 31 * Objects.hash(fixedRatePayer, fixedRate, rollConvention)
|
||||||
var result = super.hashCode()
|
|
||||||
result += 31 * result + fixedRatePayer.hashCode()
|
// Can't autogenerate as not a data class :-(
|
||||||
result += 31 * result + fixedRate.hashCode()
|
fun copy(fixedRatePayer: Party = this.fixedRatePayer,
|
||||||
result += 31 * result + rollConvention.hashCode()
|
notional: Amount = this.notional,
|
||||||
return result
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode() = super.hashCode() + 31 * Objects.hash(floatingRatePayer, rollConvention,
|
||||||
var result = super.hashCode()
|
fixingRollConvention, resetDayInMonth, fixingPeriod, resetRule, fixingsPerPayment, fixingCalendar,
|
||||||
result += 31 * result + floatingRatePayer.hashCode()
|
index, indexSource, indexTenor)
|
||||||
result += 31 * result + rollConvention.hashCode()
|
|
||||||
result += 31 * result + fixingRollConvention.hashCode()
|
|
||||||
result += 31 * result + resetDayInMonth
|
fun copy(floatingRatePayer: Party = this.floatingRatePayer,
|
||||||
result += 31 * result + fixingPeriod.hashCode()
|
notional: Amount = this.notional,
|
||||||
result += 31 * result + resetRule.hashCode()
|
paymentFrequency: Frequency = this.paymentFrequency,
|
||||||
result += 31 * result + fixingsPerPayment.hashCode()
|
effectiveDate: LocalDate = this.effectiveDate,
|
||||||
result += 31 * result + fixingCalendar.hashCode()
|
effectiveDateAdjustment: DateRollConvention? = this.effectiveDateAdjustment,
|
||||||
result += 31 * result + index.hashCode()
|
terminationDate: LocalDate = this.terminationDate,
|
||||||
result += 31 * result + indexSource.hashCode()
|
terminationDateAdjustment: DateRollConvention? = this.terminationDateAdjustment,
|
||||||
result += 31 * result + indexTenor.hashCode()
|
dayCountBasisDay: DayCountBasisDay = this.dayCountBasisDay,
|
||||||
return result
|
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) {
|
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 command = tx.commands.requireSingleCommand<InterestRateSwap.Commands>()
|
||||||
val time = tx.commands.getTimestampByName("Mock Company 0", "Timestamping Service", "Bank A")?.midpoint
|
val time = tx.commands.getTimestampByName("Mock Company 0", "Timestamping Service", "Bank A")?.midpoint
|
||||||
if (time == null) throw IllegalArgumentException("must be timestamped")
|
if (time == null) throw IllegalArgumentException("must be timestamped")
|
||||||
|
|
||||||
val irs = tx.outStates.filterIsInstance<InterestRateSwap.State>().single()
|
for ((inputs, outputs, key) in groups) {
|
||||||
when (command.value) {
|
when (command.value) {
|
||||||
is Commands.Agree -> {
|
is Commands.Agree -> {
|
||||||
requireThat {
|
val irs = outputs.filterIsInstance<InterestRateSwap.State>().single()
|
||||||
"There are no in states for an agreement" by tx.inStates.isEmpty()
|
requireThat {
|
||||||
"The fixed rate is non zero" by (irs.fixedLeg.fixedRate != FixedRate(PercentageRatioUnit("0.0")))
|
"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 fix schedule" by (irs.calculation.fixedLegPaymentSchedule.size > 0)
|
||||||
"There are events in the float schedule" by (irs.calculation.floatingLegPaymentSchedule.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)
|
"All notionals must be non zero" by ( irs.fixedLeg.notional.pennies > 0 && irs.floatingLeg.notional.pennies > 0)
|
||||||
// TODO: shortlist of other tests
|
"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)
|
||||||
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
|
|
||||||
|
|
||||||
|
"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.Fix -> {
|
||||||
is Commands.Pay -> {
|
val irs = outputs.filterIsInstance<InterestRateSwap.State>().single()
|
||||||
requireThat {
|
val prevIrs = inputs.filterIsInstance<InterestRateSwap.State>().single()
|
||||||
// TODO: see previous block
|
val paymentDifferences = getFloatingLegPaymentsDifferences(prevIrs.calculation.floatingLegPaymentSchedule, irs.calculation.floatingLegPaymentSchedule)
|
||||||
//"A counterparty must be making a payment" by false // TODO
|
|
||||||
// "The right counterparty must be receiving the payment" by false // TODO
|
// 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.
|
* Just makes printing it out a bit better for those who don't have 80000 column wide monitors.
|
||||||
*/
|
*/
|
||||||
fun prettyPrint(): String = toString().replace(",", "\n")
|
fun prettyPrint(): String = toString().replace(",", "\n")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -580,7 +724,7 @@ class InterestRateSwap() : Contract {
|
|||||||
// TODO: Replace with rates oracle
|
// TODO: Replace with rates oracle
|
||||||
fun generateFix(tx: TransactionBuilder, irs: StateAndRef<State>, fixing: Pair<LocalDate, Rate>) {
|
fun generateFix(tx: TransactionBuilder, irs: StateAndRef<State>, fixing: Pair<LocalDate, Rate>) {
|
||||||
tx.addInputState(irs.ref)
|
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))
|
tx.addCommand(Commands.Fix(), listOf(irs.state.floatingLeg.floatingRatePayer.owningKey, irs.state.fixedLeg.fixedRatePayer.owningKey))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,17 +24,14 @@ open class RatioUnit(value: BigDecimal) { // TODO: Discuss this type
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode() = value.hashCode()
|
||||||
return value.hashCode()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class to reprecent a percentage in an unambiguous way.
|
* A class to reprecent a percentage in an unambiguous way.
|
||||||
*/
|
*/
|
||||||
open class PercentageRatioUnit(percentageAsString: String) : RatioUnit(BigDecimal(percentageAsString).divide(BigDecimal("100"))) {
|
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
|
* that have not yet happened. Yet-to-be fixed floating rates need to be equal such that schedules can be tested
|
||||||
* for equality.
|
* for equality.
|
||||||
*/
|
*/
|
||||||
override fun hashCode(): Int {
|
override fun hashCode() = ratioUnit?.hashCode() ?: 0
|
||||||
return ratioUnit?.hashCode() ?: 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A very basic subclass to represent a fixed rate.
|
* A very basic subclass to represent a fixed rate.
|
||||||
*/
|
*/
|
||||||
class FixedRate(ratioUnit: RatioUnit) : Rate(ratioUnit) {
|
class FixedRate(ratioUnit: RatioUnit) : Rate(ratioUnit) {
|
||||||
|
|
||||||
|
constructor(otherRate: Rate) : this(ratioUnit = otherRate.ratioUnit!!)
|
||||||
|
|
||||||
override fun toString(): String = "$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()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -6,9 +6,9 @@
|
|||||||
"currency": "USD"
|
"currency": "USD"
|
||||||
},
|
},
|
||||||
"paymentFrequency": "SemiAnnual",
|
"paymentFrequency": "SemiAnnual",
|
||||||
"effectiveDate": "2016-03-16",
|
"effectiveDate": "2016-03-11",
|
||||||
"effectiveDateAdjustment": null,
|
"effectiveDateAdjustment": null,
|
||||||
"terminationDate": "2026-03-16",
|
"terminationDate": "2026-03-11",
|
||||||
"terminationDateAdjustment": null,
|
"terminationDateAdjustment": null,
|
||||||
"fixedRate": {
|
"fixedRate": {
|
||||||
"ratioUnit": {
|
"ratioUnit": {
|
||||||
@ -31,9 +31,9 @@
|
|||||||
"currency": "USD"
|
"currency": "USD"
|
||||||
},
|
},
|
||||||
"paymentFrequency": "Quarterly",
|
"paymentFrequency": "Quarterly",
|
||||||
"effectiveDate": "2016-03-12",
|
"effectiveDate": "2016-03-11",
|
||||||
"effectiveDateAdjustment": null,
|
"effectiveDateAdjustment": null,
|
||||||
"terminationDate": "2026-03-12",
|
"terminationDate": "2026-03-11",
|
||||||
"terminationDateAdjustment": null,
|
"terminationDateAdjustment": null,
|
||||||
"dayCountBasisDay": "D30",
|
"dayCountBasisDay": "D30",
|
||||||
"dayCountBasisYear": "Y360",
|
"dayCountBasisYear": "Y360",
|
||||||
|
@ -5,7 +5,8 @@ ICE LIBOR 2016-03-16 2M = 0.655
|
|||||||
EURIBOR 2016-03-15 1M = 0.123
|
EURIBOR 2016-03-15 1M = 0.123
|
||||||
EURIBOR 2016-03-15 2M = 0.111
|
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-07 3M = 0.0063516
|
||||||
ICE LIBOR 2016-03-08 3M = 0.0063517
|
ICE LIBOR 2016-03-08 3M = 0.0063517
|
||||||
ICE LIBOR 2016-03-09 3M = 0.0063518
|
ICE LIBOR 2016-03-09 3M = 0.0063518
|
||||||
|
@ -113,6 +113,8 @@ interface KeyManagementService {
|
|||||||
|
|
||||||
fun toPrivate(publicKey: PublicKey) = keys[publicKey] ?: throw IllegalStateException("No private key known for requested public key")
|
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. */
|
/** Generates a new random key and adds it to the exposed map. */
|
||||||
fun freshKey(): KeyPair
|
fun freshKey(): KeyPair
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ import core.utilities.ANSIProgressRenderer
|
|||||||
import core.utilities.ProgressTracker
|
import core.utilities.ProgressTracker
|
||||||
import demos.DemoClock
|
import demos.DemoClock
|
||||||
import protocols.TwoPartyDealProtocol
|
import protocols.TwoPartyDealProtocol
|
||||||
|
import java.security.KeyPair
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -98,7 +99,13 @@ object UpdateBusinessDayProtocol {
|
|||||||
progressTracker.childrenFor[FIXING] = TwoPartyDealProtocol.Primary.tracker()
|
progressTracker.childrenFor[FIXING] = TwoPartyDealProtocol.Primary.tracker()
|
||||||
progressTracker.currentStep = FIXING
|
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)
|
val result = subProtocol(participant)
|
||||||
return result.tx.outRef(0)
|
return result.tx.outRef(0)
|
||||||
}
|
}
|
||||||
|
@ -367,10 +367,9 @@ object TwoPartyDealProtocol {
|
|||||||
val deal: T = dealToFix.state
|
val deal: T = dealToFix.state
|
||||||
val myOldParty = deal.parties.single { it.name == myName }
|
val myOldParty = deal.parties.single { it.name == myName }
|
||||||
val theirOldParty = deal.parties.single { it.name != myName }
|
val theirOldParty = deal.parties.single { it.name != myName }
|
||||||
val myNewKey = serviceHub.keyManagementService.freshKey().public
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
val newDeal = deal.withPublicKey(myOldParty, myNewKey).withPublicKey(theirOldParty, handshake.publicKey) as T
|
val newDeal = deal
|
||||||
val oldRef = dealToFix.ref
|
val oldRef = dealToFix.ref
|
||||||
|
|
||||||
val ptx = TransactionBuilder()
|
val ptx = TransactionBuilder()
|
||||||
@ -386,7 +385,7 @@ object TwoPartyDealProtocol {
|
|||||||
}
|
}
|
||||||
subProtocol(addFixing)
|
subProtocol(addFixing)
|
||||||
|
|
||||||
return Pair(ptx, arrayListOf(myNewKey))
|
return Pair(ptx, arrayListOf(myOldParty.owningKey))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import core.*
|
|||||||
import core.node.services.DummyTimestampingAuthority
|
import core.node.services.DummyTimestampingAuthority
|
||||||
import core.testutils.*
|
import core.testutils.*
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import java.math.BigDecimal
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@ -15,9 +16,9 @@ fun createDummyIRS(irsSelect: Int): InterestRateSwap.State {
|
|||||||
fixedRatePayer = MEGA_CORP,
|
fixedRatePayer = MEGA_CORP,
|
||||||
notional = 15900000.DOLLARS,
|
notional = 15900000.DOLLARS,
|
||||||
paymentFrequency = Frequency.SemiAnnual,
|
paymentFrequency = Frequency.SemiAnnual,
|
||||||
effectiveDate = LocalDate.of(2016, 3, 16),
|
effectiveDate = LocalDate.of(2016, 3, 10),
|
||||||
effectiveDateAdjustment = null,
|
effectiveDateAdjustment = null,
|
||||||
terminationDate = LocalDate.of(2026, 3, 16),
|
terminationDate = LocalDate.of(2026, 3, 10),
|
||||||
terminationDateAdjustment = null,
|
terminationDateAdjustment = null,
|
||||||
fixedRate = FixedRate(PercentageRatioUnit("1.677")),
|
fixedRate = FixedRate(PercentageRatioUnit("1.677")),
|
||||||
dayCountBasisDay = DayCountBasisDay.D30,
|
dayCountBasisDay = DayCountBasisDay.D30,
|
||||||
@ -98,7 +99,6 @@ fun createDummyIRS(irsSelect: Int): InterestRateSwap.State {
|
|||||||
InterestRateSwap.State(fixedLeg = fixedLeg, floatingLeg = floatingLeg, calculation = calculation, common = common)
|
InterestRateSwap.State(fixedLeg = fixedLeg, floatingLeg = floatingLeg, calculation = calculation, common = common)
|
||||||
}
|
}
|
||||||
2 -> {
|
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)
|
// 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
|
// 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 = "",
|
addressForTransfers = "",
|
||||||
exposure = UnknownType(),
|
exposure = UnknownType(),
|
||||||
localBusinessDay = BusinessCalendar.getInstance("London"),
|
localBusinessDay = BusinessCalendar.getInstance("London"),
|
||||||
tradeID = "trade1",
|
tradeID = "trade2",
|
||||||
hashLegalDocs = "put hash here",
|
hashLegalDocs = "put hash here",
|
||||||
dailyInterestAmount = Expression("(CashAmount * InterestRate ) / (fixedLeg.notional.currency.currencyCode.equals('GBP')) ? 365 : 360")
|
dailyInterestAmount = Expression("(CashAmount * InterestRate ) / (fixedLeg.notional.currency.currencyCode.equals('GBP')) ? 365 : 360")
|
||||||
)
|
)
|
||||||
@ -197,10 +197,25 @@ class IRSTests {
|
|||||||
|
|
||||||
val attachments = MockStorageService().attachments
|
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
|
@Test
|
||||||
fun ok() {
|
fun ok() {
|
||||||
val t = trade()
|
trade().verify()
|
||||||
t.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
|
@Test
|
||||||
fun generateIRS() {
|
fun generateIRS() {
|
||||||
@ -251,6 +266,9 @@ class IRSTests {
|
|||||||
generateIRSTxn(1)
|
generateIRSTxn(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Testing a simple IRS, add a few fixings and then display as CSV
|
||||||
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun `IRS Export test`() {
|
fun `IRS Export test`() {
|
||||||
// No transactions etc required - we're just checking simple maths and export functionallity
|
// 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> {
|
fun trade(): TransactionGroupDSL<InterestRateSwap.State> {
|
||||||
|
|
||||||
|
val ld = LocalDate.of(2016, 3, 8)
|
||||||
|
val bd = BigDecimal("0.0063518")
|
||||||
|
|
||||||
val txgroup: TransactionGroupDSL<InterestRateSwap.State> = transactionGroupFor() {
|
val txgroup: TransactionGroupDSL<InterestRateSwap.State> = transactionGroupFor() {
|
||||||
transaction("Agreement") {
|
transaction("Agreement") {
|
||||||
output("irs post agreement") { singleIRS() }
|
output("irs post agreement") { singleIRS() }
|
||||||
@ -361,18 +386,349 @@ class IRSTests {
|
|||||||
|
|
||||||
transaction("Fix") {
|
transaction("Fix") {
|
||||||
input("irs post agreement")
|
input("irs post agreement")
|
||||||
output("irs post first fixing") { "irs post agreement".output }
|
output("irs post first fixing") {
|
||||||
arg(ORACLE_PUBKEY) { InterestRateSwap.Commands.Fix() }
|
"irs post agreement".output.copy(
|
||||||
timestamp(TEST_TX_TIME)
|
"irs post agreement".output.fixedLeg,
|
||||||
}
|
"irs post agreement".output.floatingLeg,
|
||||||
|
"irs post agreement".output.calculation.applyFixing(ld, FixedRate(RatioUnit(bd))),
|
||||||
transaction("Pay") {
|
"irs post agreement".output.common
|
||||||
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() }
|
arg(ORACLE_PUBKEY) {
|
||||||
|
InterestRateSwap.Commands.Fix()
|
||||||
|
}
|
||||||
|
arg(ORACLE_PUBKEY) {
|
||||||
|
Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)
|
||||||
|
}
|
||||||
timestamp(TEST_TX_TIME)
|
timestamp(TEST_TX_TIME)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return txgroup
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -157,16 +157,16 @@ open class TransactionForTest : AbstractTransactionForTest() {
|
|||||||
private val inStates = arrayListOf<ContractState>()
|
private val inStates = arrayListOf<ContractState>()
|
||||||
fun input(s: () -> ContractState) = inStates.add(s())
|
fun input(s: () -> ContractState) = inStates.add(s())
|
||||||
|
|
||||||
protected fun run(time: Instant) {
|
protected fun runCommandsAndVerify(time: Instant) {
|
||||||
val cmds = commandsToAuthenticatedObjects()
|
val cmds = commandsToAuthenticatedObjects()
|
||||||
val tx = TransactionForVerification(inStates, outStates.map { it.state }, emptyList(), cmds, SecureHash.randomSHA256())
|
val tx = TransactionForVerification(inStates, outStates.map { it.state }, emptyList(), cmds, SecureHash.randomSHA256())
|
||||||
tx.verify()
|
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) {
|
fun rejects(withMessage: String? = null, time: Instant = TEST_TX_TIME) {
|
||||||
val r = try {
|
val r = try {
|
||||||
run(time)
|
runCommandsAndVerify(time)
|
||||||
false
|
false
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
val m = e.message
|
val m = e.message
|
||||||
@ -179,7 +179,9 @@ open class TransactionForTest : AbstractTransactionForTest() {
|
|||||||
if (!r) throw AssertionError("Expected exception but didn't get one")
|
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)
|
infix fun `fails requirement`(msg: String) = rejects(msg)
|
||||||
|
|
||||||
fun fails_requirement(msg: String) = this.`fails requirement`(msg)
|
fun fails_requirement(msg: String) = this.`fails requirement`(msg)
|
||||||
|
Loading…
Reference in New Issue
Block a user