mirror of
https://github.com/corda/corda.git
synced 2024-12-19 13:08:04 +00:00
Merged in mike-fix-irs (pull request #244)
Fix how the IRS contract uses fix commands
This commit is contained in:
commit
5167ed7df2
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user