#if NET5_0_OR_GREATER using System.Collections.Generic; using System; using System.ComponentModel; using System.Linq; using Microsoft.VisualBasic; namespace Masuit.Tools.Models; /// /// 贷款模型 /// /// 贷款本金 /// 贷款初始利率 /// 开始还款日 public record LoanModel(decimal Loan, decimal Rate, int Period, DateTime Start, LoanType LoanType = LoanType.EquivalentInterest) { public Dictionary RateAdjustments { get; set; } = new(); public List Prepayments { get; set; } = new(); private static decimal CumIPMT(decimal rate, decimal loan, int period) { double interest = 0; for (int i = 1; i <= period; i++) { interest += Financial.IPmt((double)(rate / 12), i, period, (double)loan); } return interest.ToDecimal(2); } /// /// 生成还款计划 /// /// public LoanResult Payment() { var result = LoanType == LoanType.EquivalentPrincipal ? PrepaymentPrincipal() : PrepaymentInterest(); result.Plans[0].OriginRemainInterest = result.Plans[0].RemainInterest; for (var i = 1; i < result.Plans.Count; i++) { result.Plans[i].Period = i + 1; result.Plans[i].OriginRemainInterest = result.Plans[i - 1].RemainInterest - result.Plans[i].Interest; if (result.Plans[i].LoanType != result.Plans[i - 1].LoanType) { result.Plans[i].Repayment = result.Plans[i - 1].Balance - result.Plans[i].Balance - result.Plans[i].Amount; } } return result; } private LoanResult PrepaymentInterest() { var list = new List() { new() { Date = Start, LoanType = LoanType.EquivalentInterest } }; var pmt = -Financial.Pmt((double)(Rate / 12), Period, (double)Loan); list[0].Rate = Rate; list[0].Period = 1; list[0].RemainPeriod = Period; list[0].Payment = pmt.ToDecimal(2); list[0].Interest = Math.Round(Loan * Rate / 12, 2, MidpointRounding.AwayFromZero); list[0].Amount = list[0].Payment - list[0].Interest; list[0].Balance = Loan - list[0].Amount; for (var i = 1; i < Period; i++) { var current = Start.AddMonths(i); var adj = RateAdjustments.FirstOrDefault(x => x.Key.AddMonths(1) <= current && x.Key.AddMonths(1) > current.AddMonths(-1)); var newRate = adj.Value ?? list[i - 1].Rate; var prepayment = Prepayments.Find(x => x.Date <= current && x.Date > current.AddMonths(-1)); if (prepayment?.ChangeType is LoanType.EquivalentPrincipal) { list.AddRange(new LoanModel(list[i - 1].Balance - prepayment.Amount, newRate, list[i - 1].RemainPeriod - 1, current, LoanType.EquivalentPrincipal) { Prepayments = Prepayments, RateAdjustments = RateAdjustments }.PrepaymentPrincipal().Plans); break; } list.Add(new PaymentPlan() { Period = i, Date = current, LoanType = LoanType.EquivalentInterest }); list[i].Rate = newRate; list[i].Repayment = prepayment?.Amount ?? 0; if (Prepayments.FirstOrDefault(x => x.Date <= current.AddMonths(-1) && x.Date > current.AddMonths(-2))?.ReducePeriod == true) { var leftPeriod = (int)Math.Round(-Math.Log((double)(1 - (list[i - 1].Balance * list[i].Rate / 12) / list[i - 1].Payment)) / Math.Log((double)(1 + list[i].Rate / 12))); list[i].PeriodReduce = Period - list.Count + 1 - leftPeriod; list[i].RemainPeriod = leftPeriod; } else { list[i].RemainPeriod = list[i - 1].RemainPeriod - 1; } list[i].Payment = -Financial.Pmt((double)(list[i].Rate / 12), list[i].RemainPeriod, (double)list[i - 1].Balance).ToDecimal(2); if ((current - adj.Key).TotalDays > 0 && (current - adj.Key).TotalDays < 30) { var days = (decimal)(list[i].Date - list[i - 1].Date).TotalDays; list[i].Payment = list[i - 1].Payment / days * (decimal)Math.Abs((adj.Key - list[i - 1].Date).TotalDays) + list[i].Payment / days * (decimal)Math.Abs((current - adj.Key).TotalDays); } list[i].Interest = Math.Round(list[i - 1].Balance * list[i].Rate / 12, 2); list[i].Amount = Math.Round(list[i].Payment - list[i].Interest, 2); list[i].Balance = Math.Round(list[i - 1].Balance - list[i].Amount - list[i].Repayment, 2); if (list[i].Balance <= 0) { list[i].Payment += list[i].Balance; break; } } var totalInterest = -CumIPMT(Rate, Loan, Period); return new LoanResult(totalInterest, list); } private LoanResult PrepaymentPrincipal() { var list = new List() { new() { Date = Start, LoanType = LoanType.EquivalentPrincipal, RemainPeriod = Period } }; list[0].Rate = Rate; list[0].Period = 1; list[0].Interest = Math.Round(Loan * Rate / 12, 2, MidpointRounding.AwayFromZero); list[0].Amount = Math.Round(Loan / Period, 2, MidpointRounding.AwayFromZero); list[0].Payment = Math.Round(list[0].Amount + list[0].Interest, 2, MidpointRounding.AwayFromZero); list[0].Balance = Math.Round(Loan - list[0].Amount, 2, MidpointRounding.AwayFromZero); for (var i = 1; i < Period; i++) { var current = Start.AddMonths(i); var adj = RateAdjustments.FirstOrDefault(x => x.Key.AddMonths(1) <= current && x.Key.AddMonths(1) > current.AddMonths(-1)); var newRate = adj.Value ?? list[i - 1].Rate; var prepayment = Prepayments.Find(x => x.Date <= current && x.Date > current.AddMonths(-1)); if (prepayment?.ChangeType is LoanType.EquivalentInterest) { list.AddRange(new LoanModel(list[i - 1].Balance - prepayment.Amount, newRate, list[i - 1].RemainPeriod, current) { Prepayments = Prepayments, RateAdjustments = RateAdjustments }.PrepaymentInterest().Plans); break; } list.Add(new PaymentPlan() { Period = i, Date = current, LoanType = LoanType.EquivalentPrincipal }); list[i].Rate = newRate; list[i].Repayment = prepayment?.Amount ?? 0; list[i].Interest = Math.Round(list[i - 1].Balance * list[i].Rate / 12, 2, MidpointRounding.AwayFromZero); if ((current - adj.Key).TotalDays > 0 && (current - adj.Key).TotalDays < 30) { var days = (decimal)(list[i].Date - list[i - 1].Date).TotalDays; list[i].Interest = list[i - 1].Interest / days * (decimal)Math.Abs((adj.Key - list[i - 1].Date).TotalDays) + list[i].Interest / days * (decimal)Math.Abs((current - adj.Key).TotalDays); } if (prepayment?.ReducePeriod == true) { list[i].PeriodReduce = (int)Math.Round(list[i].Repayment / (Loan / Period)); list[i].RemainPeriod = list[i - 1].RemainPeriod - list[i].PeriodReduce - 1; } else { list[i].RemainPeriod = list[i - 1].RemainPeriod - 1; } list[i].Amount = Math.Round(list[i - 1].Balance / (Period - i - list.Sum(p => p.PeriodReduce)), 2, MidpointRounding.AwayFromZero); list[i].Payment = Math.Round(list[i].Amount + list[i].Interest, 2, MidpointRounding.AwayFromZero); list[i].Balance = Math.Round(list[i - 1].Balance - list[i].Amount - list[i].Repayment, 2, MidpointRounding.AwayFromZero); if (list[i].Balance <= 0) { list[i].Payment += list[i].Balance; break; } } var totalInterest = Loan * Rate / 12 * (Period + 1) / 2; return new LoanResult(totalInterest, list); } } /// /// 贷款方式 /// public enum LoanType { /// /// 等额本息 /// [Description("等额本息")] EquivalentInterest, /// /// 等额本金 /// [Description("等额本金")] EquivalentPrincipal, } /// /// 提前还款选项 /// /// 提前还款时间 /// 提前还款金额 /// 是否减少期数 /// 新还款方式(若还款方式改变,不支持减少期数) public record PrepaymentOption(DateTime Date, decimal Amount, bool ReducePeriod = false, LoanType? ChangeType = null); /// /// 贷款结果 /// /// 总利息 /// 还款计划 public record LoanResult(decimal TotalInterest, List Plans) { /// /// 总提前还款额 /// public decimal TotalRepayment => Plans.Sum(e => e.Repayment); /// /// 实际总利息 /// public decimal ActualInterest => Plans.Sum(e => e.Interest); /// /// 实际还款总额 /// public decimal ActualPayment => Plans.Sum(e => e.Payment + e.Repayment); /// /// 节省利息 /// public decimal SavedInterest => TotalInterest - ActualInterest; public void Deconstruct(out decimal totalInterest, out decimal actualInterest, out decimal totalRepayment, out List paymentPlans) { totalInterest = TotalInterest; actualInterest = ActualInterest; totalRepayment = TotalRepayment; paymentPlans = Plans; } public void Deconstruct(out decimal totalInterest, out decimal actualInterest, out decimal savedInterest, out decimal totalRepayment, out List paymentPlans) { totalInterest = TotalInterest; actualInterest = ActualInterest; totalRepayment = TotalRepayment; paymentPlans = Plans; savedInterest = SavedInterest; } public void Deconstruct(out decimal totalInterest, out decimal actualInterest, out decimal savedInterest, out decimal totalRepayment, out decimal actualPayment, out List paymentPlans) { totalInterest = TotalInterest; actualInterest = ActualInterest; totalRepayment = TotalRepayment; paymentPlans = Plans; savedInterest = SavedInterest; actualPayment = ActualPayment; } } public record PaymentPlan { /// /// 期数 /// public int Period { get; internal set; } = 12; /// /// 还款日 /// public DateTime Date { get; internal set; } = DateTime.Now; /// /// 月供 /// public decimal Payment { get; internal set; } /// /// 年利率 /// public decimal Rate { get; internal set; } /// /// 月还利息 /// public decimal Interest { get; internal set; } /// /// 月还本金 /// public decimal Amount { get; internal set; } /// /// 当期提前还款额 /// public decimal Repayment { get; internal set; } /// /// 当期剩余本金 /// public decimal Balance { get; internal set; } /// /// 当期剩余利息 /// public decimal RemainInterest => LoanType switch { LoanType.EquivalentInterest => Payment * (RemainPeriod - 1) - Balance - Repayment, LoanType.EquivalentPrincipal => RemainPeriod * Interest - (RemainPeriod - 1) * RemainPeriod * Amount * (Rate / 12) / 2 - Interest, _ => 0 }; /// /// 当期剩余利息(提前还款/利率调整前) /// public decimal OriginRemainInterest { get; internal set; } /// /// 贷款类型(默认等额本息) /// public LoanType LoanType { get; internal set; } /// /// 期数减少 /// internal int PeriodReduce { get; set; } /// /// 剩余期数 /// internal int RemainPeriod { get; set; } } #endif