diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt b/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt deleted file mode 100644 index 60ac9f07a5..0000000000 --- a/contracts/src/main/kotlin/com/r3corda/contracts/IRS.kt +++ /dev/null @@ -1,801 +0,0 @@ -package com.r3corda.contracts - -import com.r3corda.core.contracts.* -import com.r3corda.core.contracts.clauses.* -import com.r3corda.core.crypto.Party -import com.r3corda.core.crypto.SecureHash -import com.r3corda.core.node.services.ServiceType -import com.r3corda.core.protocols.ProtocolLogicRefFactory -import com.r3corda.core.transactions.TransactionBuilder -import com.r3corda.core.utilities.suggestInterestRateAnnouncementTimeWindow -import com.r3corda.protocols.TwoPartyDealProtocol -import org.apache.commons.jexl3.JexlBuilder -import org.apache.commons.jexl3.MapContext -import java.math.BigDecimal -import java.math.RoundingMode -import java.security.PublicKey -import java.time.LocalDate -import java.util.* - -val IRS_PROGRAM_ID = InterestRateSwap() - -// This is a placeholder for some types that we haven't identified exactly what they are just yet for things still in discussion -open class UnknownType() { - - override fun equals(other: Any?): Boolean { - return (other is UnknownType) - } - - override fun hashCode() = 1 -} - -/** - * Event superclass - everything happens on a date. - */ -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. - */ -abstract class PaymentEvent(date: LocalDate) : Event(date) { - abstract fun calculate(): Amount -} - -/** - * A [RatePaymentEvent] represents a dated obligation of payment. - * It is a specialisation / modification of a basic cash flow event (to be written) that has some additional assistance - * functions for interest rate swap legs of the fixed and floating nature. - * For the fixed leg, the rate is already known at creation and therefore the flows can be pre-determined. - * For the floating leg, the rate refers to a reference rate which is to be "fixed" at a point in the future. - */ -abstract class RatePaymentEvent(date: LocalDate, - val accrualStartDate: LocalDate, - val accrualEndDate: LocalDate, - val dayCountBasisDay: DayCountBasisDay, - val dayCountBasisYear: DayCountBasisYear, - val notional: Amount, - val rate: Rate) : PaymentEvent(date) { - companion object { - val CSVHeader = "AccrualStartDate,AccrualEndDate,DayCountFactor,Days,Date,Ccy,Notional,Rate,Flow" - } - - override fun calculate(): Amount = flow - - abstract val flow: Amount - - val days: Int get() = calculateDaysBetween(accrualStartDate, accrualEndDate, dayCountBasisYear, dayCountBasisDay) - - // 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() = "$accrualStartDate,$accrualEndDate,$dayCountFactor,$days,$date,${notional.token},$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) -} - -/** - * Basic class for the Fixed Rate Payments on the fixed leg - see [RatePaymentEvent]. - * Assumes that the rate is valid. - */ -class FixedRatePaymentEvent(date: LocalDate, - accrualStartDate: LocalDate, - accrualEndDate: LocalDate, - dayCountBasisDay: DayCountBasisDay, - dayCountBasisYear: DayCountBasisYear, - notional: Amount, - rate: Rate) : - RatePaymentEvent(date, accrualStartDate, accrualEndDate, dayCountBasisDay, dayCountBasisYear, notional, rate) { - companion object { - val CSVHeader = RatePaymentEvent.CSVHeader - } - - override val flow: Amount get() = Amount(dayCountFactor.times(BigDecimal(notional.quantity)).times(rate.ratioUnit!!.value).toLong(), notional.token) - - override fun toString(): String = - "FixedRatePaymentEvent $accrualStartDate -> $accrualEndDate : $dayCountFactor : $days : $date : $notional : $rate : $flow" -} - -/** - * Basic class for the Floating Rate Payments on the floating leg - see [RatePaymentEvent]. - * If the rate is null returns a zero payment. // TODO: Is this the desired behaviour? - */ -class FloatingRatePaymentEvent(date: LocalDate, - accrualStartDate: LocalDate, - accrualEndDate: LocalDate, - dayCountBasisDay: DayCountBasisDay, - dayCountBasisYear: DayCountBasisYear, - val fixingDate: LocalDate, - notional: Amount, - rate: Rate) : RatePaymentEvent(date, accrualStartDate, accrualEndDate, dayCountBasisDay, dayCountBasisYear, notional, rate) { - - companion object { - val CSVHeader = RatePaymentEvent.CSVHeader + ",FixingDate" - } - - override val flow: Amount get() { - // TODO: Should an uncalculated amount return a zero ? null ? etc. - val v = rate.ratioUnit?.value ?: return Amount(0, notional.token) - return Amount(dayCountFactor.times(BigDecimal(notional.quantity)).times(v).toLong(), notional.token) - } - - 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.token},$notional,$fixingDate,$rate,$flow" - - /** - * Used for making immutables. - */ - 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) -} - - -/** - * The Interest Rate Swap class. For a quick overview of what an IRS is, see here - http://www.pimco.co.uk/EN/Education/Pages/InterestRateSwapsBasics1-08.aspx (no endorsement). - * This contract has 4 significant data classes within it, the "Common", "Calculation", "FixedLeg" and "FloatingLeg". - * It also has 4 commands, "Agree", "Fix", "Pay" and "Mature". - * Currently, we are not interested (excuse pun) in valuing the swap, calculating the PVs, DFs and all that good stuff (soon though). - * This is just a representation of a vanilla Fixed vs Floating (same currency) IRS in the R3 prototype model. - */ -class InterestRateSwap() : Contract { - override val legalContractReference = SecureHash.sha256("is_this_the_text_of_the_contract ? TBD") - - companion object { - val oracleType = ServiceType.corda.getSubType("interest_rates") - } - - /** - * This Common area contains all the information that is not leg specific. - */ - data class Common( - val baseCurrency: Currency, - val eligibleCurrency: Currency, - val eligibleCreditSupport: String, - val independentAmounts: Amount, - val threshold: Amount, - val minimumTransferAmount: Amount, - val rounding: Amount, - val valuationDateDescription: String, // This describes (in english) how regularly the swap is to be valued, e.g. "every local working day" - val notificationTime: String, - val resolutionTime: String, - val interestRate: ReferenceRate, - val addressForTransfers: String, - val exposure: UnknownType, - val localBusinessDay: BusinessCalendar, - val dailyInterestAmount: Expression, - val tradeID: String, - val hashLegalDocs: String - ) - - /** - * The Calculation data class is "mutable" through out the life of the swap, as in, it's the only thing that contains - * data that will changed from state to state (Recall that the design insists that everything is immutable, so we actually - * copy / update for each transition). - */ - data class Calculation( - val expression: Expression, - val floatingLegPaymentSchedule: Map, - val fixedLegPaymentSchedule: Map - ) { - /** - * Gets the date of the next fixing. - * @return LocalDate or null if no more fixings. - */ - fun nextFixingDate(): LocalDate? { - return floatingLegPaymentSchedule. - filter { it.value.rate is ReferenceRate }.// TODO - a better way to determine what fixings remain to be fixed - minBy { it.value.fixingDate.toEpochDay() }?.value?.fixingDate - } - - /** - * Returns the fixing for that date. - */ - fun getFixing(date: LocalDate): FloatingRatePaymentEvent = - floatingLegPaymentSchedule.values.single { it.fixingDate == date } - - /** - * Returns a copy after modifying (applying) the fixing for that date. - */ - 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) - } - } - - abstract class CommonLeg( - val notional: Amount, - val paymentFrequency: Frequency, - val effectiveDate: LocalDate, - val effectiveDateAdjustment: DateRollConvention?, - val terminationDate: LocalDate, - val terminationDateAdjustment: DateRollConvention?, - 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," + - "TerminationDateAdjustment=$terminationDateAdjustment,DayCountBasis=$dayCountBasisDay/$dayCountBasisYear,DayInMonth=$dayInMonth," + - "PaymentRule=$paymentRule,PaymentDelay=$paymentDelay,PaymentCalendar=$paymentCalendar,InterestPeriodAdjustment=$interestPeriodAdjustment" - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other?.javaClass != javaClass) return false - - other as CommonLeg - - if (notional != other.notional) return false - if (paymentFrequency != other.paymentFrequency) return false - if (effectiveDate != other.effectiveDate) return false - if (effectiveDateAdjustment != other.effectiveDateAdjustment) return false - if (terminationDate != other.terminationDate) return false - if (terminationDateAdjustment != other.terminationDateAdjustment) return false - if (dayCountBasisDay != other.dayCountBasisDay) return false - if (dayCountBasisYear != other.dayCountBasisYear) return false - if (dayInMonth != other.dayInMonth) return false - if (paymentRule != other.paymentRule) return false - if (paymentDelay != other.paymentDelay) return false - if (paymentCalendar != other.paymentCalendar) return false - if (interestPeriodAdjustment != other.interestPeriodAdjustment) return false - - return true - } - - 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( - var fixedRatePayer: Party, - notional: Amount, - paymentFrequency: Frequency, - effectiveDate: LocalDate, - effectiveDateAdjustment: DateRollConvention?, - terminationDate: LocalDate, - terminationDateAdjustment: DateRollConvention?, - dayCountBasisDay: DayCountBasisDay, - dayCountBasisYear: DayCountBasisYear, - dayInMonth: Int, - paymentRule: PaymentRule, - paymentDelay: Int, - paymentCalendar: BusinessCalendar, - interestPeriodAdjustment: AccrualAdjustment, - var fixedRate: FixedRate, - var rollConvention: DateRollConvention // TODO - best way of implementing - still awaiting some clarity - ) : CommonLeg - (notional, paymentFrequency, effectiveDate, effectiveDateAdjustment, terminationDate, terminationDateAdjustment, - dayCountBasisDay, dayCountBasisYear, dayInMonth, paymentRule, paymentDelay, paymentCalendar, interestPeriodAdjustment) { - override fun toString(): String = "FixedLeg(Payer=$fixedRatePayer," + super.toString() + ",fixedRate=$fixedRate," + - "rollConvention=$rollConvention" - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other?.javaClass != javaClass) return false - if (!super.equals(other)) return false - - other as FixedLeg - - if (fixedRatePayer != other.fixedRatePayer) return false - if (fixedRate != other.fixedRate) return false - if (rollConvention != other.rollConvention) return false - - return true - } - - 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) - - } - - open class FloatingLeg( - var floatingRatePayer: Party, - notional: Amount, - paymentFrequency: Frequency, - effectiveDate: LocalDate, - effectiveDateAdjustment: DateRollConvention?, - terminationDate: LocalDate, - terminationDateAdjustment: DateRollConvention?, - dayCountBasisDay: DayCountBasisDay, - dayCountBasisYear: DayCountBasisYear, - dayInMonth: Int, - paymentRule: PaymentRule, - paymentDelay: Int, - paymentCalendar: BusinessCalendar, - interestPeriodAdjustment: AccrualAdjustment, - var rollConvention: DateRollConvention, - var fixingRollConvention: DateRollConvention, - var resetDayInMonth: Int, - var fixingPeriodOffset: Int, - var resetRule: PaymentRule, - var fixingsPerPayment: Frequency, - var fixingCalendar: BusinessCalendar, - var index: String, - var indexSource: String, - var indexTenor: Tenor - ) : CommonLeg(notional, paymentFrequency, effectiveDate, effectiveDateAdjustment, terminationDate, terminationDateAdjustment, - dayCountBasisDay, dayCountBasisYear, dayInMonth, paymentRule, paymentDelay, paymentCalendar, interestPeriodAdjustment) { - override fun toString(): String = "FloatingLeg(Payer=$floatingRatePayer," + super.toString() + - "rollConvention=$rollConvention,FixingRollConvention=$fixingRollConvention,ResetDayInMonth=$resetDayInMonth" + - "FixingPeriondOffset=$fixingPeriodOffset,ResetRule=$resetRule,FixingsPerPayment=$fixingsPerPayment,FixingCalendar=$fixingCalendar," + - "Index=$index,IndexSource=$indexSource,IndexTenor=$indexTenor" - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other?.javaClass != javaClass) return false - if (!super.equals(other)) return false - - other as FloatingLeg - - if (floatingRatePayer != other.floatingRatePayer) return false - if (rollConvention != other.rollConvention) return false - if (fixingRollConvention != other.fixingRollConvention) return false - if (resetDayInMonth != other.resetDayInMonth) return false - if (fixingPeriodOffset != other.fixingPeriodOffset) return false - if (resetRule != other.resetRule) return false - if (fixingsPerPayment != other.fixingsPerPayment) return false - if (fixingCalendar != other.fixingCalendar) return false - if (index != other.index) return false - if (indexSource != other.indexSource) return false - if (indexTenor != other.indexTenor) return false - - return true - } - - override fun hashCode() = super.hashCode() + 31 * Objects.hash(floatingRatePayer, rollConvention, - fixingRollConvention, resetDayInMonth, fixingPeriodOffset, 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: Int = this.fixingPeriodOffset, - 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) - } - - override fun verify(tx: TransactionForContract) = verifyClause(tx, AllComposition(Clauses.Timestamped(), Clauses.Group()), tx.commands.select()) - - interface Clauses { - /** - * Common superclass for IRS contract clauses, which defines behaviour on match/no-match, and provides - * helper functions for the clauses. - */ - abstract class AbstractIRSClause : Clause() { - // These functions may make more sense to use for basket types, but for now let's leave them here - fun checkLegDates(legs: List) { - 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: List) { - requireThat { - "The notional is non zero" by legs.any { it.notional.quantity > (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: List): Boolean = true - - fun checkRates(@Suppress("UNUSED_PARAMETER") legs: List): Boolean = true - - /** - * 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, payments2: Map): List>> { - val diff1 = payments1.filter { payments1[it.key] != payments2[it.key] } - val diff2 = payments2.filter { payments1[it.key] != payments2[it.key] } - return (diff1.keys + diff2.keys).map { - it to Pair(diff1[it] as FloatingRatePaymentEvent, diff2[it] as FloatingRatePaymentEvent) - } - } - } - - class Group : GroupClauseVerifier(AnyComposition(Agree(), Fix(), Pay(), Mature())) { - override fun groupStates(tx: TransactionForContract): List> - // Group by Trade ID for in / out states - = tx.groupStates() { state -> state.linearId } - } - - class Timestamped : Clause() { - override fun verify(tx: TransactionForContract, - inputs: List, - outputs: List, - commands: List>, - groupingKey: Unit?): Set { - require(tx.timestamp?.midpoint != null) { "must be timestamped" } - // We return an empty set because we don't process any commands - return emptySet() - } - } - - class Agree : AbstractIRSClause() { - override val requiredCommands: Set> = setOf(Commands.Agree::class.java) - - override fun verify(tx: TransactionForContract, - inputs: List, - outputs: List, - commands: List>, - groupingKey: UniqueIdentifier?): Set { - val command = tx.commands.requireSingleCommand() - val irs = outputs.filterIsInstance().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.quantity > 0 && irs.floatingLeg.notional.quantity > 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.token == irs.floatingLeg.notional.token) - "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(listOf(irs.fixedLeg, irs.floatingLeg)) - "The schedules are valid" by checkSchedules(listOf(irs.fixedLeg, irs.floatingLeg)) - "The fixing period date offset cannot be negative" by (irs.floatingLeg.fixingPeriodOffset >= 0) - - // TODO: further tests - } - checkLegAmounts(listOf(irs.fixedLeg, irs.floatingLeg)) - checkLegDates(listOf(irs.fixedLeg, irs.floatingLeg)) - - return setOf(command.value) - } - } - - class Fix : AbstractIRSClause() { - override val requiredCommands: Set> = setOf(Commands.Refix::class.java) - - override fun verify(tx: TransactionForContract, - inputs: List, - outputs: List, - commands: List>, - groupingKey: UniqueIdentifier?): Set { - val command = tx.commands.requireSingleCommand() - val irs = outputs.filterIsInstance().single() - val prevIrs = inputs.filterIsInstance().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 fixValue = command.value.fix - // 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.token == irs.floatingLeg.notional.token) - // "The fixing is not in the future " by (fixCommand) // The oracle should not have signed this . - } - - return setOf(command.value) - } - } - - class Pay : AbstractIRSClause() { - override val requiredCommands: Set> = setOf(Commands.Pay::class.java) - - override fun verify(tx: TransactionForContract, - inputs: List, - outputs: List, - commands: List>, - groupingKey: UniqueIdentifier?): Set { - val command = tx.commands.requireSingleCommand() - requireThat { - "Payments not supported / verifiable yet" by false - } - return setOf(command.value) - } - } - - class Mature : AbstractIRSClause() { - override val requiredCommands: Set> = setOf(Commands.Mature::class.java) - - override fun verify(tx: TransactionForContract, - inputs: List, - outputs: List, - commands: List>, - groupingKey: UniqueIdentifier?): Set { - val command = tx.commands.requireSingleCommand() - val irs = inputs.filterIsInstance().single() - requireThat { - "No more fixings to be applied" by (irs.calculation.nextFixingDate() == null) - "The irs is fully consumed and there is no id matched output state" by outputs.isEmpty() - } - - return setOf(command.value) - } - } - - } - - interface Commands : CommandData { - data class Refix(val fix: Fix) : Commands // Receive interest rate from oracle, Both sides agree - class Pay : TypeOnlyCommandData(), Commands // Not implemented just yet - class Agree : TypeOnlyCommandData(), Commands // Both sides agree to trade - class Mature : TypeOnlyCommandData(), Commands // Trade has matured; no more actions. Cleanup. // TODO: Do we need this? - } - - /** - * The state class contains the 4 major data classes. - */ - data class State( - val fixedLeg: FixedLeg, - val floatingLeg: FloatingLeg, - val calculation: Calculation, - val common: Common, - override val linearId: UniqueIdentifier = UniqueIdentifier(common.tradeID) - ) : FixableDealState, SchedulableState { - - override val contract = IRS_PROGRAM_ID - - override val oracleType: ServiceType - get() = InterestRateSwap.oracleType - - override val ref = common.tradeID - - override val participants: List - get() = parties.map { it.owningKey } - - override fun isRelevant(ourKeys: Set): Boolean { - return (fixedLeg.fixedRatePayer.owningKey in ourKeys) || (floatingLeg.floatingRatePayer.owningKey in ourKeys) - } - - override val parties: List - get() = listOf(fixedLeg.fixedRatePayer, floatingLeg.floatingRatePayer) - - override fun nextScheduledActivity(thisStateRef: StateRef, protocolLogicRefFactory: ProtocolLogicRefFactory): ScheduledActivity? { - val nextFixingOf = nextFixingOf() ?: return null - - // This is perhaps not how we should determine the time point in the business day, but instead expect the schedule to detail some of these aspects - val instant = suggestInterestRateAnnouncementTimeWindow(index = nextFixingOf.name, source = floatingLeg.indexSource, date = nextFixingOf.forDay).start - return ScheduledActivity(protocolLogicRefFactory.create(TwoPartyDealProtocol.FixingRoleDecider::class.java, thisStateRef), instant) - } - - override fun generateAgreement(notary: Party): TransactionBuilder = InterestRateSwap().generateAgreement(floatingLeg, fixedLeg, calculation, common, notary) - - override fun generateFix(ptx: TransactionBuilder, oldState: StateAndRef<*>, fix: Fix) { - InterestRateSwap().generateFix(ptx, StateAndRef(TransactionState(this, oldState.state.notary), oldState.ref), fix) - } - - override fun nextFixingOf(): FixOf? { - val date = calculation.nextFixingDate() - return if (date == null) null else { - val fixingEvent = calculation.getFixing(date) - val oracleRate = fixingEvent.rate as ReferenceRate - FixOf(oracleRate.name, date, oracleRate.tenor) - } - } - - /** - * For evaluating arbitrary java on the platform. - */ - - fun evaluateCalculation(businessDate: LocalDate, expression: Expression = calculation.expression): Any { - // TODO: Jexl is purely for prototyping. It may be replaced - // TODO: Whatever we do use must be secure and sandboxed - val jexl = JexlBuilder().create() - val expr = jexl.createExpression(expression.expr) - val jc = MapContext() - jc.set("fixedLeg", fixedLeg) - jc.set("floatingLeg", floatingLeg) - jc.set("calculation", calculation) - jc.set("common", common) - jc.set("currentBusinessDate", businessDate) - return expr.evaluate(jc) - } - - /** - * Just makes printing it out a bit better for those who don't have 80000 column wide monitors. - */ - fun prettyPrint() = toString().replace(",", "\n") - - } - - /** - * This generates the agreement state and also the schedules from the initial data. - * Note: The day count, interest rate calculation etc are not finished yet, but they are demonstrable. - */ - fun generateAgreement(floatingLeg: FloatingLeg, fixedLeg: FixedLeg, calculation: Calculation, - common: Common, notary: Party): TransactionBuilder { - - val fixedLegPaymentSchedule = HashMap() - var dates = BusinessCalendar.createGenericSchedule(fixedLeg.effectiveDate, fixedLeg.paymentFrequency, fixedLeg.paymentCalendar, fixedLeg.rollConvention, endDate = fixedLeg.terminationDate) - var periodStartDate = fixedLeg.effectiveDate - - // Create a schedule for the fixed payments - for (periodEndDate in dates) { - val paymentDate = BusinessCalendar.getOffsetDate(periodEndDate, Frequency.Daily, fixedLeg.paymentDelay) - val paymentEvent = FixedRatePaymentEvent( - paymentDate, - periodStartDate, - periodEndDate, - fixedLeg.dayCountBasisDay, - fixedLeg.dayCountBasisYear, - fixedLeg.notional, - fixedLeg.fixedRate - ) - fixedLegPaymentSchedule[paymentDate] = paymentEvent - periodStartDate = periodEndDate - } - - dates = BusinessCalendar.createGenericSchedule(floatingLeg.effectiveDate, - floatingLeg.fixingsPerPayment, - floatingLeg.fixingCalendar, - floatingLeg.rollConvention, - endDate = floatingLeg.terminationDate) - - val floatingLegPaymentSchedule: MutableMap = HashMap() - periodStartDate = floatingLeg.effectiveDate - - // Now create a schedule for the floating and fixes. - for (periodEndDate in dates) { - val paymentDate = BusinessCalendar.getOffsetDate(periodEndDate, Frequency.Daily, floatingLeg.paymentDelay) - val paymentEvent = FloatingRatePaymentEvent( - paymentDate, - periodStartDate, - periodEndDate, - floatingLeg.dayCountBasisDay, - floatingLeg.dayCountBasisYear, - calcFixingDate(periodStartDate, floatingLeg.fixingPeriodOffset, floatingLeg.fixingCalendar), - floatingLeg.notional, - ReferenceRate(floatingLeg.indexSource, floatingLeg.indexTenor, floatingLeg.index) - ) - - floatingLegPaymentSchedule[paymentDate] = paymentEvent - periodStartDate = periodEndDate - } - - val newCalculation = Calculation(calculation.expression, floatingLegPaymentSchedule, fixedLegPaymentSchedule) - - // Put all the above into a new State object. - val state = State(fixedLeg, floatingLeg, newCalculation, common) - return TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Agree(), listOf(state.floatingLeg.floatingRatePayer.owningKey, state.fixedLeg.fixedRatePayer.owningKey))) - } - - private fun calcFixingDate(date: LocalDate, fixingPeriodOffset: Int, calendar: BusinessCalendar): LocalDate { - return when (fixingPeriodOffset) { - 0 -> date - else -> calendar.moveBusinessDays(date, DateRollDirection.BACKWARD, fixingPeriodOffset) - } - } - - fun generateFix(tx: TransactionBuilder, irs: StateAndRef, fixing: Fix) { - tx.addInputState(irs) - val fixedRate = FixedRate(RatioUnit(fixing.value)) - tx.addOutputState( - irs.state.data.copy(calculation = irs.state.data.calculation.applyFixing(fixing.of.forDay, fixedRate)), - irs.state.notary - ) - tx.addCommand(Commands.Refix(fixing), listOf(irs.state.data.floatingLeg.floatingRatePayer.owningKey, irs.state.data.fixedLeg.fixedRatePayer.owningKey)) - } -} diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/IRSExport.kt b/contracts/src/main/kotlin/com/r3corda/contracts/IRSExport.kt deleted file mode 100644 index 99951c4a96..0000000000 --- a/contracts/src/main/kotlin/com/r3corda/contracts/IRSExport.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.r3corda.contracts - -fun InterestRateSwap.State.exportIRSToCSV(): String = - "Fixed Leg\n" + FixedRatePaymentEvent.CSVHeader + "\n" + - this.calculation.fixedLegPaymentSchedule.toSortedMap().values.map { it.asCSV() }.joinToString("\n") + "\n" + - "Floating Leg\n" + FloatingRatePaymentEvent.CSVHeader + "\n" + - this.calculation.floatingLegPaymentSchedule.toSortedMap().values.map { it.asCSV() }.joinToString("\n") + "\n" diff --git a/contracts/src/main/kotlin/com/r3corda/contracts/IRSUtils.kt b/contracts/src/main/kotlin/com/r3corda/contracts/IRSUtils.kt deleted file mode 100644 index 55aec6d42d..0000000000 --- a/contracts/src/main/kotlin/com/r3corda/contracts/IRSUtils.kt +++ /dev/null @@ -1,88 +0,0 @@ -package com.r3corda.contracts - -import com.r3corda.core.contracts.Amount -import com.r3corda.core.contracts.Tenor -import java.math.BigDecimal -import java.util.* - - -// Things in here will move to the general utils class when we've hammered out various discussions regarding amounts, dates, oracle etc. - -/** - * A utility class to prevent the various mixups between percentages, decimals, bips etc. - */ -open class RatioUnit(val value: BigDecimal) { // TODO: Discuss this type - override fun equals(other: Any?) = (other as? RatioUnit)?.value == value - override fun hashCode() = value.hashCode() - override fun toString() = value.toString() -} - -/** - * A class to reprecent a percentage in an unambiguous way. - */ -open class PercentageRatioUnit(percentageAsString: String) : RatioUnit(BigDecimal(percentageAsString).divide(BigDecimal("100"))) { - override fun toString() = value.times(BigDecimal(100)).toString() + "%" -} - -/** - * For the convenience of writing "5".percent - * Note that we do not currently allow 10.percent (ie no quotes) as this might get a little confusing if 0.1.percent was - * written. Additionally, there is a possibility of creating a precision error in the implicit conversion. - */ -val String.percent: PercentageRatioUnit get() = PercentageRatioUnit(this) - -/** - * Parent of the Rate family. Used to denote fixed rates, floating rates, reference rates etc. - */ -open class Rate(val ratioUnit: RatioUnit? = null) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other?.javaClass != javaClass) return false - - other as Rate - - if (ratioUnit != other.ratioUnit) return false - - return true - } - - /** - * @returns the hash code of the ratioUnit or zero if the ratioUnit is null, as is the case for floating rate fixings - * 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() = ratioUnit?.hashCode() ?: 0 - override fun toString() = ratioUnit.toString() -} - -/** - * A very basic subclass to represent a fixed rate. - */ -class FixedRate(ratioUnit: RatioUnit) : Rate(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() -} - -/** - * The parent class of the Floating rate classes. - */ -open class FloatingRate : Rate(null) - -/** - * So a reference rate is a rate that takes its value from a source at a given date - * e.g. LIBOR 6M as of 17 March 2016. Hence it requires a source (name) and a value date in the getAsOf(..) method. - */ -class ReferenceRate(val oracle: String, val tenor: Tenor, val name: String) : FloatingRate() { - override fun toString(): String = "$name - $tenor" -} - -// TODO: For further discussion. -operator fun Amount.times(other: RatioUnit): Amount = Amount((BigDecimal(this.quantity).multiply(other.value)).longValueExact(), this.token) -//operator fun Amount.times(other: FixedRate): Amount = Amount((BigDecimal(this.pennies).multiply(other.value)).longValueExact(), this.currency) -//fun Amount.times(other: InterestRateSwap.RatioUnit): Amount = Amount((BigDecimal(this.pennies).multiply(other.value)).longValueExact(), this.currency) - -operator fun kotlin.Int.times(other: FixedRate): Int = BigDecimal(this).multiply(other.ratioUnit!!.value).intValueExact() -operator fun Int.times(other: Rate): Int = BigDecimal(this).multiply(other.ratioUnit!!.value).intValueExact() -operator fun Int.times(other: RatioUnit): Int = BigDecimal(this).multiply(other.value).intValueExact() diff --git a/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt b/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt deleted file mode 100644 index facd82f1e6..0000000000 --- a/contracts/src/test/kotlin/com/r3corda/contracts/IRSTests.kt +++ /dev/null @@ -1,718 +0,0 @@ -package com.r3corda.contracts - -import com.r3corda.core.contracts.* -import com.r3corda.core.node.recordTransactions -import com.r3corda.core.seconds -import com.r3corda.core.transactions.SignedTransaction -import com.r3corda.core.utilities.DUMMY_NOTARY -import com.r3corda.core.utilities.DUMMY_NOTARY_KEY -import com.r3corda.core.utilities.TEST_TX_TIME -import com.r3corda.testing.* -import com.r3corda.testing.node.MockServices -import org.junit.Test -import java.math.BigDecimal -import java.time.LocalDate -import java.util.* - -fun createDummyIRS(irsSelect: Int): InterestRateSwap.State { - return when (irsSelect) { - 1 -> { - - val fixedLeg = InterestRateSwap.FixedLeg( - fixedRatePayer = MEGA_CORP, - notional = 15900000.DOLLARS, - paymentFrequency = Frequency.SemiAnnual, - effectiveDate = LocalDate.of(2016, 3, 10), - effectiveDateAdjustment = null, - terminationDate = LocalDate.of(2026, 3, 10), - terminationDateAdjustment = null, - fixedRate = FixedRate(PercentageRatioUnit("1.677")), - dayCountBasisDay = DayCountBasisDay.D30, - dayCountBasisYear = DayCountBasisYear.Y360, - rollConvention = DateRollConvention.ModifiedFollowing, - dayInMonth = 10, - paymentRule = PaymentRule.InArrears, - paymentDelay = 3, - paymentCalendar = BusinessCalendar.getInstance("London", "NewYork"), - interestPeriodAdjustment = AccrualAdjustment.Adjusted - ) - - val floatingLeg = InterestRateSwap.FloatingLeg( - floatingRatePayer = MINI_CORP, - notional = 15900000.DOLLARS, - paymentFrequency = Frequency.Quarterly, - effectiveDate = LocalDate.of(2016, 3, 10), - effectiveDateAdjustment = null, - terminationDate = LocalDate.of(2026, 3, 10), - terminationDateAdjustment = null, - dayCountBasisDay = DayCountBasisDay.D30, - dayCountBasisYear = DayCountBasisYear.Y360, - rollConvention = DateRollConvention.ModifiedFollowing, - fixingRollConvention = DateRollConvention.ModifiedFollowing, - dayInMonth = 10, - resetDayInMonth = 10, - paymentRule = PaymentRule.InArrears, - paymentDelay = 3, - paymentCalendar = BusinessCalendar.getInstance("London", "NewYork"), - interestPeriodAdjustment = AccrualAdjustment.Adjusted, - fixingPeriodOffset = 2, - resetRule = PaymentRule.InAdvance, - fixingsPerPayment = Frequency.Quarterly, - fixingCalendar = BusinessCalendar.getInstance("London"), - index = "LIBOR", - indexSource = "TEL3750", - indexTenor = Tenor("3M") - ) - - val calculation = InterestRateSwap.Calculation ( - - // TODO: this seems to fail quite dramatically - //expression = "fixedLeg.notional * fixedLeg.fixedRate", - - // TODO: How I want it to look - //expression = "( fixedLeg.notional * (fixedLeg.fixedRate)) - (floatingLeg.notional * (rateSchedule.get(context.getDate('currentDate'))))", - - // How it's ended up looking, which I think is now broken but it's a WIP. - expression = Expression("( fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value)) -" + - "(floatingLeg.notional.pennies * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))"), - - floatingLegPaymentSchedule = HashMap(), - fixedLegPaymentSchedule = HashMap() - ) - - val EUR = currency("EUR") - - val common = InterestRateSwap.Common( - baseCurrency = EUR, - eligibleCurrency = EUR, - eligibleCreditSupport = "Cash in an Eligible Currency", - independentAmounts = Amount(0, EUR), - threshold = Amount(0, EUR), - minimumTransferAmount = Amount(250000 * 100, EUR), - rounding = Amount(10000 * 100, EUR), - valuationDateDescription = "Every Local Business Day", - notificationTime = "2:00pm London", - resolutionTime = "2:00pm London time on the first LocalBusiness Day following the date on which the notice is given ", - interestRate = ReferenceRate("T3270", Tenor("6M"), "EONIA"), - addressForTransfers = "", - exposure = UnknownType(), - localBusinessDay = BusinessCalendar.getInstance("London"), - tradeID = "trade1", - hashLegalDocs = "put hash here", - dailyInterestAmount = Expression("(CashAmount * InterestRate ) / (fixedLeg.notional.currency.currencyCode.equals('GBP')) ? 365 : 360") - ) - - 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 - - val fixedLeg = InterestRateSwap.FixedLeg( - fixedRatePayer = MEGA_CORP, - notional = 25000000.DOLLARS, - paymentFrequency = Frequency.SemiAnnual, - effectiveDate = LocalDate.of(2015, 3, 10), - effectiveDateAdjustment = null, - terminationDate = LocalDate.of(2025, 3, 10), - terminationDateAdjustment = null, - fixedRate = FixedRate(PercentageRatioUnit("1.3")), - dayCountBasisDay = DayCountBasisDay.D30, - dayCountBasisYear = DayCountBasisYear.Y360, - rollConvention = DateRollConvention.ModifiedFollowing, - dayInMonth = 10, - paymentRule = PaymentRule.InArrears, - paymentDelay = 0, - paymentCalendar = BusinessCalendar.getInstance(), - interestPeriodAdjustment = AccrualAdjustment.Adjusted - ) - - val floatingLeg = InterestRateSwap.FloatingLeg( - floatingRatePayer = MINI_CORP, - notional = 25000000.DOLLARS, - paymentFrequency = Frequency.Quarterly, - effectiveDate = LocalDate.of(2015, 3, 10), - effectiveDateAdjustment = null, - terminationDate = LocalDate.of(2025, 3, 10), - terminationDateAdjustment = null, - dayCountBasisDay = DayCountBasisDay.DActual, - dayCountBasisYear = DayCountBasisYear.Y360, - rollConvention = DateRollConvention.ModifiedFollowing, - fixingRollConvention = DateRollConvention.ModifiedFollowing, - dayInMonth = 10, - resetDayInMonth = 10, - paymentRule = PaymentRule.InArrears, - paymentDelay = 0, - paymentCalendar = BusinessCalendar.getInstance(), - interestPeriodAdjustment = AccrualAdjustment.Adjusted, - fixingPeriodOffset = 2, - resetRule = PaymentRule.InAdvance, - fixingsPerPayment = Frequency.Quarterly, - fixingCalendar = BusinessCalendar.getInstance(), - index = "USD LIBOR", - indexSource = "TEL3750", - indexTenor = Tenor("3M") - ) - - val calculation = InterestRateSwap.Calculation ( - - // TODO: this seems to fail quite dramatically - //expression = "fixedLeg.notional * fixedLeg.fixedRate", - - // TODO: How I want it to look - //expression = "( fixedLeg.notional * (fixedLeg.fixedRate)) - (floatingLeg.notional * (rateSchedule.get(context.getDate('currentDate'))))", - - // How it's ended up looking, which I think is now broken but it's a WIP. - expression = Expression("( fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value)) -" + - "(floatingLeg.notional.pennies * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))"), - - floatingLegPaymentSchedule = HashMap(), - fixedLegPaymentSchedule = HashMap() - ) - - val EUR = currency("EUR") - - val common = InterestRateSwap.Common( - baseCurrency = EUR, - eligibleCurrency = EUR, - eligibleCreditSupport = "Cash in an Eligible Currency", - independentAmounts = Amount(0, EUR), - threshold = Amount(0, EUR), - minimumTransferAmount = Amount(250000 * 100, EUR), - rounding = Amount(10000 * 100, EUR), - valuationDateDescription = "Every Local Business Day", - notificationTime = "2:00pm London", - resolutionTime = "2:00pm London time on the first LocalBusiness Day following the date on which the notice is given ", - interestRate = ReferenceRate("T3270", Tenor("6M"), "EONIA"), - addressForTransfers = "", - exposure = UnknownType(), - localBusinessDay = BusinessCalendar.getInstance("London"), - tradeID = "trade2", - hashLegalDocs = "put hash here", - dailyInterestAmount = Expression("(CashAmount * InterestRate ) / (fixedLeg.notional.currency.currencyCode.equals('GBP')) ? 365 : 360") - ) - - return InterestRateSwap.State(fixedLeg = fixedLeg, floatingLeg = floatingLeg, calculation = calculation, common = common) - - } - else -> TODO("IRS number $irsSelect not defined") - } -} - -class IRSTests { - @Test - fun ok() { - trade().verifies() - } - - @Test - fun `ok with groups`() { - tradegroups().verifies() - } - - /** - * Generate an IRS txn - we'll need it for a few things. - */ - fun generateIRSTxn(irsSelect: Int): SignedTransaction { - val dummyIRS = createDummyIRS(irsSelect) - val genTX: SignedTransaction = run { - val gtx = InterestRateSwap().generateAgreement( - fixedLeg = dummyIRS.fixedLeg, - floatingLeg = dummyIRS.floatingLeg, - calculation = dummyIRS.calculation, - common = dummyIRS.common, - notary = DUMMY_NOTARY).apply { - setTime(TEST_TX_TIME, 30.seconds) - signWith(MEGA_CORP_KEY) - signWith(MINI_CORP_KEY) - signWith(DUMMY_NOTARY_KEY) - } - gtx.toSignedTransaction() - } - return genTX - } - - /** - * Just make sure it's sane. - */ - @Test - fun pprintIRS() { - val irs = singleIRS() - println(irs.prettyPrint()) - } - - /** - * Utility so I don't have to keep typing this. - */ - fun singleIRS(irsSelector: Int = 1): InterestRateSwap.State { - return generateIRSTxn(irsSelector).tx.outputs.map { it.data }.filterIsInstance().single() - } - - /** - * Test the generate. No explicit exception as if something goes wrong, we'll find out anyway. - */ - @Test - fun generateIRS() { - // Tests aren't allowed to return things - 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 - val irs = singleIRS(2) - - var newCalculation = irs.calculation - - val fixings = mapOf(LocalDate.of(2015, 3, 6) to "0.6", - LocalDate.of(2015, 6, 8) to "0.75", - LocalDate.of(2015, 9, 8) to "0.8", - LocalDate.of(2015, 12, 8) to "0.55", - LocalDate.of(2016, 3, 8) to "0.644") - - for ((key, value) in fixings) { - newCalculation = newCalculation.applyFixing(key, FixedRate(PercentageRatioUnit(value))) - } - - val newIRS = InterestRateSwap.State(irs.fixedLeg, irs.floatingLeg, newCalculation, irs.common) - println(newIRS.exportIRSToCSV()) - } - - /** - * Make sure it has a schedule and the schedule has some unfixed rates. - */ - @Test - fun `next fixing date`() { - val irs = singleIRS(1) - println(irs.calculation.nextFixingDate()) - } - - /** - * Iterate through all the fix dates and add something. - */ - @Test - fun generateIRSandFixSome() { - val services = MockServices() - var previousTXN = generateIRSTxn(1) - previousTXN.toLedgerTransaction(services).verify() - services.recordTransactions(previousTXN) - fun currentIRS() = previousTXN.tx.outputs.map { it.data }.filterIsInstance().single() - - while (true) { - val nextFix: FixOf = currentIRS().nextFixingOf() ?: break - val fixTX: SignedTransaction = run { - val tx = TransactionType.General.Builder(DUMMY_NOTARY) - val fixing = Fix(nextFix, "0.052".percent.value) - InterestRateSwap().generateFix(tx, previousTXN.tx.outRef(0), fixing) - with(tx) { - setTime(TEST_TX_TIME, 30.seconds) - signWith(MEGA_CORP_KEY) - signWith(MINI_CORP_KEY) - signWith(DUMMY_NOTARY_KEY) - } - tx.toSignedTransaction() - } - fixTX.toLedgerTransaction(services).verify() - services.recordTransactions(fixTX) - previousTXN = fixTX - } - } - - // Move these later as they aren't IRS specific. - @Test - fun `test some rate objects 100 * FixedRate(5%)`() { - val r1 = FixedRate(PercentageRatioUnit("5")) - assert(100 * r1 == 5) - } - - @Test - fun `expression calculation testing`() { - val dummyIRS = singleIRS() - val stuffToPrint: ArrayList = arrayListOf( - "fixedLeg.notional.quantity", - "fixedLeg.fixedRate.ratioUnit", - "fixedLeg.fixedRate.ratioUnit.value", - "floatingLeg.notional.quantity", - "fixedLeg.fixedRate", - "currentBusinessDate", - "calculation.floatingLegPaymentSchedule.get(currentBusinessDate)", - "fixedLeg.notional.token.currencyCode", - "fixedLeg.notional.quantity * 10", - "fixedLeg.notional.quantity * fixedLeg.fixedRate.ratioUnit.value", - "(fixedLeg.notional.token.currencyCode.equals('GBP')) ? 365 : 360 ", - "(fixedLeg.notional.quantity * (fixedLeg.fixedRate.ratioUnit.value))" - // "calculation.floatingLegPaymentSchedule.get(context.getDate('currentDate')).rate" - // "calculation.floatingLegPaymentSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value", - //"( fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value)) - (floatingLeg.notional.pennies * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))", - // "( fixedLeg.notional * fixedLeg.fixedRate )" - ) - - for (i in stuffToPrint) { - println(i) - val z = dummyIRS.evaluateCalculation(LocalDate.of(2016, 9, 15), Expression(i)) - println(z.javaClass) - println(z) - println("-----------") - } - // This does not throw an exception in the test itself; it evaluates the above and they will throw if they do not pass. - } - - - /** - * Generates a typical transactional history for an IRS. - */ - fun trade(): LedgerDSL { - - val ld = LocalDate.of(2016, 3, 8) - val bd = BigDecimal("0.0063518") - - return ledger { - transaction("Agreement") { - output("irs post agreement") { singleIRS() } - command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } - timestamp(TEST_TX_TIME) - this.verifies() - } - - transaction("Fix") { - input("irs post agreement") - val postAgreement = "irs post agreement".output() - output("irs post first fixing") { - postAgreement.copy( - postAgreement.fixedLeg, - postAgreement.floatingLeg, - postAgreement.calculation.applyFixing(ld, FixedRate(RatioUnit(bd))), - postAgreement.common - ) - } - command(ORACLE_PUBKEY) { - InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)) - } - timestamp(TEST_TX_TIME) - this.verifies() - } - } - } - - @Test - fun `ensure failure occurs when there are inbound states for an agreement command`() { - val irs = singleIRS() - transaction { - input() { irs } - output("irs post agreement") { irs } - command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } - timestamp(TEST_TX_TIME) - this `fails with` "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() - transaction { - output() { - irs.copy(calculation = irs.calculation.copy(fixedLegPaymentSchedule = emptySchedule)) - } - command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } - timestamp(TEST_TX_TIME) - this `fails with` "There are events in the fix schedule" - } - } - - @Test - fun `ensure failure occurs when no events in floating schedule`() { - val irs = singleIRS() - val emptySchedule = HashMap() - transaction { - output() { - irs.copy(calculation = irs.calculation.copy(floatingLegPaymentSchedule = emptySchedule)) - } - command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } - timestamp(TEST_TX_TIME) - this `fails with` "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(quantity = 0))) - } - command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } - timestamp(TEST_TX_TIME) - this `fails with` "All notionals must be non zero" - } - - transaction { - output() { - irs.copy(irs.fixedLeg.copy(notional = irs.floatingLeg.notional.copy(quantity = 0))) - } - command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } - timestamp(TEST_TX_TIME) - this `fails with` "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 - } - command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } - timestamp(TEST_TX_TIME) - this `fails with` "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.quantity, Currency.getInstance("JPY")))) - transaction { - output() { - modifiedIRS - } - command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } - timestamp(TEST_TX_TIME) - this `fails with` "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.quantity + 1, irs.floatingLeg.notional.token))) - transaction { - output() { - modifiedIRS - } - command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } - timestamp(TEST_TX_TIME) - this `fails with` "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 - } - command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } - timestamp(TEST_TX_TIME) - this `fails with` "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 - } - command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } - timestamp(TEST_TX_TIME) - this `fails with` "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 - } - command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } - timestamp(TEST_TX_TIME) - this `fails with` "The termination dates are aligned" - } - - - val modifiedIRS4 = irs.copy(floatingLeg = irs.floatingLeg.copy(effectiveDate = irs.fixedLeg.effectiveDate.minusDays(1))) - transaction { - output() { - modifiedIRS4 - } - command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } - timestamp(TEST_TX_TIME) - this `fails with` "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() } - command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } - timestamp(TEST_TX_TIME) - this.verifies() - } - - 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 { - command(ORACLE_PUBKEY) { - InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)) - } - timestamp(TEST_TX_TIME) - output() { newIRS } - this.verifies() - } - - // This test makes sure that verify confirms the fixing was applied and there is a difference in the old and new - tweak { - command(ORACLE_PUBKEY) { InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)) } - timestamp(TEST_TX_TIME) - output() { oldIRS } - this `fails with` "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 { - command(ORACLE_PUBKEY) { InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)) } - timestamp(TEST_TX_TIME) - - val firstResetKey = newIRS.calculation.floatingLegPaymentSchedule.keys.first() - val firstResetValue = newIRS.calculation.floatingLegPaymentSchedule[firstResetKey] - val modifiedFirstResetValue = firstResetValue!!.copy(notional = Amount(firstResetValue.notional.quantity, 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 with` "There is only one change in the IRS floating leg payment schedule" - } - - // This tests modifies the payment currency for the fixing - tweak { - command(ORACLE_PUBKEY) { InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)) } - timestamp(TEST_TX_TIME) - - val latestReset = newIRS.calculation.floatingLegPaymentSchedule.filter { it.value.rate is FixedRate }.maxBy { it.key } - val modifiedLatestResetValue = latestReset!!.value.copy(notional = Amount(latestReset.value.notional.quantity, 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 with` "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(): LedgerDSL { - val ld1 = LocalDate.of(2016, 3, 8) - val bd1 = BigDecimal("0.0063518") - - val irs = singleIRS() - - return ledger { - transaction("Agreement") { - output("irs post agreement1") { - irs.copy( - irs.fixedLeg, - irs.floatingLeg, - irs.calculation, - irs.common.copy(tradeID = "t1") - ) - } - command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } - timestamp(TEST_TX_TIME) - this.verifies() - } - - transaction("Agreement") { - output("irs post agreement2") { - irs.copy( - linearId = UniqueIdentifier("t2"), - fixedLeg = irs.fixedLeg, - floatingLeg = irs.floatingLeg, - calculation = irs.calculation, - common = irs.common.copy(tradeID = "t2") - ) - } - command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } - timestamp(TEST_TX_TIME) - this.verifies() - } - - transaction("Fix") { - input("irs post agreement1") - input("irs post agreement2") - val postAgreement1 = "irs post agreement1".output() - output("irs post first fixing1") { - postAgreement1.copy( - postAgreement1.fixedLeg, - postAgreement1.floatingLeg, - postAgreement1.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))), - postAgreement1.common.copy(tradeID = "t1") - ) - } - val postAgreement2 = "irs post agreement2".output() - output("irs post first fixing2") { - postAgreement2.copy( - postAgreement2.fixedLeg, - postAgreement2.floatingLeg, - postAgreement2.calculation.applyFixing(ld1, FixedRate(RatioUnit(bd1))), - postAgreement2.common.copy(tradeID = "t2") - ) - } - - command(ORACLE_PUBKEY) { - InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld1, Tenor("3M")), bd1)) - } - timestamp(TEST_TX_TIME) - this.verifies() - } - } - } -} - -