Simple-Interest Calculations
Definition
Simple interest is a method of calculating interest where the interest is accrued during the schedule. The initial interest balance is zero, and each time a scheduled payment is due or an actual payment is made, the interest is calculated based on the outstanding principal balance and the number of days it has been outstanding. Payments are applied in the order charges -> interest -> fees -> principal, but as interest is not front loaded (as in the add-on interest method), this means each payment pays off the interest in full and then the principal balance is reduced. This means that the principal balance is lower than under the add-on interest method, and therefore the interest accrued is lower. Calculating the interest this way is the simplest method of calculating interest.
Relevant Code
The Scheduling module contains the functions that create the initial schedule. The initial schedule is a simple schedule that allows us to calculate the interest accrued over the schedule as well as the level and final payments.
Let's start by defining the parameters. Let's define a loan of £1000 advanced on 22 April 2025, paid back over 4 months starting one month after the advance date. The loan has a daily interest rate of 0.798% and a cap of 0.8% per day as well as a cap of 100% of the principal amount. Interest is calculated using the simple method.
Show/hide parameters
let parameters = {
EvaluationDate = Date(2025, 4, 22)
StartDate = Date(2025, 4, 22)
Principal = 1000_00L<Cent>
ScheduleConfig = AutoGenerateSchedule {
UnitPeriodConfig = Monthly(1, 2025, 5, 22)
ScheduleLength = PaymentCount 4
}
PaymentConfig = {
LevelPaymentOption = LowerFinalPayment
ScheduledPaymentOption = AsScheduled
Rounding = RoundUp
Minimum = DeferOrWriteOff 50L<Cent>
Timeout = 3<DurationDay>
}
FeeConfig = None
ChargeConfig = None
InterestConfig = {
Method = Interest.Method.Simple
StandardRate = Interest.Rate.Daily (Percent 0.798m)
Cap = {
TotalAmount = Amount.Percentage (Percent 100m, Restriction.NoLimit)
DailyAmount = Amount.Percentage (Percent 0.8m, Restriction.NoLimit)
}
InitialGracePeriod = 3<DurationDay>
PromotionalRates = [||]
RateOnNegativeBalance = Interest.Rate.Zero
Rounding = RoundDown
AprMethod = Apr.CalculationMethod.UnitedKingdom 3
}
}
Then we call the Scheduling.calculate function to generate the schedule:
let schedule = Scheduling.calculate parameters
Day | Scheduled payment | Simple interest | Interest portion | Principal portion | Interest balance | Principal balance | Total simple interest | Total interest | Total principal |
---|---|---|---|---|---|---|---|---|---|
0 | 0.00 | 0.0000 | 0.00 | 0.00 | 0.00 | 1,000.00 | 0.0000 | 0.00 | 0.00 |
30 | 417.72 | 239.4000 | 239.40 | 178.32 | 0.00 | 821.68 | 239.4000 | 239.40 | 178.32 |
61 | 417.72 | 203.2672 | 203.26 | 214.46 | 0.00 | 607.22 | 442.6672 | 442.66 | 392.78 |
91 | 417.72 | 145.3685 | 145.36 | 272.36 | 0.00 | 334.86 | 588.0357 | 588.02 | 665.14 |
122 | 417.69 | 82.8377 | 82.83 | 334.86 | 0.00 | 0.00 | 670.8733 | 670.85 | 1,000.00 |
As there is no initial interest balance, the principal starts to be paid off immediately, and the total interest accrued is therefore lower.
Add-on-interest comparison (click to expand)
To illustrate this, we can compare the add-on-interest schedule with an add-on-interest schedule:let simpleInterestSchedule = Scheduling.calculate { parameters with InterestConfig.Method = Interest.Method.AddOn }
Day | Scheduled payment | Simple interest | Interest portion | Principal portion | Interest balance | Principal balance | Total simple interest | Total interest | Total principal |
---|---|---|---|---|---|---|---|---|---|
0 | 0.00 | 0.0000 | 0.00 | 0.00 | 816.56 | 1,000.00 | 0.0000 | 0.00 | 0.00 |
30 | 454.15 | 239.4000 | 454.15 | 0.00 | 362.41 | 1,000.00 | 239.4000 | 454.15 | 0.00 |
61 | 454.15 | 247.3800 | 362.41 | 91.74 | 0.00 | 908.26 | 486.7800 | 816.56 | 91.74 |
91 | 454.15 | 217.4374 | 0.00 | 454.15 | 0.00 | 454.11 | 704.2174 | 816.56 | 545.89 |
122 | 454.11 | 112.3377 | 0.00 | 454.11 | 0.00 | 0.00 | 816.5552 | 816.56 | 1,000.00 |
Here, the schedule has calculated an initial interest balance of £816.56. We can see that the interest balance is paid off before the principal, meaning that the full principal remains outstanding for two months. Given that interest is accrued on the principal balance only (no interest on interest), maintaining a higher principal balance for longer means that the interest accrued is higher than it would be if the principal was paid off first.
Calculation Details
Scheduling.calculate is the function that generates the schedule. Here is a summary of the calculation steps:
- Generate payment days: generate the payment days based on the unit-period (e.g. monthly) and the first payment date
- Solve for payment values: use the bisection method to determine the level payments required
- Tweak final payment: ensure the final payment fully amortises the schedule
Let's look at each of these items in more detail.
Step 1: Generate payment days
Here we take the schedule config from the parameters and generate the payment days. In this example, the schedule is auto-generated, so the Scheduling.generatePaymentMap function takes the start date of the schedule, the unit period and the first payment date, and generates the payment days. It is also possible to specify the payment days manually or specify multiple ranges of dates at different intervals. However, the auto-generated schedule is the most common use case, and ensures respect for varying month lengths and month-end tracking dates.
let paymentMap = generatePaymentMap parameters.StartDate parameters.ScheduleConfig
let paymentDays = paymentMap |> Map.keys |> Seq.toArray
Result:
30, 61, 91, 122Step 2: Solve for payment values
Determining the payment values requires the use of a solver, because payment values determine how much principal is paid off each unit-period, and therefore how much interest is accrued, which in turn affects the payment values. We use the bisection method (Array.solveBisection) for this. This method runs a generator function (Scheduling.generatePaymentValue) on the schedule, which calculates the final principal balance for a given payment value. The bisection method then iteratively narrows down the level payment value until the final principal balance is close to zero (usually just below zero, so the final payment can be slightly smaller). To make the iteration more efficient, we use an initial guess for the payment value, which is calculated based on the estimated total interest and the number of payments. (In this instance, the initial guess is actually the correct payment value, as the schedule is simple, but for more complex schedules, several iterations may be required.)
Show/hide code
// precalculations
let firstItem = { SimpleItem.initial with PrincipalBalance = parameters.Principal }
let paymentCount = Array.length paymentDays
let roughPayment =
calculateLevelPayment paymentCount parameters.PaymentConfig.Rounding parameters.Principal 0L<Cent> 0m<Cent>
|> Cent.toDecimalCent
|> decimal
// the following calculations are part of `cref:M:FSharp.Finance.Personal.Scheduling.generatePaymentValue` but modified to show the intermediate steps
let scheduledPayment =
roughPayment
|> Cent.round parameters.PaymentConfig.Rounding
|> fun rp -> ScheduledPayment.quick (ValueSome rp) ValueNone
let simpleItems =
paymentDays
|> Array.scan(fun simpleItem pd ->
generateItem parameters parameters.InterestConfig.Method scheduledPayment simpleItem pd
) firstItem
Day | Scheduled payment | Simple interest | Interest portion | Principal portion | Interest balance | Principal balance | Total simple interest | Total interest | Total principal |
---|---|---|---|---|---|---|---|---|---|
0 | 0.00 | 0.0000 | 0.00 | 0.00 | 0.00 | 1,000.00 | 0.0000 | 0.00 | 0.00 |
30 | 250.00 | 239.4000 | 239.40 | 10.60 | 0.00 | 989.40 | 239.4000 | 239.40 | 10.60 |
61 | 250.00 | 244.7578 | 244.75 | 5.25 | 0.00 | 984.15 | 484.1578 | 484.15 | 15.85 |
91 | 250.00 | 235.6055 | 235.60 | 14.40 | 0.00 | 969.75 | 719.7633 | 719.75 | 30.25 |
122 | 250.00 | 239.8968 | 239.89 | 10.11 | 0.00 | 959.64 | 959.6600 | 959.64 | 40.36 |
Step 3: Tweak final payment
The final payment is adjusted (Scheduling.adjustFinalPayment) to ensure that the final principal balance is zero.
let finalScheduledPaymentDay = paymentDays |> Array.tryLast |> Option.defaultValue 0<OffsetDay>
let items =
simpleItems
|> adjustFinalPayment finalScheduledPaymentDay parameters.ScheduleConfig.IsAutoGenerateSchedule
Day | Scheduled payment | Simple interest | Interest portion | Principal portion | Interest balance | Principal balance | Total simple interest | Total interest | Total principal |
---|---|---|---|---|---|---|---|---|---|
0 | 0.00 | 0.0000 | 0.00 | 0.00 | 0.00 | 1,000.00 | 0.0000 | 0.00 | 0.00 |
30 | 250.00 | 239.4000 | 239.40 | 10.60 | 0.00 | 989.40 | 239.4000 | 239.40 | 10.60 |
61 | 250.00 | 244.7578 | 244.75 | 5.25 | 0.00 | 984.15 | 484.1578 | 484.15 | 15.85 |
91 | 250.00 | 235.6055 | 235.60 | 14.40 | 0.00 | 969.75 | 719.7633 | 719.75 | 30.25 |
122 | 1,209.64 | 239.8968 | 239.89 | 969.75 | 0.00 | 0.00 | 959.6600 | 959.64 | 1,000.00 |
As an extra step, the library calculates a number of statistics for the schedule, including the total interest accrued, the total fees and charges, the total payments made, and the final principal balance. The full output for this schedule, including stats, is available in the Output section in the page Unit-Test Outputs, under Compliance. This particular example is defined as ComplianceTest023.
namespace FSharp
--------------------
namespace Microsoft.FSharp
<summary> convenience functions and options to help with calculations </summary>
<summary> a .NET Framework polyfill equivalent to the DateOnly structure in .NET Core </summary>
<summary> functions for generating a regular payment schedule, with payment amounts, interest and APR </summary>
<summary> an unambiguous way to represent regular date intervals and generate schedules based on them note: unit-period definitions are based on US federal legislation but the definitions are universally applicable </summary>
[<Struct>] type Date = new: year: int * month: int * day: int -> Date val Year: int val Month: int val Day: int member AddDays: i: int -> Date member AddMonths: i: int -> Date member AddYears: i: int -> Date member ToDateTime: unit -> DateTime static member (-) : d1: Date * d2: Date -> TimeSpan static member DaysInMonth: year: int * month: int -> int ...
<summary> the date at the customer's location - ensure any time-zone conversion is performed before using this - as all calculations are date-only with no time component, summer time or other such time artefacts </summary>
--------------------
Date ()
new: year: int * month: int * day: int -> Date
module Cent from FSharp.Finance.Personal.Calculation
<summary> utility functions for base currency unit values </summary>
--------------------
[<Measure>] type Cent
<summary> the base unit of a currency (cent, penny, øre etc.) </summary>
module ScheduleConfig from FSharp.Finance.Personal.Scheduling
<summary> whether a payment plan is generated according to a regular schedule or is an irregular array of payments </summary>
--------------------
[<Struct>] type ScheduleConfig = | AutoGenerateSchedule of AutoGenerateSchedule: AutoGenerateSchedule | FixedSchedules of FixedSchedules: FixedSchedule array | CustomSchedule of CustomSchedule: Map<int<OffsetDay>,ScheduledPayment>
<summary> whether a payment plan is generated according to a regular schedule or is an irregular array of payments </summary>
union case ScheduleConfig.AutoGenerateSchedule: AutoGenerateSchedule: AutoGenerateSchedule -> ScheduleConfig
<summary> a schedule based on a unit-period config with a specific number of payments with an auto-calculated amount, optionally limited to a maximum duration </summary>
--------------------
[<Struct>] type AutoGenerateSchedule = { UnitPeriodConfig: Config ScheduleLength: ScheduleLength }
<summary> a regular schedule based on a unit-period config with a specific number of payments with an auto-calculated amount </summary>
<summary> (multi-)monthly: every n months starting on the date given by year, month and day, which tracks month-end (see config) </summary>
<summary> defines the length of a payment schedule, either by the number of payments or by the maximum duration </summary>
module PaymentConfig from FSharp.Finance.Personal.Scheduling
<summary> how to treat scheduled payments </summary>
--------------------
type PaymentConfig = { LevelPaymentOption: LevelPaymentOption ScheduledPaymentOption: ScheduledPaymentOption Rounding: Rounding Minimum: MinimumPayment Timeout: int<DurationDay> }
<summary> how to treat scheduled payments </summary>
module LevelPaymentOption from FSharp.Finance.Personal.Scheduling
<summary> when calculating the level payments, whether the final payment should be lower or higher than the level payment </summary>
--------------------
[<Struct>] type LevelPaymentOption = | LowerFinalPayment | SimilarFinalPayment | HigherFinalPayment member Html: string with get
<summary> when calculating the level payments, whether the final payment should be lower or higher than the level payment </summary>
<summary> the final payment must be lower than the level payment </summary>
<summary> whether to stick to scheduled payment amounts or add charges and interest to them </summary>
<summary> keep to the scheduled payment amounts even if this results in an open balance </summary>
module Rounding from FSharp.Finance.Personal.Calculation
<summary> the type of rounding, specifying midpoint-rounding where necessary </summary>
--------------------
[<Struct>] type Rounding = | NoRounding | RoundUp | RoundDown | RoundWith of MidpointRounding member Html: string with get
<summary> the type of rounding, specifying midpoint-rounding where necessary </summary>
<summary> round up to the specified precision (= ceiling) </summary>
<summary> add the payment due to the next payment or close the balance if the final payment </summary>
<summary> a duration of a number of days </summary>
<summary> methods for calculating interest and unambiguously expressing interest rates, as well as enforcing regulatory caps on interest chargeable </summary>
<summary> the method used to calculate the interest </summary>
<summary> simple interest method, where interest is based on the principal balance and the number of days outstanding </summary>
module Rate from FSharp.Finance.Personal.Interest
--------------------
[<Struct>] type Rate = | Zero | Annual of Annual: Percent | Daily of Daily: Percent member Html: string with get
<summary> the interest rate expressed as either an annual or a daily rate </summary>
<summary> the daily interest rate, or the annual interest rate divided by 365 </summary>
union case Percent.Percent: decimal -> Percent
--------------------
module Percent from FSharp.Finance.Personal.Calculation
<summary> utility functions for percent values </summary>
--------------------
[<Struct>] type Percent = | Percent of decimal member Html: string with get
<summary> a percentage, e.g. 42%, as opposed to its decimal representation 0.42m </summary>
module Amount from FSharp.Finance.Personal.Calculation
<summary> an amount specified either as a simple amount or as a percentage of another amount, optionally restricted to lower and/or upper limits </summary>
--------------------
[<Struct>] type Amount = | Percentage of Percentage: Percent * Restriction: Restriction | Simple of Simple: int64<Cent> | Unlimited member Html: string with get
<summary> an amount specified either as a simple amount or as a percentage of another amount, optionally restricted to lower and/or upper limits </summary>
<summary> a percentage of the principal, optionally restricted </summary>
module Restriction from FSharp.Finance.Personal.Calculation
<summary> the type of restriction placed on a possible value </summary>
--------------------
[<Struct>] type Restriction = | NoLimit | LowerLimit of LowerLimit: int64<Cent> | UpperLimit of UpperLimit: int64<Cent> | WithinRange of MinValue: int64<Cent> * MaxValue: int64<Cent> member Html: string with get
<summary> the type of restriction placed on a possible value </summary>
<summary> does not constrain values at all </summary>
<summary> a zero rate </summary>
<summary> round down to the specified precision (= floor) </summary>
<summary> calculating the APR according to various country-specific regulations </summary>
<summary> the calculation method used to determine the APR </summary>
<summary> calculates the APR according to UK FCA rules to the stated decimal precision (note that this is two places more than the percent precision) </summary>
<summary> calculates the number of days between two offset days on which interest is chargeable </summary>
module SimpleSchedule from FSharp.Finance.Personal.Scheduling
<summary> a schedule of payments, with statistics </summary>
--------------------
type SimpleSchedule = { EvaluationDay: int<OffsetDay> Items: SimpleItem array Stats: InitialStats }
<summary> a schedule of payments, with statistics </summary>
<summary> formats the schedule items as an HTML table (stats can be rendered separately) </summary>
<summary> add-on interest method, where the interest accrued over the loan is added to the initial balance and the interest is paid off before the principal balance </summary>
<summary> generates a map of offset days and payments based on a start date and payment schedule </summary>
<summary> the start date of the schedule, typically the day on which the principal is advanced </summary>
<summary> the scheduled payments or the parameters for generating them </summary>
module Map from FSharp.Finance.Personal.Calculation
<summary> functions for working with maps </summary>
--------------------
module Map from Microsoft.FSharp.Collections
--------------------
type Map<'Key,'Value (requires comparison)> = interface IReadOnlyDictionary<'Key,'Value> interface IReadOnlyCollection<KeyValuePair<'Key,'Value>> interface IEnumerable interface IStructuralEquatable interface IComparable interface IEnumerable<KeyValuePair<'Key,'Value>> interface ICollection<KeyValuePair<'Key,'Value>> interface IDictionary<'Key,'Value> new: elements: ('Key * 'Value) seq -> Map<'Key,'Value> member Add: key: 'Key * value: 'Value -> Map<'Key,'Value> ...
--------------------
new: elements: ('Key * 'Value) seq -> Map<'Key,'Value>
module Array from FSharp.Finance.Personal.Calculation
<summary> functions for working with arrays </summary>
--------------------
module Array from Microsoft.FSharp.Collections
val string: value: 'T -> string
--------------------
type string = System.String
module SimpleItem from FSharp.Finance.Personal.Scheduling
<summary> a scheduled payment item, with running calculations of interest and principal balance </summary>
--------------------
type SimpleItem = { Day: int<OffsetDay> ScheduledPayment: ScheduledPayment SimpleInterest: decimal<Cent> InterestPortion: int64<Cent> PrincipalPortion: int64<Cent> InterestBalance: int64<Cent> PrincipalBalance: int64<Cent> TotalSimpleInterest: decimal<Cent> TotalInterest: int64<Cent> TotalPrincipal: int64<Cent> }
<summary> a scheduled payment item, with running calculations of interest and principal balance </summary>
<summary> a default value with no data </summary>
<summary> the principal </summary>
<summary> options relating to scheduled payments </summary>
<summary> how to round payments </summary>
<summary> convert an integer cent value to a decimal cent value, e.g. for precise interest calculation, 1234¢ -> 1234.0000¢ </summary>
val decimal: value: 'T -> decimal (requires member op_Explicit)
--------------------
type decimal = System.Decimal
--------------------
type decimal<'Measure> = decimal
<summary> derive a rounded cent value from a decimal according to the specified rounding method </summary>
module ScheduledPayment from FSharp.Finance.Personal.Scheduling
--------------------
type ScheduledPayment = { Original: int64<Cent> voption Rescheduled: RescheduledPayment voption PreviousRescheduled: RescheduledPayment array Adjustment: int64<Cent> Metadata: Map<string,obj> } member Html: string with get
<summary> any original or rescheduled payment, affecting how any payment due is calculated </summary>
<summary> a quick convenient method to create a basic scheduled payment </summary>
<summary> options relating to interest </summary>
<summary> the method for calculating interest </summary>
module OffsetDay from FSharp.Finance.Personal.DateDay
<summary> functions for converting offset days to and from dates </summary>
--------------------
[<Measure>] type OffsetDay
<summary> the offset of a date from the start date, in days </summary>
module InitialStats from FSharp.Finance.Personal.Scheduling
<summary> statistics resulting from the simple schedule calculations </summary>
--------------------
[<Struct>] type InitialStats = { InitialInterestBalance: int64<Cent> LastScheduledPaymentDay: int<OffsetDay> LevelPayment: int64<Cent> FinalPayment: int64<Cent> ScheduledPaymentTotal: int64<Cent> PrincipalTotal: int64<Cent> InterestTotal: int64<Cent> InitialApr: Percent InitialCostToBorrowingRatio: Percent }
<summary> final statistics based on the payments being made on time and in full </summary>
<summary> handle any principal balance overpayment (due to rounding) on the final payment of a schedule </summary>