Pārlūkot izejas kodu

房贷计算模型

懒得勤快 2 gadi atpakaļ
vecāks
revīzija
d718c98325

+ 1 - 1
Masuit.Tools.Abstractions/Masuit.Tools.Abstractions.csproj

@@ -48,7 +48,7 @@
     <ItemGroup>
         <PackageReference Include="Castle.Core" Version="5.1.1" />
         <PackageReference Include="DnsClient" Version="1.7.0" />
-        <PackageReference Include="HtmlSanitizer" Version="8.0.723" />
+        <PackageReference Include="HtmlSanitizer" Version="8.0.746" />
         <PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
         <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
         <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />

+ 327 - 0
Masuit.Tools.Abstractions/Models/LoanModel.cs

@@ -0,0 +1,327 @@
+#if NET5_0_OR_GREATER
+using System.Collections.Generic;
+using System;
+using System.Linq;
+using Microsoft.VisualBasic;
+using System.Threading.Channels;
+
+namespace Masuit.Tools.Models;
+
+/// <summary>
+/// 贷款模型
+/// </summary>
+/// <param name="Loan">贷款本金</param>
+/// <param name="Rate">贷款初始利率</param>
+/// <param name="Start">开始还款日</param>
+public record LoanModel(decimal Loan, decimal Rate, int Period, DateTime Start, LoanType LoanType = LoanType.EquivalentInterest)
+{
+	public Dictionary<DateTime, decimal?> RateAdjustments { get; set; } = new();
+
+	public List<PrepaymentOption> 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);
+	}
+
+	/// <summary>
+	/// 生成还款计划
+	/// </summary>
+	/// <returns></returns>
+	public LoanResult Payment()
+	{
+		var result = LoanType == LoanType.EquivalentPrincipal ? PrepaymentPrincipal() : PrepaymentInterest();
+		for (var i = 1; i < result.Plans.Count; i++)
+		{
+			result.Plans[i].Period = i + 1;
+			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<PaymentPlan>()
+		{
+			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].PeriodLeft = 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 <= current && x.Key > current.AddMonths(-1));
+			var newRate = adj.Value ?? list[i - 1].Rate;
+			var prepayment = Prepayments.FirstOrDefault(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].PeriodLeft - 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].PeriodLeft = leftPeriod;
+			}
+			else
+			{
+				list[i].PeriodLeft = list[i - 1].PeriodLeft - 1;
+			}
+			list[i].Payment = -Financial.Pmt((double)(list[i].Rate / 12), list[i].PeriodLeft, (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<PaymentPlan>()
+		{
+			new()
+			{
+				Date = Start,
+				LoanType = LoanType.EquivalentPrincipal,
+				PeriodLeft = 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 <= current && x.Key > current.AddMonths(-1));
+			var newRate = adj.Value ?? list[i - 1].Rate;
+			var prepayment = Prepayments.FirstOrDefault(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].PeriodLeft, 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].PeriodLeft = list[i - 1].PeriodLeft - list[i].PeriodReduce - 1;
+			}
+			else
+			{
+				list[i].PeriodLeft = list[i - 1].PeriodLeft - 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);
+	}
+}
+
+/// <summary>
+/// 贷款方式
+/// </summary>
+public enum LoanType
+{
+	/// <summary>
+	/// 等额本息
+	/// </summary>
+	EquivalentPrincipal,
+
+	/// <summary>
+	/// 等额本金
+	/// </summary>
+	EquivalentInterest,
+}
+
+/// <summary>
+/// 提前还款选项
+/// </summary>
+/// <param name="Date">提前还款时间</param>
+/// <param name="Amount">提前还款金额</param>
+/// <param name="ReducePeriod">是否减少期数</param>
+/// <param name="ChangeType">新还款方式(若还款方式改变,不支持减少期数)</param>
+public record PrepaymentOption(DateTime Date, decimal Amount, bool ReducePeriod = false, LoanType? ChangeType = null);
+
+/// <summary>
+/// 贷款结果
+/// </summary>
+/// <param name="TotalInterest">总利息</param>
+/// <param name="Plans">还款计划</param>
+public record LoanResult(decimal TotalInterest, List<PaymentPlan> Plans)
+{
+	/// <summary>
+	/// 总提前还款额
+	/// </summary>
+	public decimal TotalRepayment => Plans.Sum(e => e.Repayment);
+
+	/// <summary>
+	/// 实际总利息
+	/// </summary>
+	public decimal ActualInterest => Plans.Sum(e => e.Interest);
+
+	/// <summary>
+	/// 实际还款总额
+	/// </summary>
+	public decimal ActualPayment => Plans.Sum(e => e.Payment + e.Repayment);
+
+	/// <summary>
+	/// 节省利息
+	/// </summary>
+	public decimal SavedInterest => TotalInterest - ActualInterest;
+
+	public void Deconstruct(out decimal totalInterest, out decimal actualInterest, out decimal totalRepayment, out List<PaymentPlan> 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<PaymentPlan> 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<PaymentPlan> paymentPlans)
+	{
+		totalInterest = TotalInterest;
+		actualInterest = ActualInterest;
+		totalRepayment = TotalRepayment;
+		paymentPlans = Plans;
+		savedInterest = SavedInterest;
+		actualPayment = ActualPayment;
+	}
+}
+
+public record PaymentPlan
+{
+	/// <summary>
+	/// 期数
+	/// </summary>
+	public int Period { get; internal set; } = 12;
+
+	/// <summary>
+	/// 还款日
+	/// </summary>
+	public DateTime Date { get; internal set; } = DateTime.Now;
+
+	/// <summary>
+	/// 月供
+	/// </summary>
+	public decimal Payment { get; internal set; }
+
+	/// <summary>
+	/// 年利率
+	/// </summary>
+	public decimal Rate { get; internal set; }
+
+	/// <summary>
+	/// 月还利息
+	/// </summary>
+	public decimal Interest { get; internal set; }
+
+	/// <summary>
+	/// 月还本金
+	/// </summary>
+	public decimal Amount { get; internal set; }
+
+	/// <summary>
+	/// 当期提前还款额
+	/// </summary>
+	public decimal Repayment { get; internal set; }
+
+	/// <summary>
+	/// 每期剩余本金
+	/// </summary>
+	public decimal Balance { get; internal set; }
+
+	/// <summary>
+	/// 贷款类型(默认等额本息)
+	/// </summary>
+	public LoanType LoanType { get; internal set; }
+
+	internal int PeriodReduce { get; set; }
+	internal int PeriodLeft { get; set; }
+}
+#endif

