LoanModel.cs 10 KB

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