Skip to content

Commit

Permalink
add maximum duration
Browse files Browse the repository at this point in the history
  • Loading branch information
simontreanor committed May 8, 2024
1 parent 7f40abb commit 1cb01a6
Show file tree
Hide file tree
Showing 21 changed files with 263 additions and 103 deletions.
3 changes: 2 additions & 1 deletion docs/exampleAmortisation.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ let scheduleParameters =
Principal = 1500_00L<Cent>
PaymentSchedule = RegularSchedule (
UnitPeriodConfig = UnitPeriod.Monthly(1, 2022, 11, 31),
PaymentCount = 5
PaymentCount = 5,
MaxDuration = ValueNone
)
FeesAndCharges = {
Fees = [||]
Expand Down
2 changes: 1 addition & 1 deletion docs/exampleAprUs.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ let principal = 5000_00L<Cent>

let transfers =
Monthly (1, 1978, 2, 10)
|> generatePaymentSchedule 24 Direction.Forward
|> generatePaymentSchedule 24 ValueNone Direction.Forward
|> Array.map(fun d -> { TransferType = Payment; TransferDate = d; Amount = 230_00L<Cent> })

let aprMethod = CalculationMethod.UsActuarial 4
Expand Down
3 changes: 2 additions & 1 deletion docs/examplePaymentSchedule.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ let scheduleParameters =
Principal = 10000_00L<Cent>
PaymentSchedule = RegularSchedule (
UnitPeriodConfig = UnitPeriod.Monthly(1, 2024, 3, 7),
PaymentCount = 36
PaymentCount = 36,
MaxDuration = ValueNone
)
FeesAndCharges = {
Fees = [||]
Expand Down
2 changes: 1 addition & 1 deletion src/Amortisation.fs
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,7 @@ module Amortisation =
| RegularFixedSchedule regularFixedSchedules ->
regularFixedSchedules
|> Array.map(fun rfs ->
UnitPeriod.generatePaymentSchedule rfs.PaymentCount UnitPeriod.Direction.Forward rfs.UnitPeriodConfig
UnitPeriod.generatePaymentSchedule rfs.PaymentCount ValueNone UnitPeriod.Direction.Forward rfs.UnitPeriodConfig
|> Array.map(fun d ->
{
PaymentDay = OffsetDay.fromDate sp.AsOfDate d
Expand Down
4 changes: 2 additions & 2 deletions src/Apr.fs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ module Apr =
let transferDates = transfers |> Array.map _.TransferDate
let transferCount = transfers |> Array.length
let unitPeriod = transferDates |> UnitPeriod.detect UnitPeriod.Direction.Reverse (UnitPeriod.Month 1)
let schedule = UnitPeriod.generatePaymentSchedule ((transferCount + 1) * multiple) UnitPeriod.Direction.Reverse unitPeriod |> Array.filter(fun d -> d >= termStart)
let schedule = UnitPeriod.generatePaymentSchedule ((transferCount + 1) * multiple) ValueNone UnitPeriod.Direction.Reverse unitPeriod |> Array.filter(fun d -> d >= termStart)
let scheduleCount = schedule |> Array.length
let lastWholeMonthBackIndex = 0
let lastWholeUnitPeriodBackIndex = (scheduleCount - 1) % multiple
Expand Down Expand Up @@ -139,7 +139,7 @@ module Apr =
let transferDates = transfers |> Array.map _.TransferDate
let transferCount = transfers |> Array.length
let frequency = transferDates |> UnitPeriod.detect UnitPeriod.Direction.Reverse UnitPeriod.SemiMonth
let schedule = UnitPeriod.generatePaymentSchedule (transferCount + 2) UnitPeriod.Direction.Reverse frequency |> Array.filter(fun d -> d >= termStart)
let schedule = UnitPeriod.generatePaymentSchedule (transferCount + 2) ValueNone UnitPeriod.Direction.Reverse frequency |> Array.filter(fun d -> d >= termStart)
let scheduleCount = schedule |> Array.length
let offset = scheduleCount - transferCount
[| 0 .. (transferCount - 1) |]
Expand Down
2 changes: 1 addition & 1 deletion src/CustomerPayments.fs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ module CustomerPayments =
[<Struct>]
type CustomerPaymentSchedule =
/// a regular schedule based on a unit-period config with a specific number of payments with an auto-calculated amount
| RegularSchedule of UnitPeriodConfig: UnitPeriod.Config * PaymentCount: int
| RegularSchedule of UnitPeriodConfig: UnitPeriod.Config * PaymentCount: int * MaxDuration: Duration voption
/// a regular schedule based on one or more unit-period configs each with a specific number of payments of a specified amount
| RegularFixedSchedule of RegularFixedSchedule array
/// just a bunch of payments
Expand Down
6 changes: 6 additions & 0 deletions src/DateDay.fs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ module DateDay =
/// a duration of a number of days
[<Measure>] type DurationDay

[<RequireQualifiedAccess; Struct>]
type Duration = {
Length: int<DurationDay>
FromDate: Date
}

/// day of month, bug: specifying 29, 30, or 31 means the dates will track the specific day of the month where
/// possible, otherwise the day will be the last day of the month; so 31 will track the month end; also note that it is
/// possible to start with e.g. (2024, 02, 31) and this will yield 2024-02-29 29 2024-03-31 2024-04-30 etc.
Expand Down
2 changes: 1 addition & 1 deletion src/FSharp.Finance.Personal.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageId>FSharp.Finance.Personal</PackageId>
<Version>0.14.3</Version>
<Version>0.15.0</Version>
<Authors>Simon Treanor</Authors>
<PackageDescription>F# Personal Finance Library</PackageDescription>
<RepositoryUrl>https://github.com/simontreanor/FSharp.Finance.Personal</RepositoryUrl>
Expand Down
6 changes: 3 additions & 3 deletions src/PaymentSchedule.fs
Original file line number Diff line number Diff line change
Expand Up @@ -121,18 +121,18 @@ module PaymentSchedule =
if startDate > unitPeriodConfigStartDate then
[||]
else
generatePaymentSchedule rfs.PaymentCount Direction.Forward rfs.UnitPeriodConfig |> Array.map (OffsetDay.fromDate startDate)
generatePaymentSchedule rfs.PaymentCount ValueNone Direction.Forward rfs.UnitPeriodConfig |> Array.map (OffsetDay.fromDate startDate)
)
|> Array.concat
| RegularSchedule (unitPeriodConfig, paymentCount) ->
| RegularSchedule (unitPeriodConfig, paymentCount, maxDuration) ->
if paymentCount = 0 then
[||]
else
let unitPeriodConfigStartDate = Config.startDate unitPeriodConfig
if startDate > unitPeriodConfigStartDate then
[||]
else
generatePaymentSchedule paymentCount Direction.Forward unitPeriodConfig |> Array.map (OffsetDay.fromDate startDate)
generatePaymentSchedule paymentCount maxDuration Direction.Forward unitPeriodConfig |> Array.map (OffsetDay.fromDate startDate)

/// calculates the number of days between two offset days on which interest is chargeable
let calculate toleranceOption sp =
Expand Down
2 changes: 1 addition & 1 deletion src/Rescheduling.fs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ module Rescheduling =
| RegularFixedSchedule regularFixedSchedules ->
regularFixedSchedules
|> Array.map(fun rfs ->
UnitPeriod.generatePaymentSchedule rfs.PaymentCount UnitPeriod.Direction.Forward rfs.UnitPeriodConfig
UnitPeriod.generatePaymentSchedule rfs.PaymentCount ValueNone UnitPeriod.Direction.Forward rfs.UnitPeriodConfig
|> Array.map(fun d -> { PaymentDay = OffsetDay.fromDate sp.StartDate d; PaymentDetails = ScheduledPayment (ScheduledPaymentType.Rescheduled rfs.PaymentAmount) })
)
|> Array.concat
Expand Down
24 changes: 14 additions & 10 deletions src/UnitPeriod.fs
Original file line number Diff line number Diff line change
Expand Up @@ -196,14 +196,14 @@ module UnitPeriod =
| invalidConfig -> fix invalidConfig

/// generates a suggested number of payments to constrain the loan within a certain duration
let maxPaymentCount (maxLoanLength: int<DurationDay>) (startDate: Date) (config: Config) =
let maxPaymentCount (maxDuration: int<DurationDay>) (config: Config) (startDate: Date) =
let offset y m td = ((TrackingDay.toDate y m td) - startDate).Days |> fun f -> int f * 1<DurationDay>
match config with
| Single d -> maxLoanLength - offset d.Year d.Month d.Day
| Daily d -> maxLoanLength - offset d.Year d.Month d.Day
| Weekly (multiple, d) when multiple > 0 -> (maxLoanLength - offset d.Year d.Month d.Day) / (multiple * 7)
| SemiMonthly (y, m, d1, _) -> (maxLoanLength - offset y m (int d1)) / 15
| Monthly (multiple, y, m, d) when multiple > 0 -> (maxLoanLength - offset y m (int d)) / (multiple * 30)
| Single d -> maxDuration - offset d.Year d.Month d.Day
| Daily d -> maxDuration - offset d.Year d.Month d.Day
| Weekly (multiple, d) when multiple > 0 -> (maxDuration - offset d.Year d.Month d.Day) / (multiple * 7)
| SemiMonthly (y, m, d1, _) -> (maxDuration - offset y m (int d1)) / 15
| Monthly (multiple, y, m, d) when multiple > 0 -> (maxDuration - offset y m (int d)) / (multiple * 30)
| _ -> 0<DurationDay>
|> int

Expand All @@ -216,8 +216,12 @@ module UnitPeriod =
| Reverse

/// generate a payment schedule based on a unit-period config
let generatePaymentSchedule count direction unitPeriodConfig =
let generatePaymentSchedule count maxDuration direction unitPeriodConfig =
if count = 0 then [||] else
let limitedCount =
match maxDuration with
| ValueSome { Duration.Length = d; Duration.FromDate = fd } -> maxPaymentCount d unitPeriodConfig fd
| ValueNone -> count
let adjustMonthEnd (monthEndTrackingDay: int) (d: Date) =
if d.Day > 15 && monthEndTrackingDay > 28 then
TrackingDay.toDate d.Year d.Month monthEndTrackingDay
Expand All @@ -237,13 +241,13 @@ module UnitPeriod =
startDate.AddMonths c |> adjustMonthEnd monthEndTrackingDay
startDate.AddMonths (c + offset) |> fun d -> TrackingDay.toDate d.Year d.Month td2 |> adjustMonthEnd monthEndTrackingDay
|])
>> Array.take count
>> Array.take limitedCount
| Monthly (multiple, year, month, td) ->
let startDate = TrackingDay.toDate year month td
Array.map (fun c -> startDate.AddMonths (c * multiple) |> adjustMonthEnd td)
match direction with
| Direction.Forward -> [| 0 .. (count - 1) |] |> generate unitPeriodConfig
| Direction.Reverse -> [| 0 .. -1 .. -(count - 1) |] |> generate unitPeriodConfig |> Array.sort
| Direction.Forward -> [| 0 .. (limitedCount - 1) |] |> generate unitPeriodConfig
| Direction.Reverse -> [| 0 .. -1 .. -(limitedCount - 1) |] |> generate unitPeriodConfig |> Array.sort

/// for a given interval and array of dates, devise the unit-period config
let detect direction interval (transferDates: Date array) =
Expand Down
59 changes: 39 additions & 20 deletions tests/ActualPaymentTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ module ActualPaymentTests =
Principal = 1500_00L<Cent>
PaymentSchedule = RegularSchedule (
UnitPeriodConfig = UnitPeriod.Monthly(1, 2022, 11, 31),
PaymentCount = 5
PaymentCount = 5,
MaxDuration = ValueNone
)
FeesAndCharges = {
Fees = [||]
Expand Down Expand Up @@ -113,7 +114,8 @@ module ActualPaymentTests =
Principal = 1500_00L<Cent>
PaymentSchedule = RegularSchedule (
UnitPeriodConfig = UnitPeriod.Monthly(1, 2022, 11, 31),
PaymentCount = 5
PaymentCount = 5,
MaxDuration = ValueNone
)
FeesAndCharges = {
Fees = [||]
Expand Down Expand Up @@ -159,7 +161,8 @@ module ActualPaymentTests =
Principal = 1500_00L<Cent>
PaymentSchedule = RegularSchedule (
UnitPeriodConfig = UnitPeriod.Monthly(1, 2022, 11, 15),
PaymentCount = 5
PaymentCount = 5,
MaxDuration = ValueNone
)
FeesAndCharges = {
Fees = [||]
Expand Down Expand Up @@ -205,7 +208,8 @@ module ActualPaymentTests =
Principal = 1500_00L<Cent>
PaymentSchedule = RegularSchedule (
UnitPeriodConfig = UnitPeriod.Monthly(1, 2022, 11, 15),
PaymentCount = 5
PaymentCount = 5,
MaxDuration = ValueNone
)
FeesAndCharges = {
Fees = [||]
Expand Down Expand Up @@ -275,7 +279,8 @@ module ActualPaymentTests =
Principal = 1500_00L<Cent>
PaymentSchedule = RegularSchedule (
UnitPeriodConfig = UnitPeriod.Monthly(1, 2022, 11, 15),
PaymentCount = 5
PaymentCount = 5,
MaxDuration = ValueNone
)
FeesAndCharges = {
Fees = [||]
Expand Down Expand Up @@ -345,7 +350,8 @@ module ActualPaymentTests =
Principal = 1500_00L<Cent>
PaymentSchedule = RegularSchedule (
UnitPeriodConfig = UnitPeriod.Monthly(1, 2022, 11, 15),
PaymentCount = 5
PaymentCount = 5,
MaxDuration = ValueNone
)
FeesAndCharges = {
Fees = [||]
Expand Down Expand Up @@ -420,7 +426,8 @@ module ActualPaymentTests =
Principal = 1500_00L<Cent>
PaymentSchedule = RegularSchedule (
UnitPeriodConfig = UnitPeriod.Monthly(1, 2022, 11, 15),
PaymentCount = 5
PaymentCount = 5,
MaxDuration = ValueNone
)
FeesAndCharges = {
Fees = [||]
Expand Down Expand Up @@ -493,7 +500,8 @@ module ActualPaymentTests =
Principal = 1500_00L<Cent>
PaymentSchedule = RegularSchedule (
UnitPeriodConfig = UnitPeriod.Weekly(2, startDate.AddDays 14),
PaymentCount = 11
PaymentCount = 11,
MaxDuration = ValueNone
)
FeesAndCharges = {
Fees = [||]
Expand Down Expand Up @@ -567,7 +575,8 @@ module ActualPaymentTests =
Principal = 1500_00L<Cent>
PaymentSchedule = RegularSchedule (
UnitPeriodConfig = UnitPeriod.Monthly(1, 2022, 11, 15),
PaymentCount = 5
PaymentCount = 5,
MaxDuration = ValueNone
)
FeesAndCharges = {
Fees = [||]
Expand Down Expand Up @@ -642,7 +651,8 @@ module ActualPaymentTests =
Principal = 1500_00L<Cent>
PaymentSchedule = RegularSchedule (
UnitPeriodConfig = UnitPeriod.Monthly(1, 2022, 11, 15),
PaymentCount = 5
PaymentCount = 5,
MaxDuration = ValueNone
)
FeesAndCharges = {
Fees = [||]
Expand Down Expand Up @@ -716,7 +726,8 @@ module ActualPaymentTests =
Principal = 1500_00L<Cent>
PaymentSchedule = RegularSchedule (
UnitPeriodConfig = UnitPeriod.Monthly(1, 2022, 11, 15),
PaymentCount = 5
PaymentCount = 5,
MaxDuration = ValueNone
)
FeesAndCharges = {
Fees = [||]
Expand Down Expand Up @@ -790,7 +801,8 @@ module ActualPaymentTests =
Principal = 1500_00L<Cent>
PaymentSchedule = RegularSchedule (
UnitPeriodConfig = UnitPeriod.Monthly(1, 2022, 11, 15),
PaymentCount = 5
PaymentCount = 5,
MaxDuration = ValueNone
)
FeesAndCharges = {
Fees = [||]
Expand Down Expand Up @@ -866,7 +878,8 @@ module ActualPaymentTests =
Principal = 250_00L<Cent>
PaymentSchedule = RegularSchedule (
UnitPeriodConfig = UnitPeriod.Monthly(1, 2024, 2, 22),
PaymentCount = 4
PaymentCount = 4,
MaxDuration = ValueNone
)
FeesAndCharges = {
Fees = [||]
Expand Down Expand Up @@ -914,7 +927,8 @@ module ActualPaymentTests =
Principal = 2500_00L<Cent>
PaymentSchedule = RegularSchedule (
UnitPeriodConfig = UnitPeriod.Weekly(1, Date(2022, 5, 6)),
PaymentCount = 24
PaymentCount = 24,
MaxDuration = ValueNone
)
FeesAndCharges = {
Fees = [| Fee.CabOrCsoFee (Amount.Percentage (Percent 154.47m, ValueNone, ValueSome RoundDown)) |]
Expand Down Expand Up @@ -985,7 +999,8 @@ module ActualPaymentTests =
Principal = 2500_00L<Cent>
PaymentSchedule = RegularSchedule (
UnitPeriodConfig = UnitPeriod.Weekly(1, Date(2022, 5, 6)),
PaymentCount = 24
PaymentCount = 24,
MaxDuration = ValueNone
)
FeesAndCharges = {
Fees = [| Fee.CabOrCsoFee (Amount.Percentage (Percent 154.47m, ValueNone, ValueSome RoundDown)) |]
Expand Down Expand Up @@ -1033,7 +1048,8 @@ module ActualPaymentTests =
Principal = 2500_00L<Cent>
PaymentSchedule = RegularSchedule (
UnitPeriodConfig = UnitPeriod.Weekly(1, Date(2022, 5, 6)),
PaymentCount = 24
PaymentCount = 24,
MaxDuration = ValueNone
)
FeesAndCharges = {
Fees = [| Fee.CabOrCsoFee (Amount.Percentage (Percent 154.47m, ValueNone, ValueSome RoundDown)) |]
Expand Down Expand Up @@ -1082,7 +1098,8 @@ module ActualPaymentTests =
Principal = 2500_00L<Cent>
PaymentSchedule = RegularSchedule (
UnitPeriodConfig = UnitPeriod.Weekly(1, Date(2022, 5, 6)),
PaymentCount = 24
PaymentCount = 24,
MaxDuration = ValueNone
)
FeesAndCharges = {
Fees = [| Fee.CabOrCsoFee (Amount.Percentage (Percent 154.47m, ValueNone, ValueSome RoundDown)) |]
Expand Down Expand Up @@ -1132,7 +1149,8 @@ module ActualPaymentTests =
Principal = 2500_00L<Cent>
PaymentSchedule = RegularSchedule (
UnitPeriodConfig = UnitPeriod.Weekly(1, Date(2024, 1, 14)),
PaymentCount = 24
PaymentCount = 24,
MaxDuration = ValueNone
)
FeesAndCharges = {
Fees = [| Fee.CabOrCsoFee (Amount.Percentage (Percent 154.47m, ValueNone, ValueSome RoundDown)) |]
Expand Down Expand Up @@ -1182,7 +1200,8 @@ module ActualPaymentTests =
Principal = 2500_00L<Cent>
PaymentSchedule = RegularSchedule (
UnitPeriodConfig = UnitPeriod.Weekly(1, Date(2024, 1, 14)),
PaymentCount = 24
PaymentCount = 24,
MaxDuration = ValueNone
)
FeesAndCharges = {
Fees = [| Fee.CabOrCsoFee (Amount.Percentage (Percent 154.47m, ValueNone, ValueSome RoundDown)) |]
Expand Down Expand Up @@ -1230,7 +1249,7 @@ module ActualPaymentTests =
AsOfDate = Date(2024, 3, 2)
StartDate = Date(2023, 8, 20)
Principal = 250_00L<Cent>
PaymentSchedule = RegularSchedule(UnitPeriod.Config.Monthly(1, 2023, 9, 5), 4)
PaymentSchedule = RegularSchedule(UnitPeriod.Config.Monthly(1, 2023, 9, 5), 4, ValueNone)
FeesAndCharges = {
Fees = [||]
FeesAmortisation = Fees.FeeAmortisation.AmortiseProportionately
Expand Down
Loading

0 comments on commit 1cb01a6

Please sign in to comment.