Skip to content

Commit 208502c

Browse files
committed
use unit period map for window
1 parent a96095f commit 208502c

File tree

6 files changed

+161
-11
lines changed

6 files changed

+161
-11
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@
88

99
---
1010

11+
## [2.5.4] - 2025-06-04
12+
13+
### Fixed
14+
15+
- The Window field of the amortisation schedule was generated by incrementing the window value each time a scheduled payment was due. This has now been
16+
modified to use a map of unit periods instead. This is more useful when projecting schedules beyond their original settlement date, and will be useful
17+
in future updates to support payment mapping. The original purpose of the window was to help analyse payment status (e.g. early/late) but is rather
18+
crude and will be replaced with more accurate mapping.
19+
20+
---
21+
1122
## [2.5.3] - 2025-06-04
1223

1324
### Fixed

src/Amortisation.fs

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -748,14 +748,52 @@ module Amortisation =
748748
let maxAppliedPaymentDay = appliedPayments |> Map.keys |> Seq.max
749749
let appliedPaymentCount = appliedPayments |> Map.count |> uint
750750

751+
// get the unit period and project it over the schedule to determine the amortisation windows
752+
let unitPeriodMap =
753+
match p.Basic.ScheduleConfig with
754+
| AutoGenerateSchedule ags ->
755+
let paymentSchedule =
756+
UnitPeriod.generatePaymentSchedule
757+
(UnitPeriod.ScheduleLength.MaxDuration(
758+
p.Basic.StartDate,
759+
int maxAppliedPaymentDay * 1<DurationDay>
760+
))
761+
UnitPeriod.Direction.Forward
762+
ags.UnitPeriodConfig
763+
|> Array.insertAt 0 p.Basic.StartDate
764+
|> Array.indexed
765+
766+
let dayToUnitPeriodMap =
767+
[| 0 .. int maxAppliedPaymentDay |]
768+
|> Array.map (fun day ->
769+
let day = day * 1<OffsetDay>
770+
let date = OffsetDay.toDate p.Basic.StartDate day
771+
772+
let unitPeriodIndex =
773+
paymentSchedule
774+
|> Array.filter (fun (_, paymentDate) -> paymentDate <= date)
775+
|> Array.tryLast
776+
|> Option.map fst
777+
|> Option.defaultValue 0
778+
779+
day, unitPeriodIndex
780+
)
781+
|> Map.ofArray
782+
783+
Some dayToUnitPeriodMap
784+
| _ -> None
785+
751786
// generate the amortisation schedule
752787
let generator ((previousDay, previous), totals) (currentDay, current: AppliedPayment) =
753788
// determine the window and increment every time a new scheduled payment is due
754789
let window =
755-
if ScheduledPayment.isSome current.ScheduledPayment then
756-
previous.Window + 1
757-
else
758-
previous.Window
790+
match unitPeriodMap with
791+
| Some upm -> upm |> Map.find currentDay
792+
| None ->
793+
if ScheduledPayment.isSome current.ScheduledPayment then
794+
previous.Window + 1
795+
else
796+
previous.Window
759797

760798
// get an array of advances
761799
// note: assumes single advance on day 0 (multiple advances are not currently supported), so this is based purely on the principal

tests/ActualPaymentTests.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -775,7 +775,7 @@ module ActualPaymentTests =
775775
OffsetDate = Date(2034, 1, 31)
776776
Advances = [||]
777777
ScheduledPayment = ScheduledPayment.zero
778-
Window = 5
778+
Window = 135
779779
PaymentDue = 0L<Cent>
780780
ActualPayments = [||]
781781
GeneratedPayment = NoGeneratedPayment

tests/ActualPaymentTestsExtra.fs

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,7 @@ module ActualPaymentTestsExtra =
435435
OffsetDate = Date(2023, 12, 11)
436436
Advances = [||]
437437
ScheduledPayment = ScheduledPayment.zero
438-
Window = 7
438+
Window = 15
439439
PaymentDue = 0L<Cent>
440440
ActualPayments = [||]
441441
GeneratedPayment = NoGeneratedPayment
@@ -772,7 +772,7 @@ module ActualPaymentTestsExtra =
772772
OffsetDate = Date(2025, 6, 2)
773773
Advances = [||]
774774
ScheduledPayment = ScheduledPayment.zero
775-
Window = 4
775+
Window = 19
776776
PaymentDue = 0L<Cent>
777777
ActualPayments = [||]
778778
GeneratedPayment = GeneratedValue 0L<Cent>
@@ -796,3 +796,104 @@ module ActualPaymentTestsExtra =
796796
}
797797

