#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