diff --git a/contracts/src/main/kotlin/contracts/IRS.kt b/contracts/src/main/kotlin/contracts/IRS.kt new file mode 100644 index 0000000000..273272a927 --- /dev/null +++ b/contracts/src/main/kotlin/contracts/IRS.kt @@ -0,0 +1,453 @@ +/* + * Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members + * pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms + * set forth therein. + * + * All other rights reserved. + */ + +package contracts + +import core.* +import core.crypto.SecureHash +import core.node.services.DummyTimestampingAuthority +import org.apache.commons.jexl3.JexlBuilder +import org.apache.commons.jexl3.MapContext +import java.math.BigDecimal +import java.math.RoundingMode +import java.time.LocalDate +import java.util.* + +val IRS_PROGRAM_ID = SecureHash.sha256("replace-me-later-with-bytecode-hash-of-irs-code") + +// 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() + +/** + * Event superclass - everything happens on a date. + */ +open class Event(val date: LocalDate) + +/** + * 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() = + dayCountCalculator(accrualStartDate, accrualEndDate, dayCountBasisYear, dayCountBasisDay) + + val dayCountFactor: BigDecimal get() = + // TODO : Fix below (use daycount convention for division) + (BigDecimal(days).divide(BigDecimal(360.0), 8, RoundingMode.HALF_UP)).setScale(4, RoundingMode.HALF_UP) + + open fun asCSV(): String = "$accrualStartDate,$accrualEndDate,$dayCountFactor,$days,$date,${notional.currency},${notional},$rate,$flow" +} + +/** + * 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.pennies)).times(rate.ratioUnit!!.value).toLong(), notional.currency) + + 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.currency) + return Amount(dayCountFactor.times(BigDecimal(notional.pennies)).times(v).toLong(), notional.currency) + } + + override fun toString(): String { + return "FloatingPaymentEvent $accrualStartDate -> $accrualEndDate : $dayCountFactor : $days : $date : $notional : $rate (fix on $fixingDate): $flow" + } + + override fun asCSV(): String = "$accrualStartDate,$accrualEndDate,$dayCountFactor,$days,$date,${notional.currency},${notional},$fixingDate,$rate,$flow" + + /** + * Used for making immutables + */ + fun withNewRate(newRate: Rate): FloatingRatePaymentEvent = + FloatingRatePaymentEvent(date, accrualStartDate, accrualEndDate, dayCountBasisDay, + dayCountBasisYear, fixingDate, notional, newRate) +} + + +/** + * Don't try and use a rate that isn't ready yet. + */ +class DataNotReadyException : Exception() + + +/** + * 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 = SecureHash.sha256("is_this_the_text_of_the_contract ? TBD") + + /** + * 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 valuationDate: String, + 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 + ) + + data class Expression(val expr: 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 OracleRetrievableReferenceRate }.// 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: Rate): Calculation { + val paymentEvent = getFixing(date) + val newFloatingLPS = floatingLegPaymentSchedule + (paymentEvent.date to paymentEvent.withNewRate(newRate)) + return Calculation(expression = expression, + floatingLegPaymentSchedule = newFloatingLPS, + fixedLegpaymentSchedule = fixedLegpaymentSchedule) + } + + fun exportSchedule() { + + } + + } + + abstract class CommonLeg( + val notional: Amount, + val paymentFrequency: Frequency, + val effectiveDate: LocalDate, + val effectiveDateAdjustment: DateRollConvention?, + val terminationDate: LocalDate, + val terminationDateAdjustment: DateRollConvention?, + var dayCountBasisDay: DayCountBasisDay, + var dayCountBasisYear: DayCountBasisYear, + var dayInMonth: Int, + var paymentRule: PaymentRule, + var paymentDelay: Int, + var paymentCalendar: BusinessCalendar, + var interestPeriodAdjustment: AccrualAdjustment + ) { + 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" + } + } + + 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" + } + + 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 fixingPeriod: DateOffset, + 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" + + "FixingPeriond=$fixingPeriod,ResetRule=$resetRule,FixingsPerPayment=$fixingsPerPayment,FixingCalendar=$fixingCalendar," + + "Index=$index,IndexSource=$indexSource,IndexTenor=$indexTenor" + + } + + /** + * verify() with a few examples of what needs to be checked. TODO: Lots more to add. + */ + override fun verify(tx: TransactionForVerification) { + val command = tx.commands.requireSingleCommand() + val time = tx.getTimestampBy(DummyTimestampingAuthority.identity)?.midpoint + if (time == null) throw IllegalArgumentException("must be timestamped") + + val irs = tx.outStates.filterIsInstance().single() + when (command.value) { + is Commands.Agree -> { + requireThat { + "There are no in states for an agreement" by tx.inStates.isEmpty() + "The fixed rate is non zero" by (irs.fixedLeg.fixedRate != FixedRate(PercentageRatioUnit("0.0"))) + "There are events in the fix schedule" by (irs.calculation.fixedLegpaymentSchedule.size > 0) + "There are events in the float schedule" by (irs.calculation.floatingLegPaymentSchedule.size > 0) + // "There are fixes in the schedule" by (irs.calculation.floatingLegPaymentSchedule!!.size > 0) + // TODO: shortlist of other tests + } + } + is Commands.Fix -> { + requireThat { + // TODO: see previous block + // "There is a fixing supplied" by false // TODO + // "The fixing has been signed by an appropriate oracle" by false // TODO + // "The fixing has arrived at the right time" by false + // "The net payment has been calculated" by false // TODO : Not sure if this is the right place + + } + } + is Commands.Pay -> { + requireThat { + // TODO: see previous block + //"A counterparty must be making a payment" by false // TODO + // "The right counterparty must be receiving the payment" by false // TODO + } + } + else -> throw IllegalArgumentException("Unrecognised verifiable command: ${command.value}") + } + } + + interface Commands : CommandData { + class Fix : TypeOnlyCommandData(), 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 + ) : ContractState { + override val programRef = IRS_PROGRAM_ID + + /** + * 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 + var jexl = JexlBuilder().create() + var expr = jexl.createExpression(expression.expr) + var 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(): String = 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): 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 paymentEvent = FixedRatePaymentEvent( + // TODO: We are assuming the payment date is the end date of the accrual period. + periodEndDate, periodStartDate, periodEndDate, + fixedLeg.dayCountBasisDay, + fixedLeg.dayCountBasisYear, + fixedLeg.notional, + fixedLeg.fixedRate + ) + fixedLegPaymentSchedule[periodEndDate] = paymentEvent + periodStartDate = periodEndDate + } + + dates = BusinessCalendar.createGenericSchedule(floatingLeg.effectiveDate, + floatingLeg.fixingsPerPayment, + floatingLeg.fixingCalendar, + floatingLeg.rollConvention, + endDate = floatingLeg.terminationDate) + + var floatingLegPaymentSchedule: MutableMap = HashMap() + periodStartDate = floatingLeg.effectiveDate + + // TODO: Temporary until implemented via Rates Oracle. + val telerate = TelerateOracle("3750") + + // Now create a schedule for the floating and fixes. + for (periodEndDate in dates) { + val paymentEvent = FloatingRatePaymentEvent( + periodEndDate, + periodStartDate, + periodEndDate, + floatingLeg.dayCountBasisDay, + floatingLeg.dayCountBasisYear, + calcFixingDate(periodStartDate, floatingLeg.fixingPeriod, floatingLeg.fixingCalendar), + floatingLeg.notional, + // TODO: OracleRetrievableReferenceRate will be replaced via oracle v soon. + OracleRetrievableReferenceRate(telerate, floatingLeg.indexTenor, floatingLeg.index) + ) + + floatingLegPaymentSchedule.put(periodEndDate, 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 TransactionBuilder().withItems(state, Command(Commands.Agree(), listOf(state.floatingLeg.floatingRatePayer.owningKey, state.fixedLeg.fixedRatePayer.owningKey))) + } + + private fun calcFixingDate(date: LocalDate, fixingPeriod: DateOffset, calendar: BusinessCalendar): LocalDate { + return when (fixingPeriod) { + DateOffset.ZERO -> date + DateOffset.TWODAYS -> calendar.moveBusinessDays(date, DateRollDirection.BACKWARD, 2) + else -> TODO("Improved fixing date calculation logic") + } + } + + // TODO: Replace with rates oracle + fun generateFix(tx: TransactionBuilder, irs: StateAndRef, fixing: Pair) { + tx.addInputState(irs.ref) + tx.addOutputState(irs.state.copy(calculation = irs.state.calculation.applyFixing(fixing.first, fixing.second))) + tx.addCommand(Commands.Fix(), listOf(irs.state.floatingLeg.floatingRatePayer.owningKey, irs.state.fixedLeg.fixedRatePayer.owningKey)) + } +} diff --git a/contracts/src/main/kotlin/contracts/IRSExport.kt b/contracts/src/main/kotlin/contracts/IRSExport.kt new file mode 100644 index 0000000000..e37009f11f --- /dev/null +++ b/contracts/src/main/kotlin/contracts/IRSExport.kt @@ -0,0 +1,15 @@ +/* + * Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members + * pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms + * set forth therein. + * + * All other rights reserved. + */ + +package 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/contracts/IRSUtils.kt b/contracts/src/main/kotlin/contracts/IRSUtils.kt new file mode 100644 index 0000000000..644efb73bf --- /dev/null +++ b/contracts/src/main/kotlin/contracts/IRSUtils.kt @@ -0,0 +1,100 @@ +package contracts + +import core.Amount +import core.Tenor +import java.math.BigDecimal +import java.time.LocalDate + + +// 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(value: BigDecimal) { // TODO: Discuss this type + val value = value +} + +/** + * A class to reprecent a percentage in an unambiguous way. + */ +open class PercentageRatioUnit(percentageAsString: String) : RatioUnit(BigDecimal(percentageAsString).divide(BigDecimal("100"))) { + override fun toString(): String = value.times(BigDecimal(100)).toString()+"%" +} + +/** + * 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 TODO: Discuss + */ +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) + +/** + * A very basic subclass to represent a fixed rate. + */ +class FixedRate(ratioUnit: RatioUnit) : Rate(ratioUnit) { + override fun toString(): String = "$ratioUnit" +} + +/** + * 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. + */ +abstract class ReferenceRate(val name: String): FloatingRate() { + abstract fun getAsOf(date: LocalDate?) : RatioUnit +} + +/** + * A concrete implementation of the above for testing purposes + */ +open class TestReferenceRate(val testrate: String) : ReferenceRate(testrate) { + override fun getAsOf(date: LocalDate?) : RatioUnit { + return testrate.percent + } +} + +/** + * This represents a source of data. + */ +abstract class Oracle() { abstract fun retrieve(tenor: Tenor, date: LocalDate) : RatioUnit } + +class ReutersOracle() : Oracle() { + override fun retrieve(tenor: Tenor, date: LocalDate): RatioUnit { + TODO("Reuters Oracle retrieval") + } +} + +class TelerateOracle(page: String) : Oracle() { + override fun retrieve(tenor: Tenor, date: LocalDate): RatioUnit { + TODO("Telerate Oracle retrieval") + } +} + +/** + * A Reference rate that is retrieved via an Oracle. + */ +open class OracleRetrievableReferenceRate(val oracle: Oracle, val tenor: Tenor, referenceRate: String) : ReferenceRate(referenceRate) { + override fun getAsOf(date: LocalDate?): RatioUnit { + return oracle.retrieve(tenor,date!!) + } + override fun toString(): String = "$name - $tenor" +} + +// TODO: For further discussion. +operator fun Amount.times(other: RatioUnit): Amount = Amount((BigDecimal(this.pennies).multiply(other.value)).longValueExact(), this.currency) +//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/core/src/main/kotlin/core/FinanceTypes.kt b/core/src/main/kotlin/core/FinanceTypes.kt index b90687d596..d0fbeb7da6 100644 --- a/core/src/main/kotlin/core/FinanceTypes.kt +++ b/core/src/main/kotlin/core/FinanceTypes.kt @@ -39,6 +39,8 @@ data class Amount(val pennies: Long, val currency: Currency) : Comparable= 0) { "Negative amounts are not allowed: $pennies" } } + constructor(amount:BigDecimal, currency: Currency) : this(amount.toLong(), currency) + operator fun plus(other: Amount): Amount { checkCurrency(other) return Amount(Math.addExact(pennies, other.pennies), currency) @@ -58,7 +60,8 @@ data class Amount(val pennies: Long, val currency: Currency) : Comparable= 0) + if ( i == 0 ) return date + var retDate = date + var ctr = 0 + while (ctr < i) { + retDate = retDate.plusDays(direction.value) + if (isWorkingDay(retDate)) ctr++ + } + return retDate + } } fun dayCountCalculator(startDate: LocalDate, endDate: LocalDate, dcbYear: DayCountBasisYear, - dcbDay: DayCountBasisDay): BigDecimal { + dcbDay: DayCountBasisDay): Int { // Right now we are only considering Actual/360 and 30/360 .. We'll do the rest later. // TODO: The rest. return when { - dcbDay == DayCountBasisDay.Actual && dcbYear == DayCountBasisYear.Y360 -> BigDecimal((endDate.toEpochDay() - startDate.toEpochDay())) - dcbDay == DayCountBasisDay.D30 && dcbYear == DayCountBasisYear.Y360 -> BigDecimal((endDate.year - startDate.year) * 360.0 + (endDate.monthValue - startDate.monthValue) * 30.0 + endDate.dayOfMonth - startDate.dayOfMonth) + dcbDay == DayCountBasisDay.DActual && dcbYear == DayCountBasisYear.Y360 -> (endDate.toEpochDay() - startDate.toEpochDay()).toInt() + dcbDay == DayCountBasisDay.D30 && dcbYear == DayCountBasisYear.Y360 -> ((endDate.year - startDate.year) * 360.0 + (endDate.monthValue - startDate.monthValue) * 30.0 + endDate.dayOfMonth - startDate.dayOfMonth).toInt() else -> TODO("Can't calculate days using convention $dcbDay / $dcbYear") } } diff --git a/core/src/main/kotlin/core/Utils.kt b/core/src/main/kotlin/core/Utils.kt index d4cff7c637..7026b3faca 100644 --- a/core/src/main/kotlin/core/Utils.kt +++ b/core/src/main/kotlin/core/Utils.kt @@ -159,3 +159,5 @@ fun extractZipFile(zipPath: Path, toPath: Path) { } } } + +// TODO: Generic csv printing utility for clases. \ No newline at end of file diff --git a/core/src/test/kotlin/core/FinanceTypesTest.kt b/core/src/test/kotlin/core/FinanceTypesTest.kt index b5b07b8f26..3ea7c2b82e 100644 --- a/core/src/test/kotlin/core/FinanceTypesTest.kt +++ b/core/src/test/kotlin/core/FinanceTypesTest.kt @@ -11,6 +11,7 @@ package core import org.junit.Test import java.time.LocalDate import java.util.* +import kotlin.test.assertEquals class FinanceTypesTest { @@ -90,7 +91,46 @@ class FinanceTypesTest { assert(result == LocalDate.of(2016,12,28)) } + @Test + fun `calendar date advancing`() { + val ldn = BusinessCalendar.getInstance("London") + val firstDay = LocalDate.of(2015, 12, 20) + val expected = mapOf(0 to firstDay, + 1 to LocalDate.of(2015, 12, 21), + 2 to LocalDate.of(2015, 12, 22), + 3 to LocalDate.of(2015, 12, 23), + 4 to LocalDate.of(2015, 12, 24), + 5 to LocalDate.of(2015, 12, 29), + 6 to LocalDate.of(2015, 12, 30), + 7 to LocalDate.of(2015, 12, 31) + ) + for ((inc, exp) in expected) { + var result = ldn.moveBusinessDays(firstDay, DateRollDirection.FORWARD, inc) + assertEquals(exp, result) + } + } + + @Test + fun `calendar date preceeding`() { + val ldn = BusinessCalendar.getInstance("London") + val firstDay = LocalDate.of(2015, 12, 31) + val expected = mapOf(0 to firstDay, + 1 to LocalDate.of(2015, 12, 30), + 2 to LocalDate.of(2015, 12, 29), + 3 to LocalDate.of(2015, 12, 24), + 4 to LocalDate.of(2015, 12, 23), + 5 to LocalDate.of(2015, 12, 22), + 6 to LocalDate.of(2015, 12, 21), + 7 to LocalDate.of(2015, 12, 18) + ) + + for ((inc, exp) in expected) { + var result = ldn.moveBusinessDays(firstDay, DateRollDirection.BACKWARD, inc) + assertEquals(exp, result) + } + + } diff --git a/src/test/kotlin/contracts/IRSTests.kt b/src/test/kotlin/contracts/IRSTests.kt new file mode 100644 index 0000000000..25fb913c0a --- /dev/null +++ b/src/test/kotlin/contracts/IRSTests.kt @@ -0,0 +1,402 @@ +/* + * Copyright 2015 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members + * pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms + * set forth therein. + * + * All other rights reserved. + */ + +package contracts + +import core.* +import core.node.services.DummyTimestampingAuthority +import core.testutils.* +import org.junit.Test +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, 16), + effectiveDateAdjustment = null, + terminationDate = LocalDate.of(2026, 3, 16), + terminationDateAdjustment = null, + fixedRate = FixedRate(PercentageRatioUnit("1.677")), + dayCountBasisDay = DayCountBasisDay.D30, + dayCountBasisYear = DayCountBasisYear.Y360, + rollConvention = DateRollConvention.ModifiedFollowing, + dayInMonth = 10, + paymentRule = PaymentRule.InArrears, + paymentDelay = 0, + 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 = 0, + paymentCalendar = BusinessCalendar.getInstance("London", "NewYork"), + interestPeriodAdjustment = AccrualAdjustment.Adjusted, + fixingPeriod = DateOffset.TWODAYS, + 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 = InterestRateSwap.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), + valuationDate = "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 = OracleRetrievableReferenceRate(TelerateOracle("T3270"), Tenor("6M"), "EONIA"), + addressForTransfers = "", + exposure = UnknownType(), + localBusinessDay = BusinessCalendar.getInstance("London"), + tradeID = "trade1", + hashLegalDocs = "put hash here", + dailyInterestAmount = InterestRateSwap.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, + fixingPeriod = DateOffset.TWODAYS, + 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 = InterestRateSwap.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), + valuationDate = "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 = OracleRetrievableReferenceRate(TelerateOracle("T3270"), Tenor("6M"), "EONIA"), + addressForTransfers = "", + exposure = UnknownType(), + localBusinessDay = BusinessCalendar.getInstance("London"), + tradeID = "trade1", + hashLegalDocs = "put hash here", + dailyInterestAmount = InterestRateSwap.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 { + + val attachments = MockStorageService().attachments + + @Test + fun ok() { + val t = trade() + t.verify() + } + + /** + * Generate an IRS txn - we'll need it for a few things. + */ + fun generateIRSTxn(irsSelect: Int): LedgerTransaction { + val dummyIRS = createDummyIRS(irsSelect) + val genTX: LedgerTransaction = run { + val gtx = InterestRateSwap().generateAgreement( + fixedLeg = dummyIRS.fixedLeg, + floatingLeg = dummyIRS.floatingLeg, + calculation = dummyIRS.calculation, + common = dummyIRS.common).apply { + setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity, 30.seconds) + signWith(MEGA_CORP_KEY) + signWith(MINI_CORP_KEY) + timestamp(DUMMY_TIMESTAMPER) + } + + val stx = gtx.toSignedTransaction() + stx.verifyToLedgerTransaction(MockIdentityService, attachments) + } + 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).outputs.filterIsInstance().single() + } + + /** + * Test the generate + */ + @Test + fun generateIRS() { + // Tests aren't allowed to return things + generateIRSTxn(1) + } + + @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 (it in fixings) { + newCalculation = newCalculation.applyFixing(it.key, FixedRate(PercentageRatioUnit(it.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() { + var previousTXN = generateIRSTxn(1) + var currentIRS = previousTXN.outputs.filterIsInstance().single() + println(currentIRS.prettyPrint()) + while (true) { + val nextFixingDate = currentIRS.calculation.nextFixingDate() ?: break + println("\n\n\n ***** Applying a fixing to $nextFixingDate \n\n\n") + var fixTX: LedgerTransaction = run { + val tx = TransactionBuilder() + val fixing = Pair(nextFixingDate, FixedRate("0.052".percent)) + InterestRateSwap().generateFix(tx, previousTXN.outRef(0), fixing) + with(tx) { + setTime(TEST_TX_TIME, DummyTimestampingAuthority.identity, 30.seconds) + signWith(MEGA_CORP_KEY) + signWith(MINI_CORP_KEY) + timestamp(DUMMY_TIMESTAMPER) + } + val stx = tx.toSignedTransaction() + stx.verifyToLedgerTransaction(MockIdentityService, attachments) + } + currentIRS = previousTXN.outputs.filterIsInstance().single() + println(currentIRS.prettyPrint()) + 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 `more rate tests`() { + val r1 = FixedRate(PercentageRatioUnit("10")) + val r2 = FixedRate(PercentageRatioUnit("10")) + + // TODO: r1+r2 ? Do we want to allow these. + // TODO: r1*r2 ? + } + + @Test + fun `reference rate testing`() { + val r1 = TestReferenceRate("5") + assert(100 * r1.getAsOf(null) == 5) + } + + @Test + fun `expression calculation testing`() { + val dummyIRS = singleIRS() + val v = FixedRate(PercentageRatioUnit("4.5")) + val stuffToPrint: ArrayList = arrayListOf( + "fixedLeg.notional.pennies", + "fixedLeg.fixedRate.ratioUnit", + "fixedLeg.fixedRate.ratioUnit.value", + "floatingLeg.notional.pennies", + "fixedLeg.fixedRate", + "currentBusinessDate", + "calculation.floatingLegPaymentSchedule.get(currentBusinessDate)", + "fixedLeg.notional.currency.currencyCode", + "fixedLeg.notional.pennies * 10", + "fixedLeg.notional.pennies * fixedLeg.fixedRate.ratioUnit.value", + "(fixedLeg.notional.currency.currencyCode.equals('GBP')) ? 365 : 360 ", + "(fixedLeg.notional.pennies * (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) + var z = dummyIRS.evaluateCalculation(LocalDate.of(2016, 9, 12), InterestRateSwap.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. + } + + + fun trade(): TransactionGroupDSL { + val txgroup: TransactionGroupDSL = transactionGroupFor() { + transaction("Agreement") { + output("irs post agreement") { singleIRS() } + arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } + timestamp(TEST_TX_TIME) + } + + transaction("Fix") { + input("irs post agreement") + output("irs post first fixing") { "irs post agreement".output } + arg(ORACLE_PUBKEY) { InterestRateSwap.Commands.Fix() } + timestamp(TEST_TX_TIME) + } + + transaction("Pay") { + input("irs post first fixing") + output("irs post first payment") { "irs post first fixing".output } + arg(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY) { InterestRateSwap.Commands.Pay() } + timestamp(TEST_TX_TIME) + } + } + return txgroup + } +} \ No newline at end of file diff --git a/src/test/kotlin/core/testutils/TestUtils.kt b/src/test/kotlin/core/testutils/TestUtils.kt index 349389c6f8..68aac26d83 100644 --- a/src/test/kotlin/core/testutils/TestUtils.kt +++ b/src/test/kotlin/core/testutils/TestUtils.kt @@ -37,6 +37,7 @@ inline fun rootCauseExceptions(body: () -> R) : R { object TestUtils { val keypair = generateKeyPair() val keypair2 = generateKeyPair() + val keypair3 = generateKeyPair() } // A dummy time at which we will be pretending test transactions are created. val TEST_TX_TIME = Instant.parse("2015-04-17T12:00:00.00Z") @@ -44,14 +45,22 @@ val TEST_TX_TIME = Instant.parse("2015-04-17T12:00:00.00Z") // A few dummy values for testing. val MEGA_CORP_KEY = TestUtils.keypair val MEGA_CORP_PUBKEY = MEGA_CORP_KEY.public + val MINI_CORP_KEY = TestUtils.keypair2 val MINI_CORP_PUBKEY = MINI_CORP_KEY.public + +val ORACLE_KEY = TestUtils.keypair3 +val ORACLE_PUBKEY = ORACLE_KEY.public + val DUMMY_PUBKEY_1 = DummyPublicKey("x1") val DUMMY_PUBKEY_2 = DummyPublicKey("x2") + val ALICE_KEY = generateKeyPair() val ALICE = ALICE_KEY.public + val BOB_KEY = generateKeyPair() val BOB = BOB_KEY.public + val MEGA_CORP = Party("MegaCorp", MEGA_CORP_PUBKEY) val MINI_CORP = Party("MiniCorp", MINI_CORP_PUBKEY) @@ -70,7 +79,8 @@ val TEST_PROGRAM_MAP: Map> = mapOf( CP_PROGRAM_ID to CommercialPaper::class.java, JavaCommercialPaper.JCP_PROGRAM_ID to JavaCommercialPaper::class.java, CROWDFUND_PROGRAM_ID to CrowdFund::class.java, - DUMMY_PROGRAM_ID to DummyContract::class.java + DUMMY_PROGRAM_ID to DummyContract::class.java, + IRS_PROGRAM_ID to InterestRateSwap::class.java ) ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////