+ 2 - 2
Masuit.Tools.AspNetCore/Masuit.Tools.AspNetCore.csproj

@@ -59,9 +59,9 @@
         <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="[5.0.17]" />
     </ItemGroup>
     <ItemGroup Condition=" '$(TargetFramework)' == 'net6'">
-        <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="[6.0.23]" />
+        <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="[6.0.24]" />
     </ItemGroup>
     <ItemGroup Condition=" '$(TargetFramework)' == 'net7'">
-        <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.12" />
+        <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.13" />
     </ItemGroup>
 </Project>

+ 2 - 2
Masuit.Tools.Core/Masuit.Tools.Core.csproj

@@ -48,10 +48,10 @@ github:https://github.com/ldqk/Masuit.Tools
         <PackageReference Include="Microsoft.EntityFrameworkCore" Version="[5.0.17]" />
     </ItemGroup>
     <ItemGroup Condition=" '$(TargetFramework)' == 'net6'">
-        <PackageReference Include="Microsoft.EntityFrameworkCore" Version="[6.0.23]" />
+        <PackageReference Include="Microsoft.EntityFrameworkCore" Version="[6.0.24]" />
     </ItemGroup>
     <ItemGroup Condition=" '$(TargetFramework)' == 'net7'">
-        <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.12" />
+        <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.13" />
     </ItemGroup>
     <ItemGroup>
       <Compile Remove="..\Masuit.Tools.Abstractions\Mapping\**" />

+ 1 - 1
Masuit.Tools.Net45/Masuit.Tools.Net45.csproj

@@ -339,7 +339,7 @@
       <Version>1.7.0</Version>
     </PackageReference>
     <PackageReference Include="Microsoft.AspNet.Mvc">
-      <Version>5.2.9</Version>
+      <Version>5.3.0</Version>
     </PackageReference>
     <PackageReference Include="Microsoft.CSharp">
       <Version>4.7.0</Version>

+ 2 - 2
Masuit.Tools/Masuit.Tools.csproj

@@ -205,10 +205,10 @@
       <Version>1.7.0</Version>
     </PackageReference>
     <PackageReference Include="HtmlSanitizer">
-      <Version>8.0.723</Version>
+      <Version>8.0.746</Version>
     </PackageReference>
     <PackageReference Include="Microsoft.AspNet.Mvc">
-      <Version>5.2.9</Version>
+      <Version>5.3.0</Version>
     </PackageReference>
     <PackageReference Include="Newtonsoft.Json">
       <Version>13.0.3</Version>

+ 52 - 0
README.md

