微粒贷还款问题

起源

yuange1975 在微博发布了一个趣味问题,看到觉得挺有意思,试着做了一下,相比原需求,多写了一个贷款数据生成的类,便于测试.
需求介绍及算法思路都在代码中注释,最终正确结果等yuange公布.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
"""
LG
ourantech@163.com
2019-09-04
这是yuange1975的一个兴趣题。just for fun。

需求:
@yuange1975
https://weibo.com/2246379231/I5fgWcrFF?from=page_1005052246379231_profile&wvr=6&mod=weibotime&type=comment

最近在微信的微粒贷里借钱了,发现还款还是一个好的算法征解题。
问题,在微信微粒贷里不同时间借款若干笔,现在有一笔资金,需要还款,求通用的还款算法(还哪些笔欠款)?

已知条件有:
1、微粒贷不支持单笔借款部分还款,一笔还款必须还完。如果支持部分还款,那就问题比较简单了。
2、借款总额度还够,微粒贷还可以马上借款出来。
3、一个还款周期里,不收复利,每天按本金按固定利率算利息。
4、现金的收益利率比借款利率低,否者就不去还款了。
5、假定所有的借款下一还款时间都一样。(微粒贷不同借款还款周期都是每月固定的某一天,如果某笔借款到下一个月的还款时间小于一个月,好像这笔借款第一次还款就会到再下一个月)。这个假设简化一点点。
6、微粒贷每笔借款有最高限额4万,这个没什么影响。
7、微粒贷利率每天万2。
8、现金理财收益每天万1。
9、借款、还款本金必须是100的整数倍。

"""
import random

LOAN_RATIO = 0.0002 # 贷款利率
CASH_RATIO = 0.0001 # 现金收益


class Repay(object):
"""
还款算法:
每需要还一笔就迭代一轮看应该最先还哪一笔
迭代的内容是 计算还款收益,还款收益的主要分为两种情况:
一是如果手上的金额大于该笔待还款金额,则 还款收益=贷款剩余本金利息-待还款金额现金收益;
二是如果手上的金额小于待还款金额,则 还款收益=贷款剩余本金利息-与手上现金和大于待还款金额的满足借款规则的最小贷款金额利息-待还款金额现金收益;且增加一笔贷款。
选择还款收益为正且最大的那一笔贷款最优先还款
如果没有为正的还款收益贷款,则不还款,停止迭代
如果手上没有现金,则停止迭代
如果没有剩余的贷款,则停止迭代
"""
def __init__(self, loans, cash):
self.loans = loans
self.cash = cash
self.update_loans()

def plan(self):
max_repay_income = self.get_max_repay_income()
while self.loans and max_repay_income > 0:
self.pay()
self.update_loans()
max_repay_income = self.get_max_repay_income()
return self.loans, self.cash

def update_loans(self):
temp = []
for m, n, i, j, k in self.loans:
if self.cash >= j:
temp.append([m, n, i, j, round(self.repay_income_1(i, j), 3)])
else:
min_aount = self.min_loan_amount(j, self.cash)
temp.append([m, n, i, j, round(self.repay_income_2(i, j, min_aount), 3)])
self.loans = temp

def get_max_repay_income(self):
if self.loans:
return max(i[4] for i in self.loans)
else:
return 0

def pay(self):
self.loans = sorted(self.loans, key=lambda x: x[4])
payment = self.loans.pop()
print(f"还款前现金金额: {self.cash}\t\t\t偿还贷款: {payment[:-1]}\t\t剩余现金金额:{round(self.cash-payment[-2], 2)}")
if self.cash >= payment[-2]:
self.cash = round(self.cash - payment[-2], 2)
else:
min_aount = self.min_loan_amount(payment[-2], self.cash)
self.cash = round(self.cash + min_aount - payment[-2], 2)
self.loans.append([min_aount, 5, min_aount, min_aount, 0]) # 假设为还款而产生的借款都为5期
print(f"还款时暂时借款:{min_aount}")

@staticmethod
def repay_income_1(principal, balance) -> float:
"""
当手中现金大于待还款金额时,计算还款收益。
:param principal: 贷款剩余本金
:param balance: 剩余还款总额
:return:
"""
return principal*LOAN_RATIO - balance*CASH_RATIO

@staticmethod
def repay_income_2(principal, balance, min_loan) -> float:
"""
当手中现金小于待还款金额时,先借到使手中金额大于待还款金额的最小借款。在综合计算还款收益。
:param principal: 贷款剩余本金
:param balance: 剩余还款总额
:param min_loan: 最小借款金额
:return:
"""
return (principal-min_loan)*LOAN_RATIO - balance*CASH_RATIO

@staticmethod
def min_loan_amount(balance, cash_num):
"""
剩余还款金额大于手中现金是,计算满足规则且覆盖还款金额的最小借款金额。
规则是借款金额必须是100 的整数倍
:param balance: 待还款金额
:param cash_num: 手中现金
:return:
"""
from math import ceil
diff = balance - cash_num
return 100*ceil(diff/100)


class GenerateData(object):
"""
用于生成满足规则的测试数据。
"""

def __init__(self, max_cash, loans_num):
self.max_cash = max_cash
self.loans_num = loans_num

def gen(self):
cash = round(random.random()*self.max_cash) # 随机生成手上现金数。
loans = [self.get_loan() for _ in range(self.loans_num)] # 随机生成若干笔贷款

return cash, loans

@staticmethod
def get_loan():
loan = random.randint(5, 400) * 100 # 生成[500, 40000]的借款金额
months = [5, 10, 20][random.randint(0, 2)] # 随机生成借款月数
days = random.randint(0, 30*months) # 生成[0, 720]天的借款天数(已发生)
days_1 = random.randint(30, 60) # 借款时距下一固定还款日期天数

over_months = (days - days_1)//30 + 1 # 已还款周期数

remain_loan = round(loan*(1 - over_months/months), 2) # 剩余借款本金
remain_loan_inter = round(remain_loan *
(1 + LOAN_RATIO * (days - (30 * (over_months - 1) + days_1))), 2) # 截止现在剩余还款总额(含息)

return [loan, months, remain_loan, remain_loan_inter, 0]


if __name__ == '__main__':
cash_0, loans_0 = GenerateData(100000, 10).gen()

print(f"初始现金情况:{cash_0}")
print(f"贷款数据说明: [初始贷款金额, 贷款期数, 剩余贷款本金, 剩余贷款总额]")
print(f"初始贷款情况:{[i[:-1] for i in loans_0]}\n\n")

loans_, cash_ = Repay(loans=loans_0, cash=cash_0).plan()
print(f"\n\n剩余贷款:{[i[:-1] for i in loans_]}")
print(f"剩余现金金额:{cash_}")

示例结果

Markdown