798798
actual |> should equal expected
799+
800+
[<Fact>]
801+
let ActualPaymentTestExtra009 () =
802+
let title = "ActualPaymentTestExtra009"
803+
804+
let description =
805+
"Over-refund should not lead to large final interest adjustment; 00224840cd8a"
806+
807+
let parameters: Parameters = {
808+
Basic = {
809+
EvaluationDate = Date(2025, 6, 2)
810+
StartDate = Date(2023, 8, 29)
811+
Principal = 250_00L<Cent>
812+
ScheduleConfig =
813+
AutoGenerateSchedule {
814+
UnitPeriodConfig = Monthly(1, 2023, 9, 23)
815+
ScheduleLength = PaymentCount 4
816+
}
817+
PaymentConfig = {
818+
LevelPaymentOption = LowerFinalPayment
819+
Rounding = RoundUp
820+
}
821+
FeeConfig = ValueNone
822+
InterestConfig = {
823+
Method = Interest.Method.AddOn
824+
StandardRate = Interest.Rate.Daily <| Percent 0.8m
825+
Cap = {
826+
TotalAmount = Amount.Percentage(Percent 100m, Restriction.NoLimit)
827+
DailyAmount = Amount.Percentage(Percent 0.8m, Restriction.NoLimit)
828+
}
829+
Rounding = RoundDown
830+
AprMethod = Apr.CalculationMethod.UnitedKingdom 3
831+
}
832+
}
833+
Advanced = {
834+
PaymentConfig = {
835+
ScheduledPaymentOption = AsScheduled
836+
Minimum = NoMinimumPayment
837+
Timeout = 0<DurationDay>
838+
}
839+
FeeConfig = ValueNone
840+
ChargeConfig = None
841+
InterestConfig = {
842+
InitialGracePeriod = 0<DurationDay>
843+
PromotionalRates = [||]
844+
RateOnNegativeBalance = Interest.Rate.Annual <| Percent 8m
845+
}
846+
SettlementDay = SettlementDay.SettlementOnEvaluationDay
847+
TrimEnd = false
848+
}
849+
}
850+
851+
let actual =
852+
let actualPayments =
853+
Map [
854+
25<OffsetDay>, [| ActualPayment.quickConfirmed 120_50L<Cent> |]
855+
55<OffsetDay>, [| ActualPayment.quickConfirmed 120_50L<Cent> |]
856+
86<OffsetDay>, [| ActualPayment.quickConfirmed 120_50L<Cent> |]
857+
116<OffsetDay>, [| ActualPayment.quickConfirmed 120_50L<Cent> |]
858+
388<OffsetDay>,
859+
[|
860+
ActualPayment.quickConfirmed -0_63L<Cent>
861+
ActualPayment.quickConfirmed -56_42L<Cent>
862+
|]
863+
]
864+
865+
let schedules = amortise parameters actualPayments
866+
schedules |> Schedule.outputHtmlToFile folder title description parameters ""
867+
schedules.AmortisationSchedule.ScheduleItems |> Map.maxKeyValue
868+
869+
let expected =
870+
573<OffsetDay>,
871+
{
872+
OffsetDayType = OffsetDayType.SettlementDay
873+
OffsetDate = Date(2025, 6, 2)
874+
Advances = [||]
875+
ScheduledPayment = ScheduledPayment.zero
876+
Window = 21
877+
PaymentDue = 0L<Cent>
878+
ActualPayments = [||]
879+
GeneratedPayment = GeneratedValue 1_91L<Cent>
880+
NetEffect = 1_91L<Cent>
881+
PaymentStatus = Generated
882+
BalanceStatus = ClosedBalance
883+
ActuarialInterest = 1_28.52m<Cent>
884+
NewInterest = 1_28m<Cent>
885+
NewCharges = [||]
886+
PrincipalPortion = 63L<Cent>
887+
FeePortion = 0L<Cent>
888+
InterestPortion = 1_28L<Cent>
889+
ChargesPortion = 0L<Cent>
890+
FeeRebate = 0L<Cent>
891+
PrincipalBalance = 0L<Cent>
892+
FeeBalance = 0L<Cent>
893+
InterestBalance = 0m<Cent>
894+
ChargesBalance = 0L<Cent>
895+
SettlementFigure = 0L<Cent>
896+
FeeRebateIfSettled = 0L<Cent>
897+
}
898+
899+
actual |> should equal expected

tests/EdgeCaseTests.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -845,7 +845,7 @@ module EdgeCaseTests =
845845
OffsetDate = Date(2024, 4, 5)
846846
Advances = [||]
847847
ScheduledPayment = ScheduledPayment.zero
848-
Window = 4
848+
Window = 11
849849
PaymentDue = 0L<Cent>
850850
ActualPayments = [||]
851851
GeneratedPayment = NoGeneratedPayment

tests/QuoteTests.fs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -741,7 +741,7 @@ module QuoteTests =
741741
OffsetDate = startDate.AddDays 181
742742
Advances = [||]
743743
ScheduledPayment = ScheduledPayment.zero
744-
Window = 10
744+
Window = 13
745745
PaymentDue = 0L<Cent>
746746
ActualPayments = [||]
747747
GeneratedPayment = GeneratedValue 1311_67L<Cent>
@@ -834,7 +834,7 @@ module QuoteTests =
834834
OffsetDate = startDate.AddDays 388
835835
Advances = [||]
836836
ScheduledPayment = ScheduledPayment.zero
837-
Window = 11
837+
Window = 27
838838
PaymentDue = 0L<Cent>
839839
ActualPayments = [||]
840840
GeneratedPayment = GeneratedValue 1261_73L<Cent>
@@ -1664,7 +1664,7 @@ module QuoteTests =
16641664
OffsetDate = Date(2024, 2, 5)
16651665
Advances = [||]
16661666
ScheduledPayment = ScheduledPayment.zero
1667-
Window = 5
1667+
Window = 15
16681668
PaymentDue = 0L<Cent>
16691669
ActualPayments = [||]
16701670
GeneratedPayment = GeneratedValue -72_80L<Cent>

0 commit comments

Comments
 (0)