Merged in mike-fix-irs (pull request #244)

Fix how the IRS contract uses fix commands
This commit is contained in:
Mike Hearn 2016-07-28 10:19:16 +02:00
commit 5167ed7df2
3 changed files with 49 additions and 75 deletions

View File

@ -77,7 +77,7 @@ abstract class RatePaymentEvent(date: LocalDate,
// TODO : Fix below (use daycount convention for division, not hardcoded 360 etc) // 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) 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" open fun asCSV() = "$accrualStartDate,$accrualEndDate,$dayCountFactor,$days,$date,${notional.token},$notional,$rate,$flow"
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
@ -114,8 +114,7 @@ class FixedRatePaymentEvent(date: LocalDate,
val CSVHeader = RatePaymentEvent.CSVHeader val CSVHeader = RatePaymentEvent.CSVHeader
} }
override val flow: Amount<Currency> get() = override val flow: Amount<Currency> get() = Amount(dayCountFactor.times(BigDecimal(notional.quantity)).times(rate.ratioUnit!!.value).toLong(), notional.token)
Amount<Currency>(dayCountFactor.times(BigDecimal(notional.quantity)).times(rate.ratioUnit!!.value).toLong(), notional.token)
override fun toString(): String = override fun toString(): String =
"FixedRatePaymentEvent $accrualStartDate -> $accrualEndDate : $dayCountFactor : $days : $date : $notional : $rate : $flow" "FixedRatePaymentEvent $accrualStartDate -> $accrualEndDate : $dayCountFactor : $days : $date : $notional : $rate : $flow"
@ -140,13 +139,13 @@ class FloatingRatePaymentEvent(date: LocalDate,
override val flow: Amount<Currency> get() { override val flow: Amount<Currency> get() {
// TODO: Should an uncalculated amount return a zero ? null ? etc. // TODO: Should an uncalculated amount return a zero ? null ? etc.
val v = rate.ratioUnit?.value ?: return Amount<Currency>(0, notional.token) val v = rate.ratioUnit?.value ?: return Amount(0, notional.token)
return Amount<Currency>(dayCountFactor.times(BigDecimal(notional.quantity)).times(v).toLong(), 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 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" override fun asCSV(): String = "$accrualStartDate,$accrualEndDate,$dayCountFactor,$days,$date,${notional.token},$notional,$fixingDate,$rate,$flow"
/** /**
* Used for making immutables. * Used for making immutables.
@ -450,7 +449,7 @@ class InterestRateSwap() : ClauseVerifier() {
override val clauses: List<SingleClause> = listOf(Clause.Timestamped(), Clause.Group()) override val clauses: List<SingleClause> = listOf(Clause.Timestamped(), Clause.Group())
override fun extractCommands(tx: TransactionForContract): Collection<AuthenticatedObject<CommandData>> override fun extractCommands(tx: TransactionForContract): Collection<AuthenticatedObject<CommandData>>
= tx.commands.select<Commands>() + tx.commands.select<TimestampCommand>() = tx.commands.select<Commands>() + tx.commands.select<TimestampCommand>()
interface Clause { interface Clause {
/** /**
@ -498,8 +497,9 @@ class InterestRateSwap() : ClauseVerifier() {
fun getFloatingLegPaymentsDifferences(payments1: Map<LocalDate, Event>, payments2: Map<LocalDate, Event>): List<Pair<LocalDate, Pair<FloatingRatePaymentEvent, FloatingRatePaymentEvent>>> { fun getFloatingLegPaymentsDifferences(payments1: Map<LocalDate, Event>, payments2: Map<LocalDate, Event>): List<Pair<LocalDate, Pair<FloatingRatePaymentEvent, FloatingRatePaymentEvent>>> {
val diff1 = payments1.filter { payments1[it.key] != payments2[it.key] } val diff1 = payments1.filter { payments1[it.key] != payments2[it.key] }
val diff2 = payments2.filter { payments1[it.key] != payments2[it.key] } val diff2 = payments2.filter { payments1[it.key] != payments2[it.key] }
val ret = (diff1.keys + diff2.keys).map { it to Pair(diff1.get(it) as FloatingRatePaymentEvent, diff2.get(it) as FloatingRatePaymentEvent) } return (diff1.keys + diff2.keys).map {
return ret it to Pair(diff1[it] as FloatingRatePaymentEvent, diff2[it] as FloatingRatePaymentEvent)
}
} }
} }
@ -513,6 +513,7 @@ class InterestRateSwap() : ClauseVerifier() {
// Group by Trade ID for in / out states // Group by Trade ID for in / out states
return tx.groupStates() { state: InterestRateSwap.State -> state.common.tradeID } return tx.groupStates() { state: InterestRateSwap.State -> state.common.tradeID }
} }
override val clauses: List<GroupClause<State, String>> override val clauses: List<GroupClause<State, String>>
get() = listOf(Agree(), Fix(), Pay(), Mature()) get() = listOf(Agree(), Fix(), Pay(), Mature())
} }
@ -530,12 +531,11 @@ class InterestRateSwap() : ClauseVerifier() {
// derived as the correct notary // derived as the correct notary
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
val command = tx.commands.getTimestampByName("Mock Company 0", "Notary Service", "Bank A") val command = tx.commands.getTimestampByName("Mock Company 0", "Notary Service", "Bank A")
val time = command?.midpoint ?: throw IllegalArgumentException("must be timestamped")
if (time == null) throw IllegalArgumentException("must be timestamped") return setOf(command)
return setOf(command!!)
} }
} }
class Agree : AbstractIRSClause() { class Agree : AbstractIRSClause() {
override val requiredCommands: Set<Class<out CommandData>> override val requiredCommands: Set<Class<out CommandData>>
get() = setOf(Commands.Agree::class.java) get() = setOf(Commands.Agree::class.java)
@ -572,16 +572,17 @@ class InterestRateSwap() : ClauseVerifier() {
return setOf(command.value) return setOf(command.value)
} }
} }
class Fix : AbstractIRSClause() { class Fix : AbstractIRSClause() {
override val requiredCommands: Set<Class<out CommandData>> override val requiredCommands: Set<Class<out CommandData>>
get() = setOf(Commands.Fix::class.java) get() = setOf(Commands.Refix::class.java)
override fun verify(tx: TransactionForContract, override fun verify(tx: TransactionForContract,
inputs: List<State>, inputs: List<State>,
outputs: List<State>, outputs: List<State>,
commands: Collection<AuthenticatedObject<CommandData>>, commands: Collection<AuthenticatedObject<CommandData>>,
token: String): Set<CommandData> { token: String): Set<CommandData> {
val command = tx.commands.requireSingleCommand<Commands.Fix>() val command = tx.commands.requireSingleCommand<Commands.Refix>()
val irs = outputs.filterIsInstance<State>().single() val irs = outputs.filterIsInstance<State>().single()
val prevIrs = inputs.filterIsInstance<State>().single() val prevIrs = inputs.filterIsInstance<State>().single()
val paymentDifferences = getFloatingLegPaymentsDifferences(prevIrs.calculation.floatingLegPaymentSchedule, irs.calculation.floatingLegPaymentSchedule) val paymentDifferences = getFloatingLegPaymentsDifferences(prevIrs.calculation.floatingLegPaymentSchedule, irs.calculation.floatingLegPaymentSchedule)
@ -595,8 +596,7 @@ class InterestRateSwap() : ClauseVerifier() {
val changedRates = paymentDifferences.single().second // Ignore the date of the changed rate (we checked that earlier). val changedRates = paymentDifferences.single().second // Ignore the date of the changed rate (we checked that earlier).
val (oldFloatingRatePaymentEvent, newFixedRatePaymentEvent) = changedRates val (oldFloatingRatePaymentEvent, newFixedRatePaymentEvent) = changedRates
val fixCommand = tx.commands.requireSingleCommand<com.r3corda.core.contracts.Fix>() val fixValue = command.value.fix
val fixValue = fixCommand.value
// Need to check that everything is the same apart from the new fixed rate entry. // Need to check that everything is the same apart from the new fixed rate entry.
requireThat { 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 parties are constant" by (irs.fixedLeg.fixedRatePayer == prevIrs.fixedLeg.fixedRatePayer) // Although superseded by the below test, this is included for a regression issue
@ -618,6 +618,7 @@ class InterestRateSwap() : ClauseVerifier() {
return setOf(command.value) return setOf(command.value)
} }
} }
class Pay : AbstractIRSClause() { class Pay : AbstractIRSClause() {
override val requiredCommands: Set<Class<out CommandData>> override val requiredCommands: Set<Class<out CommandData>>
get() = setOf(Commands.Pay::class.java) get() = setOf(Commands.Pay::class.java)
@ -634,6 +635,7 @@ class InterestRateSwap() : ClauseVerifier() {
return setOf(command.value) return setOf(command.value)
} }
} }
class Mature : AbstractIRSClause() { class Mature : AbstractIRSClause() {
override val requiredCommands: Set<Class<out CommandData>> override val requiredCommands: Set<Class<out CommandData>>
get() = setOf(Commands.Mature::class.java) get() = setOf(Commands.Mature::class.java)
@ -656,7 +658,7 @@ class InterestRateSwap() : ClauseVerifier() {
} }
interface Commands : CommandData { interface Commands : CommandData {
class Fix : TypeOnlyCommandData(), Commands // Receive interest rate from oracle, Both sides agree data class Refix(val fix: Fix) : Commands // Receive interest rate from oracle, Both sides agree
class Pay : TypeOnlyCommandData(), Commands // Not implemented just yet class Pay : TypeOnlyCommandData(), Commands // Not implemented just yet
class Agree : TypeOnlyCommandData(), Commands // Both sides agree to trade 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? class Mature : TypeOnlyCommandData(), Commands // Trade has matured; no more actions. Cleanup. // TODO: Do we need this?
@ -713,7 +715,7 @@ class InterestRateSwap() : ClauseVerifier() {
override fun generateAgreement(notary: Party): TransactionBuilder = InterestRateSwap().generateAgreement(floatingLeg, fixedLeg, calculation, common, notary) override fun generateAgreement(notary: Party): TransactionBuilder = InterestRateSwap().generateAgreement(floatingLeg, fixedLeg, calculation, common, notary)
override fun generateFix(ptx: TransactionBuilder, oldState: StateAndRef<*>, fix: Fix) { override fun generateFix(ptx: TransactionBuilder, oldState: StateAndRef<*>, fix: Fix) {
InterestRateSwap().generateFix(ptx, StateAndRef(TransactionState(this, oldState.state.notary), oldState.ref), Pair(fix.of.forDay, Rate(RatioUnit(fix.value)))) InterestRateSwap().generateFix(ptx, StateAndRef(TransactionState(this, oldState.state.notary), oldState.ref), fix)
} }
override fun nextFixingOf(): FixOf? { override fun nextFixingOf(): FixOf? {
@ -732,9 +734,9 @@ class InterestRateSwap() : ClauseVerifier() {
fun evaluateCalculation(businessDate: LocalDate, expression: Expression = calculation.expression): Any { fun evaluateCalculation(businessDate: LocalDate, expression: Expression = calculation.expression): Any {
// TODO: Jexl is purely for prototyping. It may be replaced // TODO: Jexl is purely for prototyping. It may be replaced
// TODO: Whatever we do use must be secure and sandboxed // TODO: Whatever we do use must be secure and sandboxed
var jexl = JexlBuilder().create() val jexl = JexlBuilder().create()
var expr = jexl.createExpression(expression.expr) val expr = jexl.createExpression(expression.expr)
var jc = MapContext() val jc = MapContext()
jc.set("fixedLeg", fixedLeg) jc.set("fixedLeg", fixedLeg)
jc.set("floatingLeg", floatingLeg) jc.set("floatingLeg", floatingLeg)
jc.set("calculation", calculation) jc.set("calculation", calculation)
@ -783,12 +785,12 @@ class InterestRateSwap() : ClauseVerifier() {
floatingLeg.rollConvention, floatingLeg.rollConvention,
endDate = floatingLeg.terminationDate) endDate = floatingLeg.terminationDate)
var floatingLegPaymentSchedule: MutableMap<LocalDate, FloatingRatePaymentEvent> = HashMap() val floatingLegPaymentSchedule: MutableMap<LocalDate, FloatingRatePaymentEvent> = HashMap()
periodStartDate = floatingLeg.effectiveDate periodStartDate = floatingLeg.effectiveDate
// Now create a schedule for the floating and fixes. // Now create a schedule for the floating and fixes.
for (periodEndDate in dates) { for (periodEndDate in dates) {
val paymentDate = BusinessCalendar.getOffsetDate(periodEndDate, Frequency.Daily, floatingLeg.paymentDelay) val paymentDate = BusinessCalendar.getOffsetDate(periodEndDate, Frequency.Daily, floatingLeg.paymentDelay)
val paymentEvent = FloatingRatePaymentEvent( val paymentEvent = FloatingRatePaymentEvent(
paymentDate, paymentDate,
periodStartDate, periodStartDate,
@ -818,13 +820,13 @@ class InterestRateSwap() : ClauseVerifier() {
} }
} }
// TODO: Replace with rates oracle fun generateFix(tx: TransactionBuilder, irs: StateAndRef<State>, fixing: Fix) {
fun generateFix(tx: TransactionBuilder, irs: StateAndRef<State>, fixing: Pair<LocalDate, Rate>) {
tx.addInputState(irs) tx.addInputState(irs)
val fixedRate = FixedRate(RatioUnit(fixing.value))
tx.addOutputState( tx.addOutputState(
irs.state.data.copy(calculation = irs.state.data.calculation.applyFixing(fixing.first, FixedRate(fixing.second))), irs.state.data.copy(calculation = irs.state.data.calculation.applyFixing(fixing.of.forDay, fixedRate)),
irs.state.notary irs.state.notary
) )
tx.addCommand(Commands.Fix(), listOf(irs.state.data.floatingLeg.floatingRatePayer.owningKey, irs.state.data.fixedLeg.fixedRatePayer.owningKey)) tx.addCommand(Commands.Refix(fixing), listOf(irs.state.data.floatingLeg.floatingRatePayer.owningKey, irs.state.data.fixedLeg.fixedRatePayer.owningKey))
} }
} }

View File

@ -11,21 +11,10 @@ import java.util.*
/** /**
* A utility class to prevent the various mixups between percentages, decimals, bips etc. * A utility class to prevent the various mixups between percentages, decimals, bips etc.
*/ */
open class RatioUnit(value: BigDecimal) { // TODO: Discuss this type open class RatioUnit(val value: BigDecimal) { // TODO: Discuss this type
val value = value override fun equals(other: Any?) = (other as? RatioUnit)?.value == value
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other?.javaClass != javaClass) return false
other as RatioUnit
if (value != other.value) return false
return true
}
override fun hashCode() = value.hashCode() override fun hashCode() = value.hashCode()
override fun toString() = value.toString()
} }
/** /**
@ -63,21 +52,16 @@ open class Rate(val ratioUnit: RatioUnit? = null) {
* for equality. * for equality.
*/ */
override fun hashCode() = ratioUnit?.hashCode() ?: 0 override fun hashCode() = ratioUnit?.hashCode() ?: 0
override fun toString() = ratioUnit.toString()
} }
/** /**
* A very basic subclass to represent a fixed rate. * A very basic subclass to represent a fixed rate.
*/ */
class FixedRate(ratioUnit: RatioUnit) : Rate(ratioUnit) { class FixedRate(ratioUnit: RatioUnit) : Rate(ratioUnit) {
constructor(otherRate: Rate) : this(ratioUnit = otherRate.ratioUnit!!)
override fun toString(): String = "$ratioUnit"
fun isPositive(): Boolean = ratioUnit!!.value > BigDecimal("0.0") fun isPositive(): Boolean = ratioUnit!!.value > BigDecimal("0.0")
override fun equals(other: Any?) = other?.javaClass == javaClass && super.equals(other) override fun equals(other: Any?) = other?.javaClass == javaClass && super.equals(other)
override fun hashCode() = super.hashCode() override fun hashCode() = super.hashCode()
} }

