Private
Public Access
1
0

Use standard monthly compounding for all loan types

Daily accrual made HELOC estimates worse due to front-loaded 31-day
months. Revert to rate/12 for all types - it's the standard amortization
method and works well for mortgages and personal loans. For HELOCs and
auto loans, differences from bank statements are typically due to
variable rates, extra payments, or daily billing conventions that a
fixed amortization schedule cannot capture.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Catherine Renelle
2026-02-14 20:10:18 -05:00
parent dd2cdbf815
commit 47010e5f55
2 changed files with 6 additions and 28 deletions
+5 -20
View File
@@ -49,7 +49,7 @@ public class LoanService : ILoanService
ExtraPayment = request.ExtraPayment
};
var entries = GenerateAmortizationSchedule(loanDetail, account.Type);
var entries = GenerateAmortizationSchedule(loanDetail);
loanDetail.AmortizationEntries = entries;
_db.LoanDetails.Add(loanDetail);
@@ -82,7 +82,7 @@ public class LoanService : ILoanService
l.EscrowAmount = request.EscrowAmount;
l.ExtraPayment = request.ExtraPayment;
var entries = GenerateAmortizationSchedule(l, account.Type);
var entries = GenerateAmortizationSchedule(l);
foreach (var entry in entries)
{
entry.LoanDetailId = l.Id;
@@ -118,14 +118,11 @@ public class LoanService : ILoanService
// Build new schedule with extra payments
var balance = Math.Abs(account.Balance);
var useDailyAccrual = UseDailyAccrual(account.Type);
var monthlyRate = l.InterestRate / 100m / 12m;
var dailyRate = l.InterestRate / 100m / 365m;
var extraMonthly = request.ExtraMonthlyPayment ?? 0;
var basePayment = l.MonthlyPayment - (l.EscrowAmount ?? 0);
var now = DateTime.UtcNow;
var startDate = new DateTime(now.Year, now.Month, l.OriginationDate.Day, 0, 0, 0, DateTimeKind.Utc);
var previousDate = startDate;
var newEntries = new List<AmortizationEntryResponse>();
int paymentNum = 0;
@@ -142,15 +139,12 @@ public class LoanService : ILoanService
paymentNum++;
var paymentDate = startDate.AddMonths(paymentNum);
var interest = useDailyAccrual
? Math.Round(balance * dailyRate * (paymentDate - previousDate).Days, 2)
: Math.Round(balance * monthlyRate, 2);
var interest = Math.Round(balance * monthlyRate, 2);
var principal = Math.Min(basePayment + extraMonthly - interest, balance);
if (principal < 0) principal = 0;
balance -= principal;
if (balance < 0.01m) balance = 0;
previousDate = paymentDate;
newEntries.Add(new AmortizationEntryResponse(
paymentNum, paymentDate, principal + interest + (l.EscrowAmount ?? 0),
@@ -187,30 +181,21 @@ public class LoanService : ILoanService
return new LoanDefaultsResponse(0, interestRate, termMonths, hasSyncData);
}
private static bool UseDailyAccrual(AccountType type) =>
type is AccountType.AutoLoan or AccountType.LineOfCredit;
private static List<AmortizationEntry> GenerateAmortizationSchedule(LoanDetail loan, AccountType accountType)
private static List<AmortizationEntry> GenerateAmortizationSchedule(LoanDetail loan)
{
var entries = new List<AmortizationEntry>();
var balance = loan.OriginalBalance;
var useDailyAccrual = UseDailyAccrual(accountType);
var monthlyRate = loan.InterestRate / 100m / 12m;
var dailyRate = loan.InterestRate / 100m / 365m;
var payment = loan.MonthlyPayment - (loan.EscrowAmount ?? 0);
var extra = loan.ExtraPayment ?? 0;
var previousDate = loan.OriginationDate.AddMonths(-1);
for (int i = 1; i <= loan.TermMonths && balance > 0; i++)
{
var paymentDate = loan.OriginationDate.AddMonths(i - 1);
var interest = useDailyAccrual
? Math.Round(balance * dailyRate * (paymentDate - previousDate).Days, 2)
: Math.Round(balance * monthlyRate, 2);
var interest = Math.Round(balance * monthlyRate, 2);
var principal = Math.Min(payment + extra - interest, balance);
balance -= principal;
if (balance < 0.01m) balance = 0;
previousDate = paymentDate;
entries.Add(new AmortizationEntry
{