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)
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 {
if (this === other) return true
@ -114,8 +114,7 @@ class FixedRatePaymentEvent(date: LocalDate,
val CSVHeader = RatePaymentEvent.CSVHeader
}
override val flow: Amount<Currency> get() =
Amount<Currency>(dayCountFactor.times(BigDecimal(notional.quantity)).times(rate.ratioUnit!!.value).toLong(), notional.token)
override val flow: Amount<Currency> 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"
@ -140,13 +139,13 @@ class FloatingRatePaymentEvent(date: LocalDate,
override val flow: Amount<Currency> get() {
// TODO: Should an uncalculated amount return a zero ? null ? etc.
val v = rate.ratioUnit?.value ?: return Amount<Currency>(0, notional.token)
return Amount<Currency>(dayCountFactor.times(BigDecimal(notional.quantity)).times(v).toLong(), notional.token)
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"
override fun asCSV(): String = "$accrualStartDate,$accrualEndDate,$dayCountFactor,$days,$date,${notional.token},$notional,$fixingDate,$rate,$flow"
/**
* Used for making immutables.
@ -450,7 +449,7 @@ class InterestRateSwap() : ClauseVerifier() {
override val clauses: List<SingleClause> = listOf(Clause.Timestamped(), Clause.Group())
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 {
/**
@ -498,8 +497,9 @@ class InterestRateSwap() : ClauseVerifier() {
fun getFloatingLegPaymentsDifferences(payments1: Map<LocalDate, Event>, payments2: Map<LocalDate, Event>): List<Pair<LocalDate, Pair<FloatingRatePaymentEvent, FloatingRatePaymentEvent>>> {
val diff1 = payments1.filter { payments1[it.key] != payments2[it.key] }
val diff2 = payments2.filter { payments1[it.key] != payments2[it.key] }
val ret = (diff1.keys + diff2.keys).map { it to Pair(diff1.get(it) as FloatingRatePaymentEvent, diff2.get(it) as FloatingRatePaymentEvent) }
return ret
return (diff1.keys + diff2.keys).map {
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
return tx.groupStates() { state: InterestRateSwap.State -> state.common.tradeID }
}
override val clauses: List<GroupClause<State, String>>
get() = listOf(Agree(), Fix(), Pay(), Mature())
}
@ -530,12 +531,11 @@ class InterestRateSwap() : ClauseVerifier() {
// derived as the correct notary
@Suppress("DEPRECATION")
val command = tx.commands.getTimestampByName("Mock Company 0", "Notary Service", "Bank A")
val time = command?.midpoint
if (time == null) throw IllegalArgumentException("must be timestamped")
return setOf(command!!)
?: throw IllegalArgumentException("must be timestamped")
return setOf(command)
}
}
class Agree : AbstractIRSClause() {
override val requiredCommands: Set<Class<out CommandData>>
get() = setOf(Commands.Agree::class.java)
@ -572,16 +572,17 @@ class InterestRateSwap() : ClauseVerifier() {
return setOf(command.value)
}
}
class Fix : AbstractIRSClause() {
override val requiredCommands: Set<Class<out CommandData>>
get() = setOf(Commands.Fix::class.java)
get() = setOf(Commands.Refix::class.java)
override fun verify(tx: TransactionForContract,
inputs: List<State>,
outputs: List<State>,
commands: Collection<AuthenticatedObject<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 prevIrs = inputs.filterIsInstance<State>().single()
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 (oldFloatingRatePaymentEvent, newFixedRatePaymentEvent) = changedRates
val fixCommand = tx.commands.requireSingleCommand<com.r3corda.core.contracts.Fix>()
val fixValue = fixCommand.value
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
@ -618,6 +618,7 @@ class InterestRateSwap() : ClauseVerifier() {
return setOf(command.value)
}
}
class Pay : AbstractIRSClause() {
override val requiredCommands: Set<Class<out CommandData>>
get() = setOf(Commands.Pay::class.java)
@ -634,6 +635,7 @@ class InterestRateSwap() : ClauseVerifier() {
return setOf(command.value)
}
}
class Mature : AbstractIRSClause() {
override val requiredCommands: Set<Class<out CommandData>>
get() = setOf(Commands.Mature::class.java)
@ -656,7 +658,7 @@ class InterestRateSwap() : ClauseVerifier() {
}
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 Agree : TypeOnlyCommandData(), Commands // Both sides agree to trade
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 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? {
@ -732,9 +734,9 @@ class InterestRateSwap() : ClauseVerifier() {
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()
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)
@ -783,12 +785,12 @@ class InterestRateSwap() : ClauseVerifier() {
floatingLeg.rollConvention,
endDate = floatingLeg.terminationDate)
var floatingLegPaymentSchedule: MutableMap<LocalDate, FloatingRatePaymentEvent> = HashMap()
val floatingLegPaymentSchedule: MutableMap<LocalDate, FloatingRatePaymentEvent> = 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 paymentDate = BusinessCalendar.getOffsetDate(periodEndDate, Frequency.Daily, floatingLeg.paymentDelay)
val paymentEvent = FloatingRatePaymentEvent(
paymentDate,
periodStartDate,
@ -818,13 +820,13 @@ class InterestRateSwap() : ClauseVerifier() {
}
}
// TODO: Replace with rates oracle
fun generateFix(tx: TransactionBuilder, irs: StateAndRef<State>, fixing: Pair<LocalDate, Rate>) {
fun generateFix(tx: TransactionBuilder, irs: StateAndRef<State>, fixing: Fix) {
tx.addInputState(irs)
val fixedRate = FixedRate(RatioUnit(fixing.value))
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
)
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.
*/
open class RatioUnit(value: BigDecimal) { // TODO: Discuss this type
val 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
}
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()
}
/**
@ -63,21 +52,16 @@ open class Rate(val ratioUnit: RatioUnit? = null) {
* 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) {
constructor(otherRate: Rate) : this(ratioUnit = otherRate.ratioUnit!!)
override fun toString(): String = "$ratioUnit"
fun isPositive(): Boolean = ratioUnit!!.value > BigDecimal("0.0")
override fun equals(other: Any?) = other?.javaClass == javaClass && super.equals(other)
override fun hashCode() = super.hashCode()
}

View File

@ -294,14 +294,16 @@ class IRSTests {
@Test
fun generateIRSandFixSome() {
var previousTXN = generateIRSTxn(1)
var currentIRS = previousTXN.outputs.map { it.data }.filterIsInstance<InterestRateSwap.State>().single()
println(currentIRS.prettyPrint())
fun currentIRS() = previousTXN.outputs.map { it.data }.filterIsInstance<InterestRateSwap.State>().single()
val txns = HashSet<LedgerTransaction>()
txns += previousTXN
while (true) {
val nextFixingDate = currentIRS.calculation.nextFixingDate() ?: break
println("\n\n\n ***** Applying a fixing to $nextFixingDate \n\n\n")
val nextFix: FixOf = currentIRS().nextFixingOf() ?: break
val fixTX: LedgerTransaction = run {
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)
with(tx) {
setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds)
@ -311,10 +313,10 @@ class IRSTests {
}
tx.toSignedTransaction().verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, attachments)
}
currentIRS = previousTXN.outputs.map { it.data }.filterIsInstance<InterestRateSwap.State>().single()
println(currentIRS.prettyPrint())
previousTXN = fixTX
txns += fixTX
}
TransactionGroup(txns, emptySet()).verify()
}
// Move these later as they aren't IRS specific.
@ -385,10 +387,7 @@ class IRSTests {
)
}
command(ORACLE_PUBKEY) {
InterestRateSwap.Commands.Fix()
}
command(ORACLE_PUBKEY) {
Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)
InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd))
}
timestamp(TEST_TX_TIME)
this.verifies()
@ -580,32 +579,25 @@ class IRSTests {
// Templated tweak for reference. A corrent fixing applied should be ok
tweak {
command(ORACLE_PUBKEY) {
InterestRateSwap.Commands.Fix()
InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd))
}
timestamp(TEST_TX_TIME)
command(ORACLE_PUBKEY) {
Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)
}
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.Fix() }
command(ORACLE_PUBKEY) { InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)) }
timestamp(TEST_TX_TIME)
command(ORACLE_PUBKEY) { Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd) }
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.Fix() }
command(ORACLE_PUBKEY) { InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)) }
timestamp(TEST_TX_TIME)
command(ORACLE_PUBKEY) {
Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)
}
val firstResetKey = newIRS.calculation.floatingLegPaymentSchedule.keys.first()
val firstResetValue = newIRS.calculation.floatingLegPaymentSchedule[firstResetKey]
@ -625,9 +617,8 @@ class IRSTests {
// This tests modifies the payment currency for the fixing
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)
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 modifiedLatestResetValue = latestReset!!.value.copy(notional = Amount(latestReset.value.notional.quantity, Currency.getInstance("JPY")))
@ -711,10 +702,7 @@ class IRSTests {
}
command(ORACLE_PUBKEY) {
InterestRateSwap.Commands.Fix()
}
command(ORACLE_PUBKEY) {
Fix(FixOf("ICE LIBOR", ld1, Tenor("3M")), bd1)
InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld1, Tenor("3M")), bd1))
}
timestamp(TEST_TX_TIME)
this.verifies()