LoanModel.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. #if NET5_0_OR_GREATER
  2. using System.Collections.Generic;
  3. using System;
  4. using System.ComponentModel;
  5. using System.Linq;
  6. using Microsoft.VisualBasic;
  7. namespace Masuit.Tools.Models;
  8. /// <summary>
  9. /// 贷款模型
  10. /// </summary>
  11. /// <param name="Loan">贷款本金</param>
  12. /// <param name="Rate">贷款初始利率</param>
  13. /// <param name="Start">开始还款日</param>
  14. public record LoanModel(decimal Loan, decimal Rate, int Period, DateTime Start, LoanType LoanType = LoanType.EquivalentInterest)
  15. {
  16. public Dictionary<DateTime, decimal?> RateAdjustments { get; set; } = new();
  17. public List<PrepaymentOption> Prepayments { get; set; } = new();
  18. private static decimal CumIPMT(decimal rate, decimal loan, int period)
  19. {
  20. double interest = 0;
  21. for (int i = 1; i <= period; i++)
  22. {
  23. interest += Financial.IPmt((double)(rate / 12), i, period, (double)loan);
  24. }
  25. return interest.ToDecimal(2);
  26. }
  27. /// <summary>
  28. /// 生成还款计划
  29. /// </summary>
  30. /// <returns></returns>
  31. public LoanResult Payment()
  32. {
  33. var result = LoanType == LoanType.EquivalentPrincipal ? PrepaymentPrincipal() : PrepaymentInterest();
  34. result.Plans[0].OriginRemainInterest=result.Plans[0].RemainInterest;
  35. for (var i = 1; i < result.Plans.Count; i++)
  36. {
  37. result.Plans[i].Period = i + 1;
  38. result.Plans[i].OriginRemainInterest = result.Plans[i-1].RemainInterest-result.Plans[i].Interest;
  39. if (result.Plans[i].LoanType != result.Plans[i - 1].LoanType)
  40. {
  41. result.Plans[i].Repayment = result.Plans[i - 1].Balance - result.Plans[i].Balance - result.Plans[i].Amount;
  42. }
  43. }
  44. return result;
  45. }
  46. private LoanResult PrepaymentInterest()
  47. {
  48. var list = new List<PaymentPlan>()
  49. {
  50. new()
  51. {
  52. Date = Start,
  53. LoanType = LoanType.EquivalentInterest
  54. }
  55. };
  56. var pmt = -Financial.Pmt((double)(Rate / 12), Period, (double)Loan);
  57. list[0].Rate = Rate;
  58. list[0].Period = 1;
  59. list[0].RemainPeriod = Period;
  60. list[0].Payment = pmt.ToDecimal(2);
  61. list[0].Interest = Math.Round(Loan * Rate / 12, 2, MidpointRounding.AwayFromZero);
  62. list[0].Amount = list[0].Payment - list[0].Interest;
  63. list[0].Balance = Loan - list[0].Amount;
  64. for (var i = 1; i < Period; i++)
  65. {
  66. var current = Start.AddMonths(i);
  67. var adj = RateAdjustments.FirstOrDefault(x => x.Key <= current && x.Key > current.AddMonths(-1));
  68. var newRate = adj.Value ?? list[i - 1].Rate;
  69. var prepayment = Prepayments.FirstOrDefault(x => x.Date <= current && x.Date > current.AddMonths(-1));
  70. if (prepayment?.ChangeType is LoanType.EquivalentPrincipal)
  71. {
  72. list.AddRange(new LoanModel(list[i - 1].Balance - prepayment.Amount, newRate, list[i - 1].RemainPeriod - 1, current, LoanType.EquivalentPrincipal)
  73. {
  74. Prepayments = Prepayments,
  75. RateAdjustments = RateAdjustments
  76. }.PrepaymentPrincipal().Plans);
  77. break;
  78. }
  79. list.Add(new PaymentPlan()
  80. {
  81. Period = i,
  82. Date = current,
  83. LoanType = LoanType.EquivalentInterest
  84. });
  85. list[i].Rate = newRate;
  86. list[i].Repayment = prepayment?.Amount ?? 0;
  87. if (Prepayments.FirstOrDefault(x => x.Date <= current.AddMonths(-1) && x.Date > current.AddMonths(-2))?.ReducePeriod == true)
  88. {
  89. 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)));
  90. list[i].PeriodReduce = Period - list.Count + 1 - leftPeriod;
  91. list[i].RemainPeriod = leftPeriod;
  92. }
  93. else
  94. {
  95. list[i].RemainPeriod = list[i - 1].RemainPeriod - 1;
  96. }
  97. list[i].Payment = -Financial.Pmt((double)(list[i].Rate / 12), list[i].RemainPeriod, (double)list[i - 1].Balance).ToDecimal(2);
  98. if ((current - adj.Key).TotalDays > 0 && (current - adj.Key).TotalDays < 30)
  99. {
  100. var days = (decimal)(list[i].Date - list[i - 1].Date).TotalDays;
  101. 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);
  102. }
  103. list[i].Interest = Math.Round(list[i - 1].Balance * list[i].Rate / 12, 2);
  104. list[i].Amount = Math.Round(list[i].Payment - list[i].Interest, 2);
  105. list[i].Balance = Math.Round(list[i - 1].Balance - list[i].Amount - list[i].Repayment, 2);
  106. if (list[i].Balance <= 0)
  107. {
  108. list[i].Payment += list[i].Balance;
  109. break;
  110. }
  111. }
  112. var totalInterest = -CumIPMT(Rate, Loan, Period);
  113. return new LoanResult(totalInterest, list);
  114. }
  115. private LoanResult PrepaymentPrincipal()
  116. {
  117. var list = new List<PaymentPlan>()
  118. {
  119. new()
  120. {
  121. Date = Start,
  122. LoanType = LoanType.EquivalentPrincipal,
  123. RemainPeriod = Period
  124. }
  125. };
  126. list[0].Rate = Rate;
  127. list[0].Period = 1;
  128. list[0].Interest = Math.Round(Loan * Rate / 12, 2, MidpointRounding.AwayFromZero);
  129. list[0].Amount = Math.Round(Loan / Period, 2, MidpointRounding.AwayFromZero);
  130. list[0].Payment = Math.Round(list[0].Amount + list[0].Interest, 2, MidpointRounding.AwayFromZero);
  131. list[0].Balance = Math.Round(Loan - list[0].Amount, 2, MidpointRounding.AwayFromZero);
  132. for (var i = 1; i < Period; i++)
  133. {
  134. var current = Start.AddMonths(i);
  135. var adj = RateAdjustments.FirstOrDefault(x => x.Key <= current && x.Key > current.AddMonths(-1));
  136. var newRate = adj.Value ?? list[i - 1].Rate;
  137. var prepayment = Prepayments.FirstOrDefault(x => x.Date <= current && x.Date > current.AddMonths(-1));
  138. if (prepayment?.ChangeType is LoanType.EquivalentInterest)
  139. {
  140. list.AddRange(new LoanModel(list[i - 1].Balance - prepayment.Amount, newRate, list[i - 1].RemainPeriod, current)
  141. {
  142. Prepayments = Prepayments,
  143. RateAdjustments = RateAdjustments
  144. }.PrepaymentInterest().Plans);
  145. break;
  146. }
  147. list.Add(new PaymentPlan()
  148. {
  149. Period = i,
  150. Date = current,
  151. LoanType = LoanType.EquivalentPrincipal
  152. });
  153. list[i].Rate = newRate;
  154. list[i].Repayment = prepayment?.Amount ?? 0;
  155. list[i].Interest = Math.Round(list[i - 1].Balance * list[i].Rate / 12, 2, MidpointRounding.AwayFromZero);
  156. if ((current - adj.Key).TotalDays > 0 && (current - adj.Key).TotalDays < 30)
  157. {
  158. var days = (decimal)(list[i].Date - list[i - 1].Date).TotalDays;
  159. 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);
  160. }
  161. if (prepayment?.ReducePeriod == true)
  162. {
  163. list[i].PeriodReduce = (int)Math.Round(list[i].Repayment / (Loan / Period));
  164. list[i].RemainPeriod = list[i - 1].RemainPeriod - list[i].PeriodReduce - 1;
  165. }
  166. else
  167. {
  168. list[i].RemainPeriod = list[i - 1].RemainPeriod - 1;
  169. }
  170. list[i].Amount = Math.Round(list[i - 1].Balance / (Period - i - list.Sum(p => p.PeriodReduce)), 2, MidpointRounding.AwayFromZero);
  171. list[i].Payment = Math.Round(list[i].Amount + list[i].Interest, 2, MidpointRounding.AwayFromZero);
  172. list[i].Balance = Math.Round(list[i - 1].Balance - list[i].Amount - list[i].Repayment, 2, MidpointRounding.AwayFromZero);
  173. if (list[i].Balance <= 0)
  174. {
  175. list[i].Payment += list[i].Balance;
  176. break;
  177. }
  178. }
  179. var totalInterest = Loan * Rate / 12 * (Period + 1) / 2;
  180. return new LoanResult(totalInterest, list);
  181. }
  182. }
  183. /// <summary>
  184. /// 贷款方式
  185. /// </summary>
  186. public enum LoanType
  187. {
  188. /// <summary>
  189. /// 等额本息
  190. /// </summary>
  191. [Description("等额本息")]
  192. EquivalentInterest,
  193. /// <summary>
  194. /// 等额本金
  195. /// </summary>
  196. [Description("等额本金")]
  197. EquivalentPrincipal,
  198. }
  199. /// <summary>
  200. /// 提前还款选项
  201. /// </summary>
  202. /// <param name="Date">提前还款时间</param>
  203. /// <param name="Amount">提前还款金额</param>
  204. /// <param name="ReducePeriod">是否减少期数</param>
  205. /// <param name="ChangeType">新还款方式(若还款方式改变,不支持减少期数)</param>
  206. public record PrepaymentOption(DateTime Date, decimal Amount, bool ReducePeriod = false, LoanType? ChangeType = null);
  207. /// <summary>
  208. /// 贷款结果
  209. /// </summary>
  210. /// <param name="TotalInterest">总利息</param>
  211. /// <param name="Plans">还款计划</param>
  212. public record LoanResult(decimal TotalInterest, List<PaymentPlan> Plans)
  213. {
  214. /// <summary>
  215. /// 总提前还款额
  216. /// </summary>
  217. public decimal TotalRepayment => Plans.Sum(e => e.Repayment);
  218. /// <summary>
  219. /// 实际总利息
  220. /// </summary>
  221. public decimal ActualInterest => Plans.Sum(e => e.Interest);
  222. /// <summary>
  223. /// 实际还款总额
  224. /// </summary>
  225. public decimal ActualPayment => Plans.Sum(e => e.Payment + e.Repayment);
  226. /// <summary>
  227. /// 节省利息
  228. /// </summary>
  229. public decimal SavedInterest => TotalInterest - ActualInterest;
  230. public void Deconstruct(out decimal totalInterest, out decimal actualInterest, out decimal totalRepayment, out List<PaymentPlan> paymentPlans)
  231. {
  232. totalInterest = TotalInterest;
  233. actualInterest = ActualInterest;
  234. totalRepayment = TotalRepayment;
  235. paymentPlans = Plans;
  236. }
  237. public void Deconstruct(out decimal totalInterest, out decimal actualInterest, out decimal savedInterest, out decimal totalRepayment, out List<PaymentPlan> paymentPlans)
  238. {
  239. totalInterest = TotalInterest;
  240. actualInterest = ActualInterest;
  241. totalRepayment = TotalRepayment;
  242. paymentPlans = Plans;
  243. savedInterest = SavedInterest;
  244. }
  245. public void Deconstruct(out decimal totalInterest, out decimal actualInterest, out decimal savedInterest, out decimal totalRepayment, out decimal actualPayment, out List<PaymentPlan> paymentPlans)
  246. {
  247. totalInterest = TotalInterest;
  248. actualInterest = ActualInterest;
  249. totalRepayment = TotalRepayment;
  250. paymentPlans = Plans;
  251. savedInterest = SavedInterest;
  252. actualPayment = ActualPayment;
  253. }
  254. }
  255. public record PaymentPlan
  256. {
  257. /// <summary>
  258. /// 期数
  259. /// </summary>
  260. public int Period { get; internal set; } = 12;
  261. /// <summary>
  262. /// 还款日
  263. /// </summary>
  264. public DateTime Date { get; internal set; } = DateTime.Now;
  265. /// <summary>
  266. /// 月供
  267. /// </summary>
  268. public decimal Payment { get; internal set; }
  269. /// <summary>
  270. /// 年利率
  271. /// </summary>
  272. public decimal Rate { get; internal set; }
  273. /// <summary>
  274. /// 月还利息
  275. /// </summary>
  276. public decimal Interest { get; internal set; }
  277. /// <summary>
  278. /// 月还本金
  279. /// </summary>
  280. public decimal Amount { get; internal set; }
  281. /// <summary>
  282. /// 当期提前还款额
  283. /// </summary>
  284. public decimal Repayment { get; internal set; }
  285. /// <summary>
  286. /// 当期剩余本金
  287. /// </summary>
  288. public decimal Balance { get; internal set; }
  289. /// <summary>
  290. /// 当期剩余利息
  291. /// </summary>
  292. public decimal RemainInterest => LoanType switch
  293. {
  294. LoanType.EquivalentInterest => Payment * (RemainPeriod-1) - Balance-Repayment,
  295. LoanType.EquivalentPrincipal => RemainPeriod * Interest - (RemainPeriod - 1) * RemainPeriod * Amount * (Rate / 12) / 2-Interest,
  296. _ => 0
  297. };
  298. /// <summary>
  299. /// 当期剩余利息(提前还款/利率调整前)
  300. /// </summary>
  301. public decimal OriginRemainInterest { get; internal set; }
  302. /// <summary>
  303. /// 贷款类型(默认等额本息)
  304. /// </summary>
  305. public LoanType LoanType { get; internal set; }
  306. /// <summary>
  307. /// 期数减少
  308. /// </summary>
  309. internal int PeriodReduce { get; set; }
  310. /// <summary>
  311. /// 剩余期数
  312. /// </summary>
  313. internal int RemainPeriod { get; set; }
  314. }
  315. #endif