View File

@ -294,14 +294,16 @@ class IRSTests {
@Test @Test
fun generateIRSandFixSome() { fun generateIRSandFixSome() {
var previousTXN = generateIRSTxn(1) var previousTXN = generateIRSTxn(1)
var currentIRS = previousTXN.outputs.map { it.data }.filterIsInstance<InterestRateSwap.State>().single() fun currentIRS() = previousTXN.outputs.map { it.data }.filterIsInstance<InterestRateSwap.State>().single()
println(currentIRS.prettyPrint())
val txns = HashSet<LedgerTransaction>()
txns += previousTXN
while (true) { while (true) {
val nextFixingDate = currentIRS.calculation.nextFixingDate() ?: break val nextFix: FixOf = currentIRS().nextFixingOf() ?: break
println("\n\n\n ***** Applying a fixing to $nextFixingDate \n\n\n")
val fixTX: LedgerTransaction = run { val fixTX: LedgerTransaction = run {
val tx = TransactionType.General.Builder() val tx = TransactionType.General.Builder()
val fixing = Pair(nextFixingDate, FixedRate("0.052".percent)) val fixing = Fix(nextFix, "0.052".percent.value)
InterestRateSwap().generateFix(tx, previousTXN.outRef(0), fixing) InterestRateSwap().generateFix(tx, previousTXN.outRef(0), fixing)
with(tx) { with(tx) {
setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds) setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds)
@ -311,10 +313,10 @@ class IRSTests {
} }
tx.toSignedTransaction().verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, attachments) tx.toSignedTransaction().verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, attachments)
} }
currentIRS = previousTXN.outputs.map { it.data }.filterIsInstance<InterestRateSwap.State>().single()
println(currentIRS.prettyPrint())
previousTXN = fixTX previousTXN = fixTX
txns += fixTX
} }
TransactionGroup(txns, emptySet()).verify()
} }
// Move these later as they aren't IRS specific. // Move these later as they aren't IRS specific.
@ -385,10 +387,7 @@ class IRSTests {
) )
} }
command(ORACLE_PUBKEY) { command(ORACLE_PUBKEY) {
InterestRateSwap.Commands.Fix() InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd))
}
command(ORACLE_PUBKEY) {
Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)
} }
timestamp(TEST_TX_TIME) timestamp(TEST_TX_TIME)
this.verifies() this.verifies()
@ -580,32 +579,25 @@ class IRSTests {
// Templated tweak for reference. A corrent fixing applied should be ok // Templated tweak for reference. A corrent fixing applied should be ok
tweak { tweak {
command(ORACLE_PUBKEY) { command(ORACLE_PUBKEY) {
InterestRateSwap.Commands.Fix() InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd))
} }
timestamp(TEST_TX_TIME) timestamp(TEST_TX_TIME)
command(ORACLE_PUBKEY) {
Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)
}
output() { newIRS } output() { newIRS }
this.verifies() this.verifies()
} }
// This test makes sure that verify confirms the fixing was applied and there is a difference in the old and new // This test makes sure that verify confirms the fixing was applied and there is a difference in the old and new
tweak { tweak {
command(ORACLE_PUBKEY) { InterestRateSwap.Commands.Fix() } command(ORACLE_PUBKEY) { InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)) }
timestamp(TEST_TX_TIME) timestamp(TEST_TX_TIME)
command(ORACLE_PUBKEY) { Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd) }
output() { oldIRS } output() { oldIRS }
this `fails with` "There is at least one difference in the IRS floating leg payment schedules" 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) // This tests tries to sneak in a change to another fixing (which may or may not be the latest one)
tweak { tweak {
command(ORACLE_PUBKEY) { InterestRateSwap.Commands.Fix() } command(ORACLE_PUBKEY) { InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)) }
timestamp(TEST_TX_TIME) timestamp(TEST_TX_TIME)
command(ORACLE_PUBKEY) {
Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)
}
val firstResetKey = newIRS.calculation.floatingLegPaymentSchedule.keys.first() val firstResetKey = newIRS.calculation.floatingLegPaymentSchedule.keys.first()
val firstResetValue = newIRS.calculation.floatingLegPaymentSchedule[firstResetKey] val firstResetValue = newIRS.calculation.floatingLegPaymentSchedule[firstResetKey]
@ -625,9 +617,8 @@ class IRSTests {
// This tests modifies the payment currency for the fixing // This tests modifies the payment currency for the fixing
tweak { tweak {
command(ORACLE_PUBKEY) { InterestRateSwap.Commands.Fix() } command(ORACLE_PUBKEY) { InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)) }
timestamp(TEST_TX_TIME) timestamp(TEST_TX_TIME)
command(ORACLE_PUBKEY) { Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd) }
val latestReset = newIRS.calculation.floatingLegPaymentSchedule.filter { it.value.rate is FixedRate }.maxBy { it.key } 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"))) val modifiedLatestResetValue = latestReset!!.value.copy(notional = Amount(latestReset.value.notional.quantity, Currency.getInstance("JPY")))
@ -711,10 +702,7 @@ class IRSTests {
} }
command(ORACLE_PUBKEY) { command(ORACLE_PUBKEY) {
InterestRateSwap.Commands.Fix() InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld1, Tenor("3M")), bd1))
}
command(ORACLE_PUBKEY) {
Fix(FixOf("ICE LIBOR", ld1, Tenor("3M")), bd1)
} }
timestamp(TEST_TX_TIME) timestamp(TEST_TX_TIME)
this.verifies() this.verifies()