diff --git a/core/src/main/kotlin/core/FinanceTypes.kt b/core/src/main/kotlin/core/FinanceTypes.kt index 18aaf52ee8..1ae88a0962 100644 --- a/core/src/main/kotlin/core/FinanceTypes.kt +++ b/core/src/main/kotlin/core/FinanceTypes.kt @@ -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.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) { + 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 { + val ret = ArrayList() + 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") + } +} diff --git a/core/src/main/kotlin/core/Utils.kt b/core/src/main/kotlin/core/Utils.kt index 59243ac333..d4cff7c637 100644 --- a/core/src/main/kotlin/core/Utils.kt +++ b/core/src/main/kotlin/core/Utils.kt @@ -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 diff --git a/core/src/main/resources/core/LondonHolidayCalendar.txt b/core/src/main/resources/core/LondonHolidayCalendar.txt new file mode 100644 index 0000000000..a68eaa7b30 --- /dev/null +++ b/core/src/main/resources/core/LondonHolidayCalendar.txt @@ -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 \ No newline at end of file diff --git a/core/src/main/resources/core/NewYorkHolidayCalendar.txt b/core/src/main/resources/core/NewYorkHolidayCalendar.txt new file mode 100644 index 0000000000..e2edfa6902 --- /dev/null +++ b/core/src/main/resources/core/NewYorkHolidayCalendar.txt @@ -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 \ No newline at end of file diff --git a/core/src/test/kotlin/core/FinanceTypesTest.kt b/core/src/test/kotlin/core/FinanceTypesTest.kt new file mode 100644 index 0000000000..9c6a997d19 --- /dev/null +++ b/core/src/test/kotlin/core/FinanceTypesTest.kt @@ -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)) + } + + + + + +} \ No newline at end of file