@@ -1458,6 +1458,58 @@ public class MyClass:MyInterface{...}
 public class MyService{...}
 ```
 
+### 52. 房贷试算模型
+**支持多次提前还款和多次调整利率,同时支持提前还款时变更贷款方式和缩短年限,如有利率调整或提前还款计划,因银行计算受实时利率或提前还款违约金影响,本试算模型的计算结果和银行结果大约有1‰的误差,结果仅供参考,请以银行结果为准**  
+模拟案例:
+贷款100万,
+初始利率6.27%,
+等额本息方式,
+贷30年,
+首次还款时间2021-2-1。  
+
+利率调整:  
+2022-1-1利率调整为5.92%,LPR调整  
+2023-1-1利率调整为5.85%,LPR调整  
+2023-9-25利率调整为4.3%,政策因素银行自动调整  
+2025-1-1利率调整为4.2%,LPR调整  
+2026-1-1利率调整为4.1%,LPR调整  
+
+提前还款计划:  
+2022-10-23提前还款10万,贷款方式不变,  
+2023-10-11提前还款10万并缩短年限(`实际目前银行政策不允许`),  
+2025-10-12提前还款10万并修改为等额本金方式,  
+2026-10-14提前还款10万并以等额本金方式+缩短年限(`实际目前银行政策不允许`)。
+
+计算代码如下:
+```csharp
+var (totalInterest, actualInterest, savedInterest, totalRepayment, actualPayment, paymentPlans) = new LoanModel(1000000, 0.0627m, 360, DateTime.Parse("2021-2-1"))
+{
+	RateAdjustments = new Dictionary<DateTime, decimal?>()
+	{
+		[DateTime.Parse("2022-1-1")] = 0.0592m, // 调整前月供6170.19,调整后月供5948.53
+		[DateTime.Parse("2023-1-1")] = 0.058m, // 调整前月供5948.53,调整后月供5273.92
+		[DateTime.Parse("2023-9-25")] = 0.043m, // 调整前月供5273.92,调整后月供4496.91,调整次月还款5118.55
+		[DateTime.Parse("2025-1-1")] = 0.042m, // 调整前月供4496.91,调整后首月4457.15
+		[DateTime.Parse("2026-1-1")] = 0.041m, // 调整前月供4762.47,调整后月供4702,调整次月还款8.87元(还款方式改为了等额本金)
+	},
+
+	Prepayments = new List<PrepaymentOption>()
+	{
+		new(DateTime.Parse("2022-10-23"), 100000m, false, LoanType.EquivalentInterest), // 提前还款前月供5948.53,提前还款后月供5339.85
+		new(DateTime.Parse("2023-10-11"), 100000m, true, LoanType.EquivalentInterest), // 提前还款前月供5273.92,提前还款后月供4493.84,期数减少64期
+		new(DateTime.Parse("2025-10-12"), 100000m, false, LoanType.EquivalentPrincipal), // 提前还款前月供4771.56,提前还款后月供首月4762.47,每月递减60.4元
+		new(DateTime.Parse("2026-10-14"), 100000m, true, LoanType.EquivalentPrincipal), // 提前还款前月供4260.28,提前还款后月供首月4251.44,每月递减8.84元,期数减少38期
+	}
+}.Payment();
+```
+计算结果:  
+总利息totalInterest:1221266.8
+实际支付利息actualInterest:403845.58
+提前还款节省利息savedInterest:817421.22
+总提前还款totalRepayment:400000.00
+实际还款总额actualPayment:1403845.58
+总还款期数paymentPlans:258期,**List类型,每条记录可以展示当期的利率,利息,本金,剩余本金等信息**  
+
 # Asp.Net MVC和Asp.Net Core的支持断点续传和多线程下载的ResumeFileResult
 
 在ASP.NET Core中通过MVC/WebAPI应用程序传输文件数据时使用断点续传以及多线程下载支持。

+ 1 - 1
Test/Masuit.Tools.Core.Test/Masuit.Tools.Core.Test.csproj

@@ -9,7 +9,7 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="7.0.12" />
+    <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="7.0.13" />
     <PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
     <PackageReference Include="xunit" Version="2.5.3" />

+ 3 - 3
Test/Masuit.Tools.Test/Masuit.Tools.Test.csproj

@@ -83,13 +83,13 @@
       <Version>5.1.1</Version>
     </PackageReference>
     <PackageReference Include="Microsoft.AspNet.Mvc">
-      <Version>5.2.9</Version>
+      <Version>5.3.0</Version>
     </PackageReference>
     <PackageReference Include="Microsoft.AspNet.Razor">
-      <Version>3.2.9</Version>
+      <Version>3.3.0</Version>
     </PackageReference>
     <PackageReference Include="Microsoft.AspNet.WebPages">
-      <Version>3.2.9</Version>
+      <Version>3.3.0</Version>
     </PackageReference>
     <PackageReference Include="Microsoft.Web.Infrastructure">
       <Version>2.0.0</Version>