Utility functions / classes / enums that will assist in the writing of some financial products

This commit is contained in:
Richard Green 2016-03-14 15:47:14 +00:00
parent bc5f29c5ee
commit 966724f941
5 changed files with 283 additions and 1 deletions

View File

@ -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")
}
}

View File

@ -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

View 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

View 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

View 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))
}
}