mirror of
https://github.com/corda/corda.git
synced 2025-01-21 03:55:00 +00:00
Utility functions / classes / enums that will assist in the writing of some financial products
This commit is contained in:
parent
bc5f29c5ee
commit
966724f941
@ -9,8 +9,10 @@
|
||||
package core
|
||||
|
||||
import java.math.BigDecimal
|
||||
import java.time.DayOfWeek
|
||||
import java.time.Duration
|
||||
import java.time.LocalDate
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
@ -77,3 +79,189 @@ fun Iterable<Amount>.sumOrZero(currency: Currency) = if (iterator().hasNext()) s
|
||||
data class FixOf(val name: String, val forDay: LocalDate, val ofTenor: Duration)
|
||||
/** A [Fix] represents a named interest rate, on a given day, for a given duration. It can be embedded in a tx. */
|
||||
data class Fix(val of: FixOf, val value: BigDecimal) : CommandData
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Placeholder class for the Tenor datatype - which is a standardised duration of time until maturity */
|
||||
data class Tenor(var name:String)
|
||||
|
||||
/** Simple enum for returning accurals adjusted or unadjusted.
|
||||
* We don't actually do anything with this yet though, so it's ignored for now.
|
||||
*/
|
||||
enum class AccrualAdjustment {
|
||||
Adjusted,Unadjusted
|
||||
}
|
||||
|
||||
/** This is utilised in the [DateRollConvention] class to determine which way we should initially step when
|
||||
* finding a business day
|
||||
*/
|
||||
enum class DateRollDirection(val value: Long) { FORWARD(1), BACKWARD(-1) }
|
||||
|
||||
/** This reflects what happens if a date on which a business event is supposed to happen actually falls upon a non-working day
|
||||
* Depending on the accounting requirement, we can move forward until we get to a business day, or backwards
|
||||
* There are some additional rules which are explained in the individual cases below
|
||||
*/
|
||||
enum class DateRollConvention {
|
||||
// direction() cannot be a val due to the throw in the Actual instance
|
||||
Actual { // Don't roll the date, use the one supplied.
|
||||
override fun direction(): DateRollDirection = throw UnsupportedOperationException("Direction is not relevant for convention Actual")
|
||||
override val isModified: Boolean = false
|
||||
},
|
||||
Following { // Following is the next business date from this one.
|
||||
override fun direction(): DateRollDirection = DateRollDirection.FORWARD
|
||||
override val isModified: Boolean = false
|
||||
},
|
||||
ModifiedFollowing { // Modified following is the next business date, unless it's in the next month, in which case use the preceeding business date
|
||||
override fun direction(): DateRollDirection = DateRollDirection.FORWARD
|
||||
override val isModified: Boolean = true
|
||||
},
|
||||
Previous { // Previous is the previous business date from this one
|
||||
override fun direction(): DateRollDirection = DateRollDirection.BACKWARD
|
||||
override val isModified: Boolean = false
|
||||
},
|
||||
ModifiedPrevious { // Modified previous is the previous business date, unless it's in the previous month, in which case use the next business date
|
||||
override fun direction(): DateRollDirection = DateRollDirection.BACKWARD
|
||||
override val isModified: Boolean = true
|
||||
};
|
||||
|
||||
abstract fun direction(): DateRollDirection
|
||||
abstract val isModified: Boolean
|
||||
}
|
||||
|
||||
|
||||
/** This forms the day part of the "Day Count Basis" used for interest calculation
|
||||
*/
|
||||
enum class DayCountBasisDay { // We have to prefix 30 etc with a letter due to enum naming constraints.
|
||||
D30, D30N, D30P, D30E, D30G, Actual, ActualJ, D30Z, D30F, Bus_SaoPaulo
|
||||
}
|
||||
|
||||
/** This forms the year part of the "Day Count Basis" used for interest calculation
|
||||
*/
|
||||
enum class DayCountBasisYear { // Ditto above comment for years.
|
||||
Y360, Y365F, Y365L, Y365Q, Y366, Actual, ActualA, Y365B, Y365, ISMA, ICMA, Y252
|
||||
}
|
||||
|
||||
/** Should the payment be made in advance or in arrears */
|
||||
enum class PaymentRule {
|
||||
InAdvance, InArrears,
|
||||
}
|
||||
|
||||
/** Date offset that the fixing is done prior to the accrual start date
|
||||
* Currently not used in the calculation
|
||||
*/
|
||||
enum class DateOffset { // TODO: Definitely shouldn't be an enum, but let's leave it for now at T-2 is a convention.
|
||||
ZERO, TWODAYS,
|
||||
}
|
||||
|
||||
|
||||
/** Frequency in which an event occurs - the enumerator also casts to an integer specifying the number of times per year
|
||||
* that would divide into (eg annually = 1, semiannual = 2, monthly = 12 etc)
|
||||
*/
|
||||
enum class Frequency(val annualCompoundCount:Int) {
|
||||
Annual(1) {
|
||||
override fun offset(d: LocalDate) = d.plusYears(1)
|
||||
},
|
||||
SemiAnnual(2) {
|
||||
override fun offset(d: LocalDate) = d.plusMonths(6)
|
||||
},
|
||||
Quarterly(4) {
|
||||
override fun offset(d: LocalDate) = d.plusMonths(3)
|
||||
},
|
||||
Monthly(12) {
|
||||
override fun offset(d: LocalDate) = d.plusMonths(1)
|
||||
},
|
||||
Weekly(52) {
|
||||
override fun offset(d: LocalDate) = d.plusWeeks(1)
|
||||
},
|
||||
BiWeekly(26) {
|
||||
override fun offset(d: LocalDate) = d.plusWeeks(2)
|
||||
};
|
||||
abstract fun offset(d: LocalDate): LocalDate
|
||||
// Daily() // Let's not worry about this for now.
|
||||
}
|
||||
|
||||
|
||||
fun LocalDate.isWorkingDay(accordingToCalendar: BusinessCalendar): Boolean = accordingToCalendar.isWorkingDay(this)
|
||||
|
||||
// TODO: Make Calendar data come from an oracle
|
||||
open class BusinessCalendar private constructor(val holidayDates: List<LocalDate>) {
|
||||
class UnknownCalendar(calname: String): Exception("$calname not found")
|
||||
companion object {
|
||||
val calendars = listOf("London","NewYork")
|
||||
val TEST_CALENDAR_DATA = calendars.map { it to BusinessCalendar::class.java.getResourceAsStream("${it}HolidayCalendar.txt").bufferedReader().readText() }.toMap()
|
||||
fun parseDateFromString(it: String) = LocalDate.parse(it, DateTimeFormatter.ISO_LOCAL_DATE)
|
||||
fun getInstance(vararg calname: String): BusinessCalendar = // This combines multiple calendars into one list of holiday dates.
|
||||
BusinessCalendar( calname.flatMap { (TEST_CALENDAR_DATA.get(it) ?: throw UnknownCalendar(it)).split(",") }.toSet().map{ parseDateFromString(it) }.toList())
|
||||
|
||||
fun createGenericSchedule(startDate: LocalDate,
|
||||
period: Frequency,
|
||||
calendar: BusinessCalendar = BusinessCalendar.getInstance(),
|
||||
dateRollConvention: DateRollConvention = DateRollConvention.Following,
|
||||
noOfAdditionalPeriods: Int = Integer.MAX_VALUE,
|
||||
endDate: LocalDate? = null,
|
||||
periodOffset: Int? = null): List<LocalDate> {
|
||||
val ret = ArrayList<LocalDate>()
|
||||
var ctr = 0
|
||||
var currentDate = startDate
|
||||
|
||||
while (true) {
|
||||
currentDate = period.offset(currentDate)
|
||||
var scheduleDate = currentDate
|
||||
scheduleDate = calendar.applyRollConvention(scheduleDate, dateRollConvention)
|
||||
|
||||
if ((periodOffset == null) || (periodOffset <= ctr)) {
|
||||
ret.add(scheduleDate)
|
||||
}
|
||||
ctr += 1
|
||||
if ((ctr > noOfAdditionalPeriods ) || (currentDate >= endDate ?: currentDate )) { // TODO: Fix addl period logic
|
||||
break
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
}
|
||||
|
||||
open fun isWorkingDay(date: LocalDate): Boolean =
|
||||
when {
|
||||
date.dayOfWeek == DayOfWeek.SATURDAY -> false
|
||||
date.dayOfWeek == DayOfWeek.SUNDAY -> false
|
||||
holidayDates.contains(date) -> false
|
||||
else -> true
|
||||
}
|
||||
|
||||
open fun applyRollConvention(testDate: LocalDate, dateRollConvention: DateRollConvention): LocalDate {
|
||||
if (dateRollConvention == DateRollConvention.Actual) return testDate
|
||||
|
||||
var direction = dateRollConvention.direction().value
|
||||
var trialDate = testDate
|
||||
while (! isWorkingDay(trialDate)) {
|
||||
trialDate = trialDate.plusDays(direction)
|
||||
}
|
||||
|
||||
// We've moved to the next working day in the right direction, but if we're using the "modified" date roll
|
||||
// convention and we've crossed into another month, reverse the direction instead to stay within the month.
|
||||
// Probably better explained here: http://www.investopedia.com/terms/m/modifiedfollowing.asp
|
||||
|
||||
if (dateRollConvention.isModified && testDate.month != trialDate.month) {
|
||||
direction = -direction
|
||||
trialDate = testDate
|
||||
while (! isWorkingDay(trialDate)) {
|
||||
trialDate = trialDate.plusDays(direction)
|
||||
}
|
||||
}
|
||||
return trialDate
|
||||
}
|
||||
}
|
||||
|
||||
fun dayCountCalculator(startDate: LocalDate, endDate: LocalDate,
|
||||
dcbYear: DayCountBasisYear,
|
||||
dcbDay: DayCountBasisDay): BigDecimal {
|
||||
// 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)
|
||||
else -> TODO("Can't calculate days using convention $dcbDay / $dcbYear")
|
||||
}
|
||||
}
|
||||
|
@ -33,7 +33,10 @@ val Int.hours: Duration get() = Duration.ofHours(this.toLong())
|
||||
val Int.minutes: Duration get() = Duration.ofMinutes(this.toLong())
|
||||
val Int.seconds: Duration get() = Duration.ofSeconds(this.toLong())
|
||||
|
||||
val String.d: BigDecimal get() = BigDecimal(this)
|
||||
val Int.bd: BigDecimal get() = BigDecimal(this)
|
||||
val Double.bd: BigDecimal get() = BigDecimal(this)
|
||||
val String.bd: BigDecimal get() = BigDecimal(this)
|
||||
val Long.bd: BigDecimal get() = BigDecimal(this)
|
||||
|
||||
/**
|
||||
* Returns a random positive long generated using a secure RNG. This function sacrifies a bit of entropy in order to
|
||||
|
1
core/src/main/resources/core/LondonHolidayCalendar.txt
Normal file
1
core/src/main/resources/core/LondonHolidayCalendar.txt
Normal file
@ -0,0 +1 @@
|
||||
2015-01-01,2015-04-03,2015-04-06,2015-05-04,2015-05-25,2015-08-31,2015-12-25,2015-12-28,2016-01-01,2016-03-25,2016-03-28,2016-05-02,2016-05-30,2016-08-29,2016-12-26,2016-12-27,2017-01-02,2017-04-14,2017-04-17,2017-05-01,2017-05-29,2017-08-28,2017-12-25,2017-12-26,2018-01-01,2018-03-30,2018-04-02,2018-05-07,2018-05-28,2018-08-27,2018-12-25,2018-12-26,2019-01-01,2019-04-19,2019-04-22,2019-05-06,2019-05-27,2019-08-26,2019-12-25,2019-12-26,2020-01-01,2020-04-10,2020-04-13,2020-05-04,2020-05-25,2020-08-31,2020-12-25,2020-12-28,2021-01-01,2021-04-02,2021-04-05,2021-05-03,2021-05-31,2021-08-30,2021-12-27,2021-12-28,2022-01-03,2022-04-15,2022-04-18,2022-05-02,2022-05-30,2022-08-29,2022-12-26,2022-12-27,2023-01-02,2023-04-07,2023-04-10,2023-05-01,2023-05-29,2023-08-28,2023-12-25,2023-12-26,2024-01-01,2024-03-29,2024-04-01,2024-05-06,2024-05-27,2024-08-26,2024-12-25,2024-12-26
|
1
core/src/main/resources/core/NewYorkHolidayCalendar.txt
Normal file
1
core/src/main/resources/core/NewYorkHolidayCalendar.txt
Normal file
@ -0,0 +1 @@
|
||||
2015-01-01,2015-01-19,2015-02-16,2015-02-18,2015-05-25,2015-07-03,2015-09-07,2015-10-12,2015-11-11,2015-11-26,2015-12-25,2016-01-01,2016-01-18,2016-02-10,2016-02-15,2016-05-30,2016-07-04,2016-09-05,2016-10-10,2016-11-11,2016-11-24,2016-12-26,2017-01-02,2017-01-16,2017-02-20,2017-03-01,2017-05-29,2017-07-04,2017-09-04,2017-10-09,2017-11-10,2017-11-23,2017-12-25,2018-01-01,2018-01-15,2018-02-14,2018-02-19,2018-05-28,2018-07-04,2018-09-03,2018-10-08,2018-11-12,2018-11-22,2018-12-25,2019-01-01,2019-01-21,2019-02-18,2019-03-06,2019-05-27,2019-07-04,2019-09-02,2019-10-14,2019-11-11,2019-11-28,2019-12-25,2020-01-01,2020-01-20,2020-02-17,2020-02-26,2020-05-25,2020-07-03,2020-09-07,2020-10-12,2020-11-11,2020-11-26,2020-12-25,2021-01-01,2021-01-18,2021-02-15,2021-02-17,2021-05-31,2021-07-05,2021-09-06,2021-10-11,2021-11-11,2021-11-25,2021-12-24,2022-01-17,2022-02-21,2022-03-02,2022-05-30,2022-07-04,2022-09-05,2022-10-10,2022-11-11,2022-11-24,2022-12-26,2023-01-02,2023-01-16,2023-02-20,2023-02-22,2023-05-29,2023-07-04,2023-09-04,2023-10-09,2023-11-10,2023-11-23,2023-12-25,2024-01-01,2024-01-15,2024-02-14,2024-02-19,2024-05-27,2024-07-04,2024-09-02,2024-10-14,2024-11-11,2024-11-28,2024-12-25
|
89
core/src/test/kotlin/core/FinanceTypesTest.kt
Normal file
89
core/src/test/kotlin/core/FinanceTypesTest.kt
Normal file
@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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 core
|
||||
|
||||
import org.junit.Test
|
||||
import java.time.LocalDate
|
||||
|
||||
class FinanceTypesTest {
|
||||
|
||||
@Test
|
||||
fun `schedule generator 1`() {
|
||||
var ret = BusinessCalendar.createGenericSchedule(startDate = LocalDate.of(2014, 11, 25), period = Frequency.Monthly, noOfAdditionalPeriods = 3)
|
||||
// We know that Jan 25th 2015 is on the weekend -> It should not be in this list returned.
|
||||
assert(! (LocalDate.of(2015,1,25) in ret))
|
||||
println(ret)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `schedule generator 2`() {
|
||||
var ret = BusinessCalendar.createGenericSchedule(startDate = LocalDate.of(2015, 11, 25), period = Frequency.Monthly, noOfAdditionalPeriods = 3, calendar = BusinessCalendar.getInstance("London"), dateRollConvention = DateRollConvention.Following)
|
||||
// Xmas should not be in the list!
|
||||
assert(! (LocalDate.of(2015,12,25) in ret))
|
||||
println(ret)
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `create a UK calendar` () {
|
||||
val cal = BusinessCalendar.getInstance("London")
|
||||
val holdates = cal.holidayDates
|
||||
println(holdates)
|
||||
assert(LocalDate.of(2016,12,27) in holdates) // Christmas this year is at the weekend...
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create a US UK calendar`() {
|
||||
val cal = BusinessCalendar.getInstance("London","NewYork")
|
||||
assert(LocalDate.of(2016,7,4) in cal.holidayDates) // The most American of holidays
|
||||
assert(LocalDate.of(2016,8,29) in cal.holidayDates) // August Bank Holiday for brits only
|
||||
println("Calendar contains both US and UK holidays")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calendar test of modified following` () {
|
||||
val ldn = BusinessCalendar.getInstance("London")
|
||||
val result = ldn.applyRollConvention(LocalDate.of(2016,12,25),DateRollConvention.ModifiedFollowing)
|
||||
assert(result == LocalDate.of(2016,12,28))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calendar test of modified following pt 2` () {
|
||||
val ldn = BusinessCalendar.getInstance("London")
|
||||
val result = ldn.applyRollConvention(LocalDate.of(2016,12,31),DateRollConvention.ModifiedFollowing)
|
||||
assert(result == LocalDate.of(2016,12,30))
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `calendar test of modified previous` () {
|
||||
val ldn = BusinessCalendar.getInstance("London")
|
||||
val result = ldn.applyRollConvention(LocalDate.of(2016,1,1),DateRollConvention.ModifiedPrevious)
|
||||
assert(result == LocalDate.of(2016,1,4))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calendar test of previous` () {
|
||||
val ldn = BusinessCalendar.getInstance("London")
|
||||
val result = ldn.applyRollConvention(LocalDate.of(2016,12,25),DateRollConvention.Previous)
|
||||
assert(result == LocalDate.of(2016,12,23))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calendar test of following` () {
|
||||
val ldn = BusinessCalendar.getInstance("London")
|
||||
val result = ldn.applyRollConvention(LocalDate.of(2016,12,25),DateRollConvention.Following)
|
||||
assert(result == LocalDate.of(2016,12,28))
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user