From 0d4cf03c0782d6a076d08d49b4fd5f3b9a98fb81 Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Tue, 25 Apr 2017 14:39:29 -0400 Subject: [PATCH 01/28] Start loan_allocation --- .../financials/income_statement_form_hist.py | 2 +- bpfin/financials/loan_allocation.py | 78 +++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 bpfin/financials/loan_allocation.py diff --git a/bpfin/financials/income_statement_form_hist.py b/bpfin/financials/income_statement_form_hist.py index 706b8ba..65ba995 100644 --- a/bpfin/financials/income_statement_form_hist.py +++ b/bpfin/financials/income_statement_form_hist.py @@ -13,7 +13,7 @@ def income_statement_character(income_statement_hist): income_statement_hist = {2014:{'revenue': 100000, ... ,'noi':37000}, 2015:{}, 2016:{}} output = {'cagr': 2.45, 'other_utility_percentage': 20.4%, 'non_utility_expense_percentage': 43.5%, ..} Note: cagr == compound annual growht rate - ''' + """ sorted_income_hist_year = sorted(income_statement_hist) start_year = sorted_income_hist_year[0] end_year = sorted_income_hist_year[-1] diff --git a/bpfin/financials/loan_allocation.py b/bpfin/financials/loan_allocation.py new file mode 100644 index 0000000..ea41587 --- /dev/null +++ b/bpfin/financials/loan_allocation.py @@ -0,0 +1,78 @@ +import bpfin.financials.financial_lib as fl +import bpfin.tests.testdata.sample_data as db +import bpfin.utilbills.bill_lib as bl +from bpfin.lib import other as lib +import pprint + + +def loan_allocation(): + + + + + + + + + +# ## LP function, return total budget and variables +# def budget_LP(Financial_list,Loan_list,saving_percentage,other_loan,exp_pb,SF_fin,is_loan_first,saving_contigency,min_DSCR): +# # loan information ************* +# other_loan_DS=other_loan +# loan_max_list=[] +# ratio_list=[] +# DSmax_list=[] +# for loan in Loan_list: +# loan_max_list.append(loan.max_amount) +# ratio=cal_loan_payback(loan.interest,loan.duration) +# ratio_list.append(ratio) +# DSmax_list.append(loan.max_amount/ratio) +# DSmax=sum(DSmax_list) + +# # client preference ************* +# ratio_SF=exp_pb #expected payback period for self finance +# # is_loan_first = does client want to reduce loan by self-finance +# # willingtopay = max amount that the client can/want to self-finance + +# # financial and savings ************* +# noi=average([Financial_list[0].noi,Financial_list[1].noi,Financial_list[2].noi]) +# savings=average([Financial_list[0].energy_opex,Financial_list[1].energy_opex,Financial_list[2].energy_opex])*saving_percentage + +# ## object formula and conditions ********************************** +# c =[-1]*(len(Loan_list)+1) + +# a_base=[] +# for ratio in ratio_list: +# a_base.append(1/ratio) +# a1=[] +# a2=[] +# for pp in a_base: +# a1.append(pp) +# a2.append(pp) +# a1.append(1/ratio_SF) +# a2.append(0) +# a3=[0]*(len(Loan_list)+1) +# a3[-1]=1 + +# A =[ +# a1, +# a2, +# a3 +# ] +# b =[ +# savings/saving_contigency, +# ((noi+savings)/min_DSCR-other_loan_DS), +# SF_fin +# ] + +# bound_list=[] +# for loan_max in loan_max_list: +# bound_list.append((0,loan_max)) +# bound_list.append((0,(max(savings-DSmax,0)*ratio_SF if is_loan_first==True else None))) +# bounds=bound_list + +# res = linprog(c,A_ub=A,b_ub=b,bounds=bounds,options={'disp':False}) +# # print('Max_Budget',-res.fun) +# # print(res.x) +# # print('\n') +# return [-res.fun,res.x] -- GitLab From 79d2c25b377be3fc77864f095f954ca2f9d740fc Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Tue, 25 Apr 2017 14:56:16 -0400 Subject: [PATCH 02/28] Comment loan_allocation --- bpfin/financials/loan_allocation.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/bpfin/financials/loan_allocation.py b/bpfin/financials/loan_allocation.py index ea41587..5586215 100644 --- a/bpfin/financials/loan_allocation.py +++ b/bpfin/financials/loan_allocation.py @@ -6,7 +6,24 @@ import pprint def loan_allocation(): - + """ + Take inputs of available financing options, and other constrains, + determine the best financing plan, with minimum debt service. + Args: + construction_date + construction_cost + income_statement_prior + income_statement_prior + cash_balance + liability + available_loan + customer_preference + required_cash_DSCR + required_noi_DSCR + required_saving_DSCR + Return: + list: [total_] + """ -- GitLab From a1d8a411c0ce4021daa6bd95a41a3fcf1385c1d1 Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Tue, 25 Apr 2017 16:17:27 -0400 Subject: [PATCH 03/28] Explain loan_allocation func purpose --- bpfin/financials/loan_allocation.py | 76 ++++++++++++++++++++++++----- bpfin/prelim/prelimfuncs.py | 6 +-- 2 files changed, 67 insertions(+), 15 deletions(-) diff --git a/bpfin/financials/loan_allocation.py b/bpfin/financials/loan_allocation.py index 5586215..afc186d 100644 --- a/bpfin/financials/loan_allocation.py +++ b/bpfin/financials/loan_allocation.py @@ -7,7 +7,7 @@ import pprint def loan_allocation(): """ - Take inputs of available financing options, and other constrains, + Take inputs of available financing options(loans and self-finance), and other constrains, determine the best financing plan, with minimum debt service. Args: construction_date @@ -16,22 +16,76 @@ def loan_allocation(): income_statement_prior cash_balance liability - available_loan + available_loans customer_preference required_cash_DSCR required_noi_DSCR required_saving_DSCR Return: - list: [total_] + dictonary: {{loan_id: amount}, {loan_id: amount}, {loan_id: amount}} """ - - - - - - - - + # run a linear programming to determine the result. + # ****pre-request is: + # quoted_cost <= sum_loan_max_amount + self_finance_max + # + # ****LP constrains are: + # 1. target function, determine minimum annual total DS. + # x1/payback1, x2/payback2, x3/payback3 = minimum + # * x is loan amount, no self-finance + # 2. x variables constraint + # x1 + x2 + x3 <= max + # max = min(project_cost, sum_loan_max_amount, saving/dscr, noi/dscr, cash/dscr) + # * default assumption is customer wants to self-finance but us loan first + # self-finance amount = project_cost - sum_x + # ** if customer wants to use self-fiance first, then + # project_cost = quoted_cost - self_finance_max + # 3. x variables bounds + # 0 <= x[i] <= loan[i]_max_amount + + + # # cauclate loan amount should be borrowed from each bank, given assumed total cost and loan info + # def loan_allocate(total_cost, loan_list): + # sum_loan_max = 0 + # bound_list = [] + # c_base = [] + # for loan in loan_list: + # sum_loan_max += loan.max_amount + # c_base.append( + # 1 / loan.payback) # loan amount / ratio = DS, targeting minimum total DS, equivalent to longest payback + # bound_list.append((0, loan.max_amount)) + # c = c_base # c_base is the target function in linear programming + # A = [-1] * len(loan_list) + # b = [max(0 - total_cost, 0 - sum_loan_max)] + # bounds = bound_list + # if total_cost > sum_loan_max: + # print('alert: upfront cost > available loan amount') + # res = linprog(c, A_ub=A, b_ub=b, bounds=bounds, options={'disp': False}) + # for i in range(len(loan_list)): + # loan_list[i].amount = res.x[i] + # # return res.x + + +# def buget_loan_allocation(): +# """ +# Take inputs of available financing options, and other constrains, +# determine the best financing plan, with minimum debt service. +# Args: +# construction_date +# construction_cost +# income_statement_prior +# income_statement_prior +# cash_balance +# liability +# available_loan +# customer_preference +# required_cash_DSCR +# required_noi_DSCR +# required_saving_DSCR +# Return: +# dictonary: {{loan_id: amount}, {loan_id: amount}, {loan_id: amount}} + +# Note: in return, loan_id == 0 means self_finance +# """ # ## LP function, return total budget and variables # def budget_LP(Financial_list,Loan_list,saving_percentage,other_loan,exp_pb,SF_fin,is_loan_first,saving_contigency,min_DSCR): # # loan information ************* diff --git a/bpfin/prelim/prelimfuncs.py b/bpfin/prelim/prelimfuncs.py index d6b688b..708f0d0 100644 --- a/bpfin/prelim/prelimfuncs.py +++ b/bpfin/prelim/prelimfuncs.py @@ -236,7 +236,7 @@ matplotlib.use('TkAgg') # c_base.append( # 1 / loan.payback) # loan amount / ratio = DS, targeting minimum total DS, equivalent to longest payback # bound_list.append((0, loan.max_amount)) -# c = c_base # c_base is target function +# c = c_base # c_base is the target function in linear programming # A = [-1] * len(loan_list) # b = [max(0 - total_cost, 0 - sum_loan_max)] # bounds = bound_list @@ -245,9 +245,7 @@ matplotlib.use('TkAgg') # res = linprog(c, A_ub=A, b_ub=b, bounds=bounds, options={'disp': False}) # for i in range(len(loan_list)): # loan_list[i].amount = res.x[i] - - -# # return res.x +# # return res.x # ##maybe we can use this, maybe not # # def transpose_matrix(matrix1): -- GitLab From 8d07d34add8e36efa3f2d701ff644906a9e1506e Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Tue, 25 Apr 2017 17:00:45 -0400 Subject: [PATCH 04/28] Minor update --- bpfin/financials/loan_allocation.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bpfin/financials/loan_allocation.py b/bpfin/financials/loan_allocation.py index afc186d..86c6cf8 100644 --- a/bpfin/financials/loan_allocation.py +++ b/bpfin/financials/loan_allocation.py @@ -24,12 +24,13 @@ def loan_allocation(): Return: dictonary: {{loan_id: amount}, {loan_id: amount}, {loan_id: amount}} """ + # run a linear programming to determine the result. # ****pre-request is: # quoted_cost <= sum_loan_max_amount + self_finance_max # # ****LP constrains are: - # 1. target function, determine minimum annual total DS. + # 1. objective function, determine minimum annual total DS. # x1/payback1, x2/payback2, x3/payback3 = minimum # * x is loan amount, no self-finance # 2. x variables constraint @@ -53,7 +54,7 @@ def loan_allocation(): # c_base.append( # 1 / loan.payback) # loan amount / ratio = DS, targeting minimum total DS, equivalent to longest payback # bound_list.append((0, loan.max_amount)) - # c = c_base # c_base is the target function in linear programming + # c = c_base # c_base is the objective function in linear programming # A = [-1] * len(loan_list) # b = [max(0 - total_cost, 0 - sum_loan_max)] # bounds = bound_list -- GitLab From 785e932f8bf4900289c975b410ae84dc150120b2 Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Tue, 25 Apr 2017 17:31:40 -0400 Subject: [PATCH 05/28] Merge from master --- bpfin/financials/loan_allocation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpfin/financials/loan_allocation.py b/bpfin/financials/loan_allocation.py index 86c6cf8..6360a97 100644 --- a/bpfin/financials/loan_allocation.py +++ b/bpfin/financials/loan_allocation.py @@ -24,7 +24,7 @@ def loan_allocation(): Return: dictonary: {{loan_id: amount}, {loan_id: amount}, {loan_id: amount}} """ - + # !!! think of loan term structure # run a linear programming to determine the result. # ****pre-request is: # quoted_cost <= sum_loan_max_amount + self_finance_max -- GitLab From d723b314c307ef7109cbdde89a95931514b5d0a8 Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Tue, 25 Apr 2017 22:50:33 -0400 Subject: [PATCH 06/28] Create form_loan_dict. # in fact, this should be done in backend. But let's make it happend here first --- bpfin/financials/financial_lib.py | 25 +++++++++++++++++++++++++ bpfin/financials/loan_allocation.py | 11 +++++++++++ bpfin/tests/testdata/sample_data.py | 5 +++++ 3 files changed, 41 insertions(+) diff --git a/bpfin/financials/financial_lib.py b/bpfin/financials/financial_lib.py index dcf1358..83ca264 100644 --- a/bpfin/financials/financial_lib.py +++ b/bpfin/financials/financial_lib.py @@ -160,3 +160,28 @@ def convert_income_statement_class(income_statement_class): 'total_opex': income_statement_class.total_opex, 'noi': income_statement_class.noi} return income_statement_dict + + +class Loan(): + def __init__(self, loan_term): + self.institute = loan_term['institute'] + self.max_amount = loan_term['max_amount'] + self.interest = loan_term['interest'] + self.duration = loan_term['duration'] + # todo interest rate!=0 + self.payback = ((1 + self.interest) ** self.duration - 1) / (self.interest * ( + 1 + self.interest) ** self.duration) # calculate loan's (loan amount/debt service), just like a payback + # self.startdate = loan_start_date + self.terms = [] + self.amount = 0 + self.debt_service = 0 + + def convert_loan_class(self): + loan_self_dict = {} + # convert loan class to dict + return loan_self_dict + + def originate(self, loan_start_date, loan_amount): + self.terms = bl.form_bill_calendar(loan_start_date, self.duration)[1] + self.amount = loan_amount + self.debt_service = self.amount / self.payback diff --git a/bpfin/financials/loan_allocation.py b/bpfin/financials/loan_allocation.py index 6360a97..4f79ca9 100644 --- a/bpfin/financials/loan_allocation.py +++ b/bpfin/financials/loan_allocation.py @@ -5,6 +5,17 @@ from bpfin.lib import other as lib import pprint +def form_loan_dict(loan_term_dict): + loan_dict = {} + for loan_id in loan_term_dict: + temp_loan = None + temp_loan = fl.Loan(loan_term_dict[loan_id]) + loan_dict[loan_id] = temp_loan.convert_loan_class() + return loan_dict + +print(form_loan_dict(db.loan_term_dict)) + + def loan_allocation(): """ Take inputs of available financing options(loans and self-finance), and other constrains, diff --git a/bpfin/tests/testdata/sample_data.py b/bpfin/tests/testdata/sample_data.py index b7a8053..c0762cf 100644 --- a/bpfin/tests/testdata/sample_data.py +++ b/bpfin/tests/testdata/sample_data.py @@ -494,6 +494,11 @@ liability_dictionary = { 'debt2': (100, 'NYCEEC', 20, datetime.date(2012, 8, 31)) } +# loan_options +loan_term_1 = {'institute': 'NYSERDA', 'max_amount': 50000, 'interest': 0.025, 'duration': 120} +loan_term_2 = {'institute': 'Joe Fund', 'max_amount': 10000, 'interest': 0.03, 'duration': 84} +loan_term_3 = {'institute': 'Tooraj Capital', 'max_amount': 100000, 'interest': 0.07, 'duration': 60} +loan_term_dict = {1: loan_term_1, 2: loan_term_2, 3: loan_term_3} # pro-forma date and bill projection - # proforma_date_from, proforma_date_to -- GitLab From e0484c9ddc82c43e6bd1c52a376ffea90c0899c7 Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Tue, 25 Apr 2017 22:51:17 -0400 Subject: [PATCH 07/28] Comment --- bpfin/financials/loan_allocation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bpfin/financials/loan_allocation.py b/bpfin/financials/loan_allocation.py index 4f79ca9..3c8819b 100644 --- a/bpfin/financials/loan_allocation.py +++ b/bpfin/financials/loan_allocation.py @@ -6,6 +6,7 @@ import pprint def form_loan_dict(loan_term_dict): + # maybe this should happen at backend, by passing in Loan class directly loan_dict = {} for loan_id in loan_term_dict: temp_loan = None -- GitLab From 3122979595f063218778fdfe86e5a160f6a1f42b Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Wed, 26 Apr 2017 11:09:12 -0400 Subject: [PATCH 08/28] Test math logic in data_generation --- bpfin/financials/financial_lib.py | 9 ++++++++- bpfin/financials/loan_allocation.py | 9 +++++---- bpfin/utilbills/data_generation.py | 20 ++++++++++++++++++++ 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/bpfin/financials/financial_lib.py b/bpfin/financials/financial_lib.py index 83ca264..d6702f2 100644 --- a/bpfin/financials/financial_lib.py +++ b/bpfin/financials/financial_lib.py @@ -178,7 +178,14 @@ class Loan(): def convert_loan_class(self): loan_self_dict = {} - # convert loan class to dict + loan_self_dict['institute'] = self.institute + loan_self_dict['max_amount'] = self.max_amount + loan_self_dict['interest'] = self.interest + loan_self_dict['duration'] = self.duration + loan_self_dict['payback'] = self.payback + loan_self_dict['terms'] = self.terms + loan_self_dict['amount'] = self.amount = 0 + loan_self_dict['debt_service'] = self.debt_service = 0 return loan_self_dict def originate(self, loan_start_date, loan_amount): diff --git a/bpfin/financials/loan_allocation.py b/bpfin/financials/loan_allocation.py index 3c8819b..45401d1 100644 --- a/bpfin/financials/loan_allocation.py +++ b/bpfin/financials/loan_allocation.py @@ -11,13 +11,12 @@ def form_loan_dict(loan_term_dict): for loan_id in loan_term_dict: temp_loan = None temp_loan = fl.Loan(loan_term_dict[loan_id]) - loan_dict[loan_id] = temp_loan.convert_loan_class() + loan_dict[loan_id] = temp_loan + # loan_dict[loan_id] = temp_loan.convert_loan_class() return loan_dict -print(form_loan_dict(db.loan_term_dict)) - -def loan_allocation(): +def loan_allocation(loan_dict): """ Take inputs of available financing options(loans and self-finance), and other constrains, determine the best financing plan, with minimum debt service. @@ -36,6 +35,7 @@ def loan_allocation(): Return: dictonary: {{loan_id: amount}, {loan_id: amount}, {loan_id: amount}} """ + sum_loan_max_amount = 0 # !!! think of loan term structure # run a linear programming to determine the result. # ****pre-request is: @@ -55,6 +55,7 @@ def loan_allocation(): # 3. x variables bounds # 0 <= x[i] <= loan[i]_max_amount +loan_dict = print(form_loan_dict(db.loan_term_dict)) # in class format # # cauclate loan amount should be borrowed from each bank, given assumed total cost and loan info # def loan_allocate(total_cost, loan_list): diff --git a/bpfin/utilbills/data_generation.py b/bpfin/utilbills/data_generation.py index d7219d8..3b5600e 100644 --- a/bpfin/utilbills/data_generation.py +++ b/bpfin/utilbills/data_generation.py @@ -11,3 +11,23 @@ # print(annual_bill_electricity) +class test_stru(): + def __init__(self, number): + self.number = number + + +test1 = test_stru(11) +test2 = test_stru(22) +test3 = test_stru(33) +test_dict = {1: test1, 2: test2, 3: test3} + +# sum_up = 0 +# sum_up = sum_up + test_dict[i].number for i in test_dict + + +def add_xy(x,y): + return x+y + +sum_up = reduce(add_xy, [1, 2, 3]) +# print(test_dict.values()) +print('\n', sum_up) -- GitLab From b2ae4bb3427797dc3cf6790ccca076294b210aba Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Wed, 26 Apr 2017 12:47:04 -0400 Subject: [PATCH 09/28] Add cost vs available amount comparison in loan_allocation --- bpfin/financials/financial_lib.py | 25 ++++++++++++------------- bpfin/financials/loan_allocation.py | 28 ++++++++++++++++++++-------- bpfin/tests/testdata/sample_data.py | 9 ++++++++- bpfin/utilbills/data_generation.py | 21 --------------------- 4 files changed, 40 insertions(+), 43 deletions(-) diff --git a/bpfin/financials/financial_lib.py b/bpfin/financials/financial_lib.py index d6702f2..f729e73 100644 --- a/bpfin/financials/financial_lib.py +++ b/bpfin/financials/financial_lib.py @@ -176,19 +176,18 @@ class Loan(): self.amount = 0 self.debt_service = 0 - def convert_loan_class(self): - loan_self_dict = {} - loan_self_dict['institute'] = self.institute - loan_self_dict['max_amount'] = self.max_amount - loan_self_dict['interest'] = self.interest - loan_self_dict['duration'] = self.duration - loan_self_dict['payback'] = self.payback - loan_self_dict['terms'] = self.terms - loan_self_dict['amount'] = self.amount = 0 - loan_self_dict['debt_service'] = self.debt_service = 0 - return loan_self_dict - - def originate(self, loan_start_date, loan_amount): + def get_loan_terms(self): + return { + 'institute': self.institute, + 'max_amount': self.max_amount, + 'interest': self.interest, + 'duration': self.duration, + 'payback': self.payback, + 'terms': self.terms, + 'amount': self.amount, + 'debt_service': self.debt_service} + + def loan_originate(self, loan_start_date, loan_amount): self.terms = bl.form_bill_calendar(loan_start_date, self.duration)[1] self.amount = loan_amount self.debt_service = self.amount / self.payback diff --git a/bpfin/financials/loan_allocation.py b/bpfin/financials/loan_allocation.py index 45401d1..0033812 100644 --- a/bpfin/financials/loan_allocation.py +++ b/bpfin/financials/loan_allocation.py @@ -5,18 +5,18 @@ from bpfin.lib import other as lib import pprint -def form_loan_dict(loan_term_dict): +def form_loan_list(loan_term_dict): # maybe this should happen at backend, by passing in Loan class directly - loan_dict = {} + loan_list = [] for loan_id in loan_term_dict: temp_loan = None temp_loan = fl.Loan(loan_term_dict[loan_id]) - loan_dict[loan_id] = temp_loan - # loan_dict[loan_id] = temp_loan.convert_loan_class() - return loan_dict + loan_list.append(temp_loan) + # loan_list[loan_id] = temp_loan.convert_loan_class() + return loan_list -def loan_allocation(loan_dict): +def loan_allocation(loan_list, customer_preference): """ Take inputs of available financing options(loans and self-finance), and other constrains, determine the best financing plan, with minimum debt service. @@ -36,10 +36,18 @@ def loan_allocation(loan_dict): dictonary: {{loan_id: amount}, {loan_id: amount}, {loan_id: amount}} """ sum_loan_max_amount = 0 + for loan in loan_list: + sum_loan_max_amount += loan.get_loan_terms()['max_amount'] + print('\n', sum_loan_max_amount) + + if (sum_loan_max_amount + customer_preference['downpayment_max']) <= 180000: + print('not financiable') + return None + return True # !!! think of loan term structure # run a linear programming to determine the result. # ****pre-request is: - # quoted_cost <= sum_loan_max_amount + self_finance_max + # quoted_cost <= sum_loan_max_amount + downpayment_max # # ****LP constrains are: # 1. objective function, determine minimum annual total DS. @@ -55,7 +63,11 @@ def loan_allocation(loan_dict): # 3. x variables bounds # 0 <= x[i] <= loan[i]_max_amount -loan_dict = print(form_loan_dict(db.loan_term_dict)) # in class format +# loan_list = print(form_loan_list(db.loan_term_dict)) # output is in objective format +loan_allocation( + form_loan_list(db.loan_term_dict), + db.customer_preference) + # # cauclate loan amount should be borrowed from each bank, given assumed total cost and loan info # def loan_allocate(total_cost, loan_list): diff --git a/bpfin/tests/testdata/sample_data.py b/bpfin/tests/testdata/sample_data.py index c0762cf..1676d7f 100644 --- a/bpfin/tests/testdata/sample_data.py +++ b/bpfin/tests/testdata/sample_data.py @@ -494,12 +494,19 @@ liability_dictionary = { 'debt2': (100, 'NYCEEC', 20, datetime.date(2012, 8, 31)) } -# loan_options +# Loan Options loan_term_1 = {'institute': 'NYSERDA', 'max_amount': 50000, 'interest': 0.025, 'duration': 120} loan_term_2 = {'institute': 'Joe Fund', 'max_amount': 10000, 'interest': 0.03, 'duration': 84} loan_term_3 = {'institute': 'Tooraj Capital', 'max_amount': 100000, 'interest': 0.07, 'duration': 60} loan_term_dict = {1: loan_term_1, 2: loan_term_2, 3: loan_term_3} +# Customer Preference +customer_preference = { + 'downpayment_max': 5000, + 'expected_payback': 120, # in months + 'cust_noi_dscr': 1.15 +} + # pro-forma date and bill projection - # proforma_date_from, proforma_date_to # prior_rough, post_rough diff --git a/bpfin/utilbills/data_generation.py b/bpfin/utilbills/data_generation.py index 3b5600e..13ce46c 100644 --- a/bpfin/utilbills/data_generation.py +++ b/bpfin/utilbills/data_generation.py @@ -10,24 +10,3 @@ # annual_bill_electricity = bl.annualizing_projection(prior_bill_electricity['date_to'], prior_bill_electricity['charge']) # print(annual_bill_electricity) - -class test_stru(): - def __init__(self, number): - self.number = number - - -test1 = test_stru(11) -test2 = test_stru(22) -test3 = test_stru(33) -test_dict = {1: test1, 2: test2, 3: test3} - -# sum_up = 0 -# sum_up = sum_up + test_dict[i].number for i in test_dict - - -def add_xy(x,y): - return x+y - -sum_up = reduce(add_xy, [1, 2, 3]) -# print(test_dict.values()) -print('\n', sum_up) -- GitLab From f75fd40926d49705e1491e11a1363648cde589ec Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Wed, 26 Apr 2017 12:54:30 -0400 Subject: [PATCH 10/28] Add Scenario dictionary in sample data and in loan_allocation --- bpfin/financials/loan_allocation.py | 12 ++++++++---- bpfin/tests/testdata/sample_data.py | 5 +++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/bpfin/financials/loan_allocation.py b/bpfin/financials/loan_allocation.py index 0033812..9c65820 100644 --- a/bpfin/financials/loan_allocation.py +++ b/bpfin/financials/loan_allocation.py @@ -16,7 +16,7 @@ def form_loan_list(loan_term_dict): return loan_list -def loan_allocation(loan_list, customer_preference): +def loan_allocation(loan_list, customer_preference, scenario): """ Take inputs of available financing options(loans and self-finance), and other constrains, determine the best financing plan, with minimum debt service. @@ -38,11 +38,14 @@ def loan_allocation(loan_list, customer_preference): sum_loan_max_amount = 0 for loan in loan_list: sum_loan_max_amount += loan.get_loan_terms()['max_amount'] - print('\n', sum_loan_max_amount) + print('\n sum_loan_max_amount=', sum_loan_max_amount) - if (sum_loan_max_amount + customer_preference['downpayment_max']) <= 180000: + # pre-request judge: project cost should be lower than total available financing + if (sum_loan_max_amount + customer_preference['downpayment_max']) <= scenario['cost_estimation']: print('not financiable') return None + + return True # !!! think of loan term structure # run a linear programming to determine the result. @@ -66,7 +69,8 @@ def loan_allocation(loan_list, customer_preference): # loan_list = print(form_loan_list(db.loan_term_dict)) # output is in objective format loan_allocation( form_loan_list(db.loan_term_dict), - db.customer_preference) + db.customer_preference, + db.scenario) # # cauclate loan amount should be borrowed from each bank, given assumed total cost and loan info diff --git a/bpfin/tests/testdata/sample_data.py b/bpfin/tests/testdata/sample_data.py index 1676d7f..b153601 100644 --- a/bpfin/tests/testdata/sample_data.py +++ b/bpfin/tests/testdata/sample_data.py @@ -507,6 +507,11 @@ customer_preference = { 'cust_noi_dscr': 1.15 } +# Engineering Scenarios +scenario = { + 'cost_estimation': 150000 +} + # pro-forma date and bill projection - # proforma_date_from, proforma_date_to # prior_rough, post_rough -- GitLab From 20d38e7ea3867663511d8c7d46c96c6866c9ae8a Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Wed, 26 Apr 2017 15:27:48 -0400 Subject: [PATCH 11/28] Minor modify on loan_allocation. Switch to re-structure income statement --- bpfin/financials/loan_allocation.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bpfin/financials/loan_allocation.py b/bpfin/financials/loan_allocation.py index 9c65820..f07b86b 100644 --- a/bpfin/financials/loan_allocation.py +++ b/bpfin/financials/loan_allocation.py @@ -24,7 +24,7 @@ def loan_allocation(loan_list, customer_preference, scenario): construction_date construction_cost income_statement_prior - income_statement_prior + income_statement_post cash_balance liability available_loans @@ -44,6 +44,9 @@ def loan_allocation(loan_list, customer_preference, scenario): if (sum_loan_max_amount + customer_preference['downpayment_max']) <= scenario['cost_estimation']: print('not financiable') return None + # linear programming (LP) + # LP constrains + # objective function: x1/payback1, x2/payback2, x3/payback3 = minimum return True -- GitLab From a84f50c7e42bdf2605e8a141d9701db098dc08c4 Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Thu, 27 Apr 2017 12:46:07 -0400 Subject: [PATCH 12/28] Improve loan_allocation. Fix Loan class payback calculation. Add sample data of DSCR --- bpfin/financials/financial_lib.py | 12 +++--- bpfin/financials/loan_allocation.py | 59 ++++++++++++++++++++++------- bpfin/tests/testdata/sample_data.py | 5 +++ 3 files changed, 58 insertions(+), 18 deletions(-) diff --git a/bpfin/financials/financial_lib.py b/bpfin/financials/financial_lib.py index f729e73..311f0b0 100644 --- a/bpfin/financials/financial_lib.py +++ b/bpfin/financials/financial_lib.py @@ -167,15 +167,17 @@ class Loan(): self.institute = loan_term['institute'] self.max_amount = loan_term['max_amount'] self.interest = loan_term['interest'] - self.duration = loan_term['duration'] + self.duration = loan_term['duration'] # in month # todo interest rate!=0 - self.payback = ((1 + self.interest) ** self.duration - 1) / (self.interest * ( - 1 + self.interest) ** self.duration) # calculate loan's (loan amount/debt service), just like a payback - # self.startdate = loan_start_date + + # calculate loan's (loan amount/debt service), similar to payback year + self.payback = ((1 + self.interest) ** (self.duration / 12) - 1) / (self.interest * ( + 1 + self.interest) ** (self.duration / 12)) + self.terms = [] self.amount = 0 self.debt_service = 0 - + # self.startdate = loan_start_date def get_loan_terms(self): return { 'institute': self.institute, diff --git a/bpfin/financials/loan_allocation.py b/bpfin/financials/loan_allocation.py index f07b86b..b9d2036 100644 --- a/bpfin/financials/loan_allocation.py +++ b/bpfin/financials/loan_allocation.py @@ -2,6 +2,7 @@ import bpfin.financials.financial_lib as fl import bpfin.tests.testdata.sample_data as db import bpfin.utilbills.bill_lib as bl from bpfin.lib import other as lib +from scipy.optimize import linprog import pprint @@ -16,11 +17,14 @@ def form_loan_list(loan_term_dict): return loan_list -def loan_allocation(loan_list, customer_preference, scenario): +def loan_allocation(loan_list, customer_preference, scenario, req_dscr): """ Take inputs of available financing options(loans and self-finance), and other constrains, determine the best financing plan, with minimum debt service. Args: + loan_list (list): list of objectives of loans + customer_preference (dictionary): customer preference for financing plan + req_dscr(dictionary): required dscr construction_date construction_cost income_statement_prior @@ -28,27 +32,54 @@ def loan_allocation(loan_list, customer_preference, scenario): cash_balance liability available_loans - customer_preference required_cash_DSCR required_noi_DSCR required_saving_DSCR Return: dictonary: {{loan_id: amount}, {loan_id: amount}, {loan_id: amount}} + can be list of loan objectives, or get from loan class + Description: + customer_preference = { + 'downpayment_max': 5000, + 'expected_payback': 120, # in months + 'cust_noi_dscr': 1.15} + Note: + dscr == debt service coverage ratio + """ - sum_loan_max_amount = 0 - for loan in loan_list: - sum_loan_max_amount += loan.get_loan_terms()['max_amount'] + first_year_saving = 30000 #!!!! need IS work, actually can be min_annual_saving + first_year_noi = 30000 #!!!! need IS work, actually can be min_annual_noi + first_year_cash = 10000 #!!!! need IS work, actually can be min_annual_cash + sum_loan_max_amount = sum(list(current_loan.max_amount for current_loan in loan_list)) print('\n sum_loan_max_amount=', sum_loan_max_amount) - # pre-request judge: project cost should be lower than total available financing if (sum_loan_max_amount + customer_preference['downpayment_max']) <= scenario['cost_estimation']: - print('not financiable') + print('alert: not financiable') return None - # linear programming (LP) - # LP constrains - # objective function: x1/payback1, x2/payback2, x3/payback3 = minimum - + # linear programming (LP) + # LP constrains + # objective function: x1/payback1, x2/payback2, x3/payback3 = minimum + c = list(1 / current_loan.payback for current_loan in loan_list) + print('\n obj fun =', c) + # x variables constraint, x1 + x2 + x3 >= max (financing amount >= project cost) + # max = min(project_cost, sum_loan_max_amount, saving/dscr, noi/dscr, cash/dscr) + A = [-1] * len(loan_list) # -x1 -x2 -x3 <= -max + b = [max( + 0 - scenario['cost_estimation'], + 0 - sum_loan_max_amount, + 0 - first_year_saving / req_dscr['req_saving_dscr'], + 0 - first_year_noi / req_dscr['req_noi_dscr'], + # 0 - first_year_noi / customer_preference['cust_noi_dscr'], + 0 - first_year_cash / req_dscr['req_cash_dscr'] + )] + print('\n', A, '<=', b) + # x variables bounds, 0 <= x[i] <= loan[i]_max_amount + bound_list = list((0, current_loan.max_amount) for current_loan in loan_list) + print('bound_list =', bound_list) + # LP calculation + res = linprog(c, A_ub=A, b_ub=b, bounds=bound_list, options={'disp': True}) + print('x1=', res.x[0], 'x2=', res.x[1], 'x3=', res.x[2]) return True # !!! think of loan term structure # run a linear programming to determine the result. @@ -62,18 +93,20 @@ def loan_allocation(loan_list, customer_preference, scenario): # 2. x variables constraint # x1 + x2 + x3 <= max # max = min(project_cost, sum_loan_max_amount, saving/dscr, noi/dscr, cash/dscr) - # * default assumption is customer wants to self-finance but us loan first + # * default assumption is customer wants to self-finance but use loan first # self-finance amount = project_cost - sum_x # ** if customer wants to use self-fiance first, then # project_cost = quoted_cost - self_finance_max # 3. x variables bounds # 0 <= x[i] <= loan[i]_max_amount + # loan_list = print(form_loan_list(db.loan_term_dict)) # output is in objective format loan_allocation( form_loan_list(db.loan_term_dict), db.customer_preference, - db.scenario) + db.scenario, + db.req_dscr) # # cauclate loan amount should be borrowed from each bank, given assumed total cost and loan info diff --git a/bpfin/tests/testdata/sample_data.py b/bpfin/tests/testdata/sample_data.py index b153601..a810690 100644 --- a/bpfin/tests/testdata/sample_data.py +++ b/bpfin/tests/testdata/sample_data.py @@ -499,6 +499,11 @@ loan_term_1 = {'institute': 'NYSERDA', 'max_amount': 50000, 'interest': 0.025, ' loan_term_2 = {'institute': 'Joe Fund', 'max_amount': 10000, 'interest': 0.03, 'duration': 84} loan_term_3 = {'institute': 'Tooraj Capital', 'max_amount': 100000, 'interest': 0.07, 'duration': 60} loan_term_dict = {1: loan_term_1, 2: loan_term_2, 3: loan_term_3} +req_dscr = { + 'req_noi_dscr': 1.15, + 'req_cash_dscr': 1.15, + 'req_saving_dscr': 1.15 +} # Customer Preference customer_preference = { -- GitLab From 24f69fd6b60936793ef9f2b524fa5476383cb9c9 Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Thu, 27 Apr 2017 15:51:36 -0400 Subject: [PATCH 13/28] Create Loan_List class, to allocate financing sources, and to originate loans in list --- bpfin/financials/financial_lib.py | 46 +++++++++++++- bpfin/financials/loan_allocation.py | 62 ++++++++++--------- .../test_financials/test_financial_lib.py | 3 - bpfin/tests/testdata/sample_data.py | 14 +++-- 4 files changed, 86 insertions(+), 39 deletions(-) diff --git a/bpfin/financials/financial_lib.py b/bpfin/financials/financial_lib.py index 311f0b0..a3fbc5c 100644 --- a/bpfin/financials/financial_lib.py +++ b/bpfin/financials/financial_lib.py @@ -3,7 +3,10 @@ # from bpfin.lib import other as lib import bpfin.utilbills.bill_lib as bl from bpfin.tests.testdata import sample_data as db +from bpfin.financials.loan_allocation import loan_allocation +import copy # import pprint +import datetime def income_statement_single_year(year, income_input, bill_overview): @@ -178,6 +181,7 @@ class Loan(): self.amount = 0 self.debt_service = 0 # self.startdate = loan_start_date + def get_loan_terms(self): return { 'institute': self.institute, @@ -189,7 +193,45 @@ class Loan(): 'amount': self.amount, 'debt_service': self.debt_service} - def loan_originate(self, loan_start_date, loan_amount): + def put_originate(self, loan_start_date): self.terms = bl.form_bill_calendar(loan_start_date, self.duration)[1] - self.amount = loan_amount self.debt_service = self.amount / self.payback + + +class Loan_List(): + def __init__(self, loan_input_list): + self.loan_list = [] + for current_loan in loan_input_list: + temp_loan = None + temp_loan = Loan(current_loan) + self.loan_list.append(temp_loan) + # loan_list[loan_id] = temp_loan.convert_loan_class() + # return loan_list + + def allocate(self, customer_preference, scenario, req_dscr): + current_loan_list = copy.deepcopy(self.loan_list) + allocation = loan_allocation( + current_loan_list, customer_preference, scenario, req_dscr) + for current_loan, current_amount in zip(current_loan_list, allocation): + current_loan.amount = current_amount + return current_loan_list # a list of objectives of loans, with amount, not schedule + + def get_schedule(self, customer_preference, scenario, req_dscr, loan_start_date): + allocated_loan_list = self.allocate(customer_preference, scenario, req_dscr) + for current_loan in allocated_loan_list: + current_loan.put_originate(loan_start_date) + return allocated_loan_list + +# ****** ugly tests ********** +loan_list = Loan_List(db.loan_input_list) +# scenario2_loan_list = loan_list.allocate( +# db.customer_preference, +# db.scenario, +# db.req_dscr) +# print(scenario2_loan_list) +allocated = loan_list.get_schedule( + db.customer_preference, + db.scenario, + db.req_dscr, + datetime.date(2017, 1, 12)) +print(allocated[2].terms) diff --git a/bpfin/financials/loan_allocation.py b/bpfin/financials/loan_allocation.py index b9d2036..d592caa 100644 --- a/bpfin/financials/loan_allocation.py +++ b/bpfin/financials/loan_allocation.py @@ -1,4 +1,4 @@ -import bpfin.financials.financial_lib as fl +# import bpfin.financials.financial_lib as fl import bpfin.tests.testdata.sample_data as db import bpfin.utilbills.bill_lib as bl from bpfin.lib import other as lib @@ -6,15 +6,15 @@ from scipy.optimize import linprog import pprint -def form_loan_list(loan_term_dict): - # maybe this should happen at backend, by passing in Loan class directly - loan_list = [] - for loan_id in loan_term_dict: - temp_loan = None - temp_loan = fl.Loan(loan_term_dict[loan_id]) - loan_list.append(temp_loan) - # loan_list[loan_id] = temp_loan.convert_loan_class() - return loan_list +# def form_loan_list(loan_term_dict): +# # maybe this should happen at backend, by passing in Loan class directly +# loan_list = [] +# for loan_id in loan_term_dict: +# temp_loan = None +# temp_loan = fl.Loan(loan_term_dict[loan_id]) +# loan_list.append(temp_loan) +# # loan_list[loan_id] = temp_loan.convert_loan_class() +# return loan_list def loan_allocation(loan_list, customer_preference, scenario, req_dscr): @@ -47,9 +47,9 @@ def loan_allocation(loan_list, customer_preference, scenario, req_dscr): dscr == debt service coverage ratio """ - first_year_saving = 30000 #!!!! need IS work, actually can be min_annual_saving - first_year_noi = 30000 #!!!! need IS work, actually can be min_annual_noi - first_year_cash = 10000 #!!!! need IS work, actually can be min_annual_cash + first_year_saving = 28000 #!!!! need IS work, actually can be min_annual_saving + first_year_noi = 28000 #!!!! need IS work, actually can be min_annual_noi + first_year_cash = 100000 #!!!! need IS work, actually can be min_annual_cash sum_loan_max_amount = sum(list(current_loan.max_amount for current_loan in loan_list)) print('\n sum_loan_max_amount=', sum_loan_max_amount) # pre-request judge: project cost should be lower than total available financing @@ -64,23 +64,29 @@ def loan_allocation(loan_list, customer_preference, scenario, req_dscr): print('\n obj fun =', c) # x variables constraint, x1 + x2 + x3 >= max (financing amount >= project cost) # max = min(project_cost, sum_loan_max_amount, saving/dscr, noi/dscr, cash/dscr) - A = [-1] * len(loan_list) # -x1 -x2 -x3 <= -max - b = [max( - 0 - scenario['cost_estimation'], - 0 - sum_loan_max_amount, - 0 - first_year_saving / req_dscr['req_saving_dscr'], - 0 - first_year_noi / req_dscr['req_noi_dscr'], + A = [] + A.append([-1] * len(loan_list)) # -x1 -x2 -x3 <= -max + b = [] + b.append( + 0 - scenario['cost_estimation'] + # 0 - sum_loan_max_amount, + ) + A.append(c) + b.append(min( + first_year_saving / req_dscr['req_saving_dscr'], + first_year_noi / req_dscr['req_noi_dscr'], + first_year_cash / req_dscr['req_cash_dscr'] # 0 - first_year_noi / customer_preference['cust_noi_dscr'], - 0 - first_year_cash / req_dscr['req_cash_dscr'] - )] + )) print('\n', A, '<=', b) # x variables bounds, 0 <= x[i] <= loan[i]_max_amount bound_list = list((0, current_loan.max_amount) for current_loan in loan_list) print('bound_list =', bound_list) - # LP calculation + # LP calculation. x[i] == loan amount from loan[i] res = linprog(c, A_ub=A, b_ub=b, bounds=bound_list, options={'disp': True}) print('x1=', res.x[0], 'x2=', res.x[1], 'x3=', res.x[2]) - return True + + return res.x # !!! think of loan term structure # run a linear programming to determine the result. # ****pre-request is: @@ -102,11 +108,11 @@ def loan_allocation(loan_list, customer_preference, scenario, req_dscr): # loan_list = print(form_loan_list(db.loan_term_dict)) # output is in objective format -loan_allocation( - form_loan_list(db.loan_term_dict), - db.customer_preference, - db.scenario, - db.req_dscr) +# loan_allocation( +# form_loan_list(db.loan_term_dict), +# db.customer_preference, +# db.scenario, +# db.req_dscr) # # cauclate loan amount should be borrowed from each bank, given assumed total cost and loan info diff --git a/bpfin/tests/test_financials/test_financial_lib.py b/bpfin/tests/test_financials/test_financial_lib.py index b756a24..9e8c487 100644 --- a/bpfin/tests/test_financials/test_financial_lib.py +++ b/bpfin/tests/test_financials/test_financial_lib.py @@ -18,6 +18,3 @@ def test_income_statement_single_year(): output_dict = db.income_statement_2016 result_dict = fl.income_statement_single_year(2016, input_income, input_bill_overview_organized) assert output_dict == result_dict - - - diff --git a/bpfin/tests/testdata/sample_data.py b/bpfin/tests/testdata/sample_data.py index a810690..f4a78ac 100644 --- a/bpfin/tests/testdata/sample_data.py +++ b/bpfin/tests/testdata/sample_data.py @@ -495,26 +495,28 @@ liability_dictionary = { } # Loan Options -loan_term_1 = {'institute': 'NYSERDA', 'max_amount': 50000, 'interest': 0.025, 'duration': 120} -loan_term_2 = {'institute': 'Joe Fund', 'max_amount': 10000, 'interest': 0.03, 'duration': 84} -loan_term_3 = {'institute': 'Tooraj Capital', 'max_amount': 100000, 'interest': 0.07, 'duration': 60} +loan_term_1 = {'institute': 'NYSERDA', 'max_amount': 100000, 'interest': 0.08, 'duration': 120} +loan_term_2 = {'institute': 'Joe Fund', 'max_amount': 50000, 'interest': 0.05, 'duration': 108} +loan_term_3 = {'institute': 'Tooraj Capital', 'max_amount': 100000, 'interest': 0.07, 'duration': 114} loan_term_dict = {1: loan_term_1, 2: loan_term_2, 3: loan_term_3} +loan_input_list = [loan_term_1, loan_term_2, loan_term_3] + req_dscr = { 'req_noi_dscr': 1.15, 'req_cash_dscr': 1.15, - 'req_saving_dscr': 1.15 + 'req_saving_dscr': 1.10 } # Customer Preference customer_preference = { - 'downpayment_max': 5000, + 'downpayment_max': 5000.00, 'expected_payback': 120, # in months 'cust_noi_dscr': 1.15 } # Engineering Scenarios scenario = { - 'cost_estimation': 150000 + 'cost_estimation': 150000.00 } # pro-forma date and bill projection - -- GitLab From c31a35834895d0f118cba537d68dcaf9052aca49 Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Thu, 27 Apr 2017 17:07:23 -0400 Subject: [PATCH 14/28] Create loan schedule calculation in Loan.originate() --- bpfin/financials/financial_lib.py | 62 ++++++++++++++++++++++++----- bpfin/tests/testdata/sample_data.py | 4 +- bpfin/utilbills/bill_lib.py | 2 +- 3 files changed, 54 insertions(+), 14 deletions(-) diff --git a/bpfin/financials/financial_lib.py b/bpfin/financials/financial_lib.py index a3fbc5c..492b206 100644 --- a/bpfin/financials/financial_lib.py +++ b/bpfin/financials/financial_lib.py @@ -181,6 +181,16 @@ class Loan(): self.amount = 0 self.debt_service = 0 # self.startdate = loan_start_date + self.principal_start_balance = [] + self.principal_end_balance = [] + self.interest_due = [] + self.principal_due = [] + self.debt_service_due = [] + # self.principal_start_balance = align2term([], [], self.loan_terms) + # self.principal_end_balance = align2term([], [], self.loan_terms) + # self.interest_repay = align2term([], [], self.loan_terms) + # self.principal_repay = align2term([], [], self.loan_terms) + # self.debt_service = align2term([], [], self.loan_terms) def get_loan_terms(self): return { @@ -193,9 +203,41 @@ class Loan(): 'amount': self.amount, 'debt_service': self.debt_service} - def put_originate(self, loan_start_date): - self.terms = bl.form_bill_calendar(loan_start_date, self.duration)[1] - self.debt_service = self.amount / self.payback + def put_amount(self, amount): + self.amount = amount + self.debt_service = self.amount * ((self.interest / 12) * (1 + self.interest / 12) ** (self.duration)) / ( + (1 + self.interest / 12) ** (self.duration) - 1) + + def put_terms(self, loan_start_date): + self.terms = bl.form_bill_calendar(loan_start_date, float(self.duration)/12)[1] + + def originate(self): + # if terms = None, return 'missing start_data' + debt_service = self.debt_service + # debt_service = self.amount / self.payback + principal_start_balance = [] + principal_end_balance = [] + interest_due = [] + principal_due = [] + debt_service_due = [] + + start_balance = self.amount + for termID in self.terms: + interest_amount = start_balance * self.interest / 12 + principal_amount = debt_service - interest_amount + + principal_start_balance.append(start_balance) + interest_due.append(interest_amount) + debt_service_due.append(debt_service) + principal_due.append(principal_amount) + principal_end_balance.append( + start_balance - principal_amount if (start_balance - principal_amount) >= 10e-5 else 0) + start_balance = start_balance - principal_amount + self.principal_start_balance = principal_start_balance + self.principal_end_balance = principal_end_balance + self.interest_due = interest_due + self.principal_due = principal_due + self.debt_service_due = debt_service_due class Loan_List(): @@ -213,25 +255,23 @@ class Loan_List(): allocation = loan_allocation( current_loan_list, customer_preference, scenario, req_dscr) for current_loan, current_amount in zip(current_loan_list, allocation): - current_loan.amount = current_amount + current_loan.put_amount(current_amount) return current_loan_list # a list of objectives of loans, with amount, not schedule def get_schedule(self, customer_preference, scenario, req_dscr, loan_start_date): allocated_loan_list = self.allocate(customer_preference, scenario, req_dscr) for current_loan in allocated_loan_list: - current_loan.put_originate(loan_start_date) + current_loan.put_terms(loan_start_date) + current_loan.originate() return allocated_loan_list + # ****** ugly tests ********** loan_list = Loan_List(db.loan_input_list) -# scenario2_loan_list = loan_list.allocate( -# db.customer_preference, -# db.scenario, -# db.req_dscr) -# print(scenario2_loan_list) allocated = loan_list.get_schedule( db.customer_preference, db.scenario, db.req_dscr, datetime.date(2017, 1, 12)) -print(allocated[2].terms) +print(allocated[0].terms) +print(allocated[0].principal_end_balance) diff --git a/bpfin/tests/testdata/sample_data.py b/bpfin/tests/testdata/sample_data.py index f4a78ac..898efff 100644 --- a/bpfin/tests/testdata/sample_data.py +++ b/bpfin/tests/testdata/sample_data.py @@ -495,9 +495,9 @@ liability_dictionary = { } # Loan Options -loan_term_1 = {'institute': 'NYSERDA', 'max_amount': 100000, 'interest': 0.08, 'duration': 120} +loan_term_1 = {'institute': 'NYSERDA', 'max_amount': 500000, 'interest': 0.08, 'duration': 120} loan_term_2 = {'institute': 'Joe Fund', 'max_amount': 50000, 'interest': 0.05, 'duration': 108} -loan_term_3 = {'institute': 'Tooraj Capital', 'max_amount': 100000, 'interest': 0.07, 'duration': 114} +loan_term_3 = {'institute': 'Tooraj Capital', 'max_amount': 75000, 'interest': 0.07, 'duration': 114} loan_term_dict = {1: loan_term_1, 2: loan_term_2, 3: loan_term_3} loan_input_list = [loan_term_1, loan_term_2, loan_term_3] diff --git a/bpfin/utilbills/bill_lib.py b/bpfin/utilbills/bill_lib.py index 8df3fd0..f7d71c4 100644 --- a/bpfin/utilbills/bill_lib.py +++ b/bpfin/utilbills/bill_lib.py @@ -42,7 +42,7 @@ def form_bill_calendar(date_start, year_term): bdstart = [] bdend = [] day = date_start - for term in range(12 * year_term): + for term in range(int(12 * year_term)): first = datetime.date(day=1, month=day.month, year=day.year) bdstart.append(first) last = datetime.date(day.year, day.month, cal_last_day(day.year, day.month)) -- GitLab From cf8bc20863c0c18fa2eb2bb6035cb8cbe16d406b Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Thu, 27 Apr 2017 17:15:27 -0400 Subject: [PATCH 15/28] Clean comments in Loan and Loan_List class --- bpfin/financials/financial_lib.py | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/bpfin/financials/financial_lib.py b/bpfin/financials/financial_lib.py index 492b206..7549dd4 100644 --- a/bpfin/financials/financial_lib.py +++ b/bpfin/financials/financial_lib.py @@ -6,7 +6,7 @@ from bpfin.tests.testdata import sample_data as db from bpfin.financials.loan_allocation import loan_allocation import copy # import pprint -import datetime +# import datetime def income_statement_single_year(year, income_input, bill_overview): @@ -172,25 +172,17 @@ class Loan(): self.interest = loan_term['interest'] self.duration = loan_term['duration'] # in month # todo interest rate!=0 - # calculate loan's (loan amount/debt service), similar to payback year self.payback = ((1 + self.interest) ** (self.duration / 12) - 1) / (self.interest * ( 1 + self.interest) ** (self.duration / 12)) - self.terms = [] self.amount = 0 self.debt_service = 0 - # self.startdate = loan_start_date self.principal_start_balance = [] self.principal_end_balance = [] self.interest_due = [] self.principal_due = [] self.debt_service_due = [] - # self.principal_start_balance = align2term([], [], self.loan_terms) - # self.principal_end_balance = align2term([], [], self.loan_terms) - # self.interest_repay = align2term([], [], self.loan_terms) - # self.principal_repay = align2term([], [], self.loan_terms) - # self.debt_service = align2term([], [], self.loan_terms) def get_loan_terms(self): return { @@ -214,7 +206,6 @@ class Loan(): def originate(self): # if terms = None, return 'missing start_data' debt_service = self.debt_service - # debt_service = self.amount / self.payback principal_start_balance = [] principal_end_balance = [] interest_due = [] @@ -225,7 +216,6 @@ class Loan(): for termID in self.terms: interest_amount = start_balance * self.interest / 12 principal_amount = debt_service - interest_amount - principal_start_balance.append(start_balance) interest_due.append(interest_amount) debt_service_due.append(debt_service) @@ -267,11 +257,11 @@ class Loan_List(): # ****** ugly tests ********** -loan_list = Loan_List(db.loan_input_list) -allocated = loan_list.get_schedule( - db.customer_preference, - db.scenario, - db.req_dscr, - datetime.date(2017, 1, 12)) -print(allocated[0].terms) -print(allocated[0].principal_end_balance) +# loan_list = Loan_List(db.loan_input_list) +# allocated = loan_list.get_schedule( +# db.customer_preference, +# db.scenario, +# db.req_dscr, +# datetime.date(2017, 1, 12)) +# print(allocated[0].terms) +# print(allocated[0].principal_end_balance) -- GitLab From 58884926e6022fdb1f1510f31b76eed07e7fbe09 Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Thu, 27 Apr 2017 17:34:51 -0400 Subject: [PATCH 16/28] Create test file and sample data for Loan_List --- bpfin/financials/loan_allocation.py | 2 +- .../test_financials/test_financial_lib.py | 20 ++++++++- bpfin/tests/testdata/sample_data.py | 45 +++++++++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/bpfin/financials/loan_allocation.py b/bpfin/financials/loan_allocation.py index d592caa..923881d 100644 --- a/bpfin/financials/loan_allocation.py +++ b/bpfin/financials/loan_allocation.py @@ -83,7 +83,7 @@ def loan_allocation(loan_list, customer_preference, scenario, req_dscr): bound_list = list((0, current_loan.max_amount) for current_loan in loan_list) print('bound_list =', bound_list) # LP calculation. x[i] == loan amount from loan[i] - res = linprog(c, A_ub=A, b_ub=b, bounds=bound_list, options={'disp': True}) + res = linprog(c, A_ub=A, b_ub=b, bounds=bound_list, options={'disp': False}) print('x1=', res.x[0], 'x2=', res.x[1], 'x3=', res.x[2]) return res.x diff --git a/bpfin/tests/test_financials/test_financial_lib.py b/bpfin/tests/test_financials/test_financial_lib.py index 9e8c487..05e201e 100644 --- a/bpfin/tests/test_financials/test_financial_lib.py +++ b/bpfin/tests/test_financials/test_financial_lib.py @@ -1,7 +1,8 @@ import datetime -from bpfin.financials import financial_lib as fl +import bpfin.financials.financial_lib as fl from bpfin.tests.testdata import sample_data as db from bpfin.financials.income_statement_form_hist import form_income_statement_hist +from bpfin.financials.financial_lib import Loan_List def test_organize_bill_overview(): @@ -18,3 +19,20 @@ def test_income_statement_single_year(): output_dict = db.income_statement_2016 result_dict = fl.income_statement_single_year(2016, input_income, input_bill_overview_organized) assert output_dict == result_dict + + +def test_Loan_List(): + input_loan_input_list = db.loan_input_list + input_customer_preference = db.customer_preference + input_scenario = db.scenario + input_req_dscr = db.req_dscr + input_loan_start_date = datetime.date(2017, 1, 12) + output_loan1_end_balance = db.loan1_end_balance + + sample_loan_list = Loan_List(input_loan_input_list) + result_loan_list = sample_loan_list.get_schedule( + input_customer_preference, + input_scenario, + input_req_dscr, + input_loan_start_date) + assert output_loan1_end_balance == result_loan_list[0].principal_end_balance diff --git a/bpfin/tests/testdata/sample_data.py b/bpfin/tests/testdata/sample_data.py index 898efff..42be891 100644 --- a/bpfin/tests/testdata/sample_data.py +++ b/bpfin/tests/testdata/sample_data.py @@ -519,6 +519,51 @@ scenario = { 'cost_estimation': 150000.00 } + +# Financing plan +loan1_end_balance = [ + 24863.347680778272, 24725.784346095064, 24587.303922513969, + 24447.900296109001, 24307.567312194667, 24166.298775054238, + 24024.088447666207, 23880.930051428921, 23736.817265883386, + 23591.743728434216, 23445.703034068716, 23298.688735074113, + 23150.694340752878, 23001.713317136171, 22851.73908669535, + 22700.765028051592, 22548.78447568354, 22395.790719633034, + 22241.77700520886, 22086.736532688523, 21930.66245701805, + 21773.547887509776, 21615.385887538112, 21456.169474233306, + 21295.891618173133, 21134.54524307256, 20972.123225471318, + 20808.618394419398, 20644.023531160467, 20478.331368813142, + 20311.534592050168, 20143.625836775442, 19974.597689798884, + 19804.442688509149, 19633.153320544148, 19460.722023459381, + 19287.141184394048, 19112.403139734946, 18936.500174778117, + 18759.424523388243, 18581.16836765577, 18401.723837551748, + 18221.083010580365, 18039.237911429173, 17856.180511616974, + 17671.902729139361, 17486.396428111897, 17299.653418410915, + 17111.665455311926, 16922.424239125612, 16731.921414831388, + 16540.148571708534, 16347.097242964863, 16152.758905362902, + 15957.124978843593, 15760.186826147488, 15561.93575243341, + 15362.363004894572, 15161.459772372142, 14959.217184966228, + 14755.626313644276, 14550.678169846844, 14344.363705090762, + 14136.673810569639, 13927.599316751708, 13717.130992974991, + 13505.259547039763, 13291.9756247983, 13077.269809741894, + 12861.132622585112, 12643.554520847285, 12424.525898431206, + 12204.037085199019, 11982.078346545284, 11758.639882967193, + 11533.711829631913, 11307.284255941066, 11079.347165092278, + 10849.890493637833, 10618.904111040358, 10386.377819225565, + 10152.301352132008, 9916.6643752578275, 9679.4564852044859, + 9440.667209217454, 9200.2860047238428, 8958.302258866941, + 8714.7052880376596, 8469.4843374028496, 8222.6285804304734, + 7974.1271184116158, 7723.9689799792986, 7472.1431206240995, + 7218.6384222065326, 6963.4436924661813, 6706.5476645275612, + 6447.9389964026841, 6187.6062704903079, 5925.5379930718491, + 5661.7225938039337, 5396.1484252075652, 5128.8037621538879, + 4859.6768013465198, 4588.7556608004352, 4316.0283793173767, + 4041.4829159577648, 3765.1071495090891, 3486.8888779507552, + 3206.8158179153656, 2924.8756041464071, 2641.055788952322, + 2355.3438416569434, 2067.7271480462618, 1778.1930098115092, + 1486.7286439885249, 1193.3211823933875, 897.9576710542824, + 600.62506963958322, 301.31025088211942, 0] + + # pro-forma date and bill projection - # proforma_date_from, proforma_date_to # prior_rough, post_rough -- GitLab From eb328e33b4071b52d5e0117d2710210fe79979be Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Fri, 28 Apr 2017 16:49:32 -0400 Subject: [PATCH 17/28] Merge from master. Fix conflicts from Income_Statement class --- bpfin/financials/financial_lib.py | 418 +++++++++++++++++- .../financials/income_statement_form_hist.py | 5 +- bpfin/financials/income_statement_next.py | 2 +- .../financials/income_statement_projection.py | 1 + bpfin/financials/liability.py | 14 +- .../test_financials/test_financial_lib.py | 23 +- .../test_income_statement_form_hist.py | 4 +- bpfin/tests/test_financials/test_liability.py | 28 +- bpfin/tests/testdata/sample_data.py | 22 +- 9 files changed, 465 insertions(+), 52 deletions(-) diff --git a/bpfin/financials/financial_lib.py b/bpfin/financials/financial_lib.py index 7549dd4..ca32cbd 100644 --- a/bpfin/financials/financial_lib.py +++ b/bpfin/financials/financial_lib.py @@ -1,16 +1,20 @@ # import datetime # import calendar # from bpfin.lib import other as lib -import bpfin.utilbills.bill_lib as bl -from bpfin.tests.testdata import sample_data as db -from bpfin.financials.loan_allocation import loan_allocation import copy +from bpfin.lib import other as lib +from bpfin.utilbills.bill_lib import form_bill_year +from bpfin.utilbills.bill_lib import form_bill_calendar +from numpy import mean +from bpfin.financials.loan_allocation import loan_allocation +# from bpfin.tests.testdata import sample_data as db +# import bpfin.utilbills.bill_lib as bl # import pprint # import datetime def income_statement_single_year(year, income_input, bill_overview): - """calculate income statement for one single year, with inputs from UI and energy bill overview + """calculate income statement for one single year, with some inputs and energy bill overview Args: year (int): the year of this income statement @@ -37,12 +41,6 @@ def income_statement_single_year(year, income_input, bill_overview): net_non_energy_opex = other_utility + non_utility_expense total_opex = energy_opex + net_non_energy_opex noi = revenue - total_opex # net operating income - # energy_debt_service = - # other_debt_service = - # total_debt_service = - # bill_saving = - # energy_DSCR = - # total_DSCR = income_statement = { 'year': year, @@ -74,8 +72,8 @@ def organize_bill_overview(bill_overview, analysis_date): Description: analysis_date: {'proforma_start': datetime.date, 'proforma_duration': int} """ - proforma_year = bl.form_bill_year(analysis_date['proforma_start'], - analysis_date['proforma_duration']) + proforma_year = form_bill_year(analysis_date['proforma_start'], + analysis_date['proforma_duration']) bill_dict = {} bill_dict[1] = bill_overview['electricity'] # electricity_bill bill_dict[2] = bill_overview['gas'] # gas_bill @@ -85,9 +83,17 @@ def organize_bill_overview(bill_overview, analysis_date): for i in range(4): if bill_dict[i + 1][1] is False: - average_bill_dict[i + 1] = float( - sum(bill_dict[i + 1][0][year] for year in bill_dict[i + 1][ - 0])) / len(bill_dict[i + 1][0]) + sum_bill = 0 + year_number = 0 + for year in bill_dict[i + 1][0]: + if bill_dict[i + 1][0][year] != 0: + sum_bill += bill_dict[i + 1][0][year] + year_number += 1 + if year_number != 0: + average_bill_dict[i + 1] = float(sum_bill) / year_number + else: + average_bill_dict[i + 1] = 0 + # average_bill_dict[i + 1] = float(sum(bill_dict[i + 1][0][year] for year in bill_dict[i + 1][0])) / len(bill_dict[i + 1][0]) for year in proforma_year: for i in range(4): @@ -95,6 +101,8 @@ def organize_bill_overview(bill_overview, analysis_date): pass else: bill_dict[i + 1][0][year] = average_bill_dict[i + 1] + if bill_dict[i + 1][0][year] == 0: + bill_dict[i + 1][0][year] = average_bill_dict[i + 1] bill_overview_organized = { 'electricity': bill_dict[1][0], 'gas': bill_dict[2][0], @@ -119,8 +127,81 @@ class Income_Statement(): self.net_non_energy_opex = None self.total_opex = None self.noi = None + # energy_debt_service = + # other_debt_service = + # total_debt_service = + # bill_saving = + # energy_DSCR = + # total_DSCR = + + def put_hist(self, year, income_input, annual_bill_table): + """ + Put historical income statement data and generate a single year income statement + Inputs are incomplete income statement items, and energy bill overview + Final output is an single year income_statement objectvie with standarized items filled + + Args: + year (int): the year of this income statement + income_input (dictionary): 3 elements of inputs + annual_bill_table (dictionary): annual bill, for 4 utility_types + + Description Sample: + income_input = {revenue': 100000, 'utility_expense': 60000,'non_utility_expense': 3000} + annual_bill_table = {'electricity': electricity_bill, 'oil': oil_bill, 'gas': gas_bill, 'water': water_bill} + """ + self.year = year + self.revenue = income_input['revenue'] + self.utility_expense = income_input['utility_expense'] + self.non_utility_expense = income_input['non_utility_expense'] + self.electricity_opex = annual_bill_table['electricity'][self.year] + self.gas_opex = annual_bill_table['gas'][self.year] + self.oil_opex = annual_bill_table['oil'][self.year] + self.water_opex = annual_bill_table['water'][self.year] + self.energy_opex = self.electricity_opex + self.oil_opex + self.gas_opex + self.water_opex + self.other_utility = self.utility_expense - self.energy_opex + self.net_non_energy_opex = self.other_utility + self.non_utility_expense + self.total_opex = self.energy_opex + self.net_non_energy_opex + self.noi = self.revenue - self.total_opex # net operating income + + def put_average(self, average_revenue, annual_bills, characters): + """ + Calculate and create average income statement. + Revenue, energy bills are average of historical data. + Other items are calculated by historical characters. + Args: + average_revenue (float): average revenue calculated from historical income statements + annual_bills (dictionary): dictionary of float value of average annual bills, for 4 utility_types + characters (dictionary): 6 characters calculated from historical income statements + Final instance is a single year income statement without named year + + Description: + characters = { + 'start_year': (int) 2014, + 'end_year': (int) 2016, + 'cagr': (float) 0.054, + 'other_utility_percent': (float) 0.020 == 2.0% + 'non_utility_expense_percent': (float) 0.030 == 3.0% + 'revenue_average': (float) 3000.00 + } + annual_bills = {'electricity': 100.0, 'oil': 200.0, 'gas': 300.0, 'water': 400.0} + """ + self.revenue = average_revenue + self.electricity_opex = annual_bills['electricity'] + self.gas_opex = annual_bills['gas'] + self.oil_opex = annual_bills['oil'] + self.water_opex = annual_bills['water'] + self.energy_opex = self.electricity_opex + self.gas_opex + self.oil_opex + self.water_opex + self.other_utility = self.revenue * characters['other_utility_percent'] + self.non_utility_expense = self.revenue * characters['non_utility_expense_percent'] + self.utility_expense = self.energy_opex + self.other_utility + self.net_non_energy_opex = self.other_utility + self.non_utility_expense + self.total_opex = self.energy_opex + self.net_non_energy_opex + self.noi = self.revenue - self.total_opex def assign_next(self, income_statement_characters, bill_overview_organized): + """ + !!! this function will be deleted in the next version!! + """ self.electricity_opex = bill_overview_organized['electricity'][self.year] self.gas_opex = bill_overview_organized['gas'][self.year] self.oil_opex = bill_overview_organized['oil'][self.year] @@ -148,6 +229,13 @@ class Income_Statement(): def convert_income_statement_class(income_statement_class): + """ + Convert single year income statement objective into a dictionary format + Args: + income_statement_class (objective): single year income statement objective + Return: + income_statement_dict (dictionary) + """ income_statement_dict = { 'year': income_statement_class.year, 'revenue': income_statement_class.revenue, @@ -165,6 +253,304 @@ def convert_income_statement_class(income_statement_class): return income_statement_dict +class Income_Statement_Next(): + """ + Create single year income_statement objective, + with input of last year data, income statement characters, and projected annual bill + """ + def __init__(self, year, last_revenue, growth_rate_flag, characters, annual_bill_table): + """ + Calculation is done in initiation. + Args: + year (int): the year that will be projected on + last_revenue (float): last year revenue + growth_rate_flag (float): indicating assumed growth rate, -2.0 == cagr, -1.0 == historical average + characters (dictionary): 6 characters calculated from historical income statements + annual_bill_table (dictionary): dictionary of dictionary of annual bills, for 4 utility_types + Final instance is a single year income_statement objective + + Description: + characters = { + 'start_year': (int) 2014, + 'end_year': (int) 2016, + 'cagr': (float) 0.054, + 'other_utility_percent': (float) 0.020 == 2.0% + 'non_utility_expense_percent': (float) 0.035 == 3.5% + 'revenue_average': (float) 3000.00 + } + annual_bill_table = {'electricity': electricity_bill, 'oil': oil_bill, 'gas': gas_bill, 'water': water_bill} + electricity_bill = {2014: 100, 2015:200, ...} + """ + self.year = year + + if growth_rate_flag == -1.0: # growth_rate_flag == average + current_revenue = characters['revenue_average'] + else: + if growth_rate_flag == -2.0: # growth_rate_flag == cagr + growth_rate = characters['cagr'] + else: + growth_rate = growth_rate_flag # growth_rate_flag == 0.00, 0.01, ... + current_revenue = last_revenue * (1 + growth_rate) + self.revenue = current_revenue + self.electricity_opex = annual_bill_table['electricity'][self.year] + self.gas_opex = annual_bill_table['gas'][self.year] + self.oil_opex = annual_bill_table['oil'][self.year] + self.water_opex = annual_bill_table['water'][self.year] + self.energy_opex = self.electricity_opex + self.gas_opex + self.oil_opex + self.water_opex + self.other_utility = self.revenue * characters['other_utility_percent'] + self.non_utility_expense = self.revenue * characters['non_utility_expense_percent'] + self.utility_expense = self.energy_opex + self.other_utility + self.net_non_energy_opex = self.other_utility + self.non_utility_expense + self.total_opex = self.energy_opex + self.net_non_energy_opex + self.noi = self.revenue - self.total_opex + + +class Income_Statement_Table(): + """ + Create a income statement table, containing multiple years, including historical and projected data + Key Attributes are: + hist_table (list): list of single year income_statement objectives, containing historical data + table(list): list of single year income_statement objectives, containing historical and projected data + """ + def __init__(self, raw_income_input, annual_bill_table): + """ + Create hist_table to store historical income statement data, and calculate some key characters. + Args: + raw_income_input (dictionary): dict of dict of incomplete income statement, for historical years. Key = year + annual_bill_table (dictionary): dictionary of dictionary of annual bills, for 4 utility_types + + Key Attributes: + cagr (float): compound annual growth rate + other_utility_percent (float): percentage, (average other_utility / average revenue) + non_utility_expense_percent (float): percentage, (average non_utility_expense_percent / average revenue) + hist_table (list): list of single year income_statement objectives, containing historical data + table(list): list of single year income_statement objectives, containing historical and projected data + characters (dictionary): contains key characters determined from historical data. Is used to project + + Description: + raw_income_input = {2014: {'revenue': 90.0, 'utility_expense': 55.0, 'non_utility_expense': 35.0}, 2015:{},} + annual_bill_table = {'electricity': electricity_bill, 'oil': oil_bill, 'gas': gas_bill, 'water': water_bill} + electricity_bill = {2014: 100, 2015:200, ...} + characters = { + 'start_year': (int) 2014, + 'end_year': (int) 2016, + 'cagr': (float) 0.054, + 'other_utility_percent': (float) 0.02, + 'non_utility_expense_percent': (float) 0.03, + 'revenue_average': (float) 3000.00 + } + """ + self.hist_start_year = None + self.hist_end_year = None + self.cagr = 0.00 + self.other_utility_percent = 0.00 + self.non_utility_expense_percent = 0.00 + self.revenue_average = 0.00 # average revenue + self.hist_table = [] + self.table = [] + self.characters = {} + + for year in raw_income_input: + current_income_statement = Income_Statement() + current_income_statement.put_hist(year, raw_income_input[year], annual_bill_table) + self.hist_table.append(current_income_statement) + + sorted_income_hist_year = sorted(raw_income_input) + self.hist_start_year = sorted_income_hist_year[0] + self.hist_end_year = sorted_income_hist_year[-1] + # if start_year == None: return None + self.cagr = lib.cal_cagr( + raw_income_input[self.hist_start_year]['revenue'], + raw_income_input[self.hist_end_year]['revenue'], + self.hist_end_year - self.hist_start_year) + + revenue_sum = 0 + other_utility_sum = 0 + non_utility_expense_sum = 0 + for current_income_statement in self.hist_table: + revenue_sum += current_income_statement.revenue + other_utility_sum += current_income_statement.other_utility + non_utility_expense_sum += current_income_statement.non_utility_expense + + self.other_utility_percent = other_utility_sum/revenue_sum + self.non_utility_expense_percent = non_utility_expense_sum/revenue_sum + self.revenue_average = revenue_sum / (self.hist_end_year - self.hist_start_year + 1) + + self.table = self.hist_table + self.characters = { + 'start_year': self.hist_start_year, + 'end_year': self.hist_end_year, + 'cagr': self.cagr, + 'other_utility_percent': self.other_utility_percent, + 'non_utility_expense_percent': self.non_utility_expense_percent, + 'revenue_average': self.revenue_average} + + def project(self, growth_rate_flag, analysis_date, annual_bill_table): + """ + Project future income statement. Append multiple single year income_statement objectives to self.table + Args: + growth_rate_flag (float): indicating assumed growth rate, -2.0 == cagr, -1.0 == historical average + analysis_date (dictionary): proforma's starting date and the years of proforma + annual_bill_table (dictionary): dictionary of dictionary of annual bills, for 4 utility_types + Return: + list: list of single year income_statement objectives, containing historical and projection + Note: + project() overwrites existing projection data + """ + # characters = copy.deepcopy(self.characters) + proforma_year = form_bill_year(analysis_date['proforma_start'], + analysis_date['proforma_duration']) + current_table = copy.deepcopy(self.hist_table) + for year in proforma_year: + last_revenue = current_table[-1].revenue + if year <= current_table[-1].year: + continue + current_income_statement = Income_Statement_Next( + year, last_revenue, growth_rate_flag, self.characters, annual_bill_table) + current_table.append(current_income_statement) + # print(current_income_statement.revenue, current_income_statement.noi) + self.table = current_table + return current_table + + def get_hist_table(self): + """ + Get historical table, in dictionary formatting + Return: + dictionary: dict of dict of income statement. Key is year + Description: + hist_table_dict = {2014: {'year': 2014, 'revenue': 100.0, ..., 'not': 5.0}, ... , 2016:{}} + """ + hist_table_dict = {} + for current_income_statement in self.hist_table: + hist_table_dict[current_income_statement.year] = { + 'year': current_income_statement.year, + 'revenue': current_income_statement.revenue, + 'utility_expense': current_income_statement.utility_expense, + 'energy_opex': current_income_statement.energy_opex, + 'electricity_opex': current_income_statement.electricity_opex, + 'gas_opex': current_income_statement.gas_opex, + 'oil_opex': current_income_statement.oil_opex, + 'water_opex': current_income_statement.water_opex, + 'other_utility': current_income_statement.other_utility, + 'non_utility_expense': current_income_statement.non_utility_expense, + 'net_non_energy_opex': current_income_statement.net_non_energy_opex, + 'total_opex': current_income_statement.total_opex, + 'noi': current_income_statement.noi} + return hist_table_dict + + def get_cagr(self): + """ + Get compound annual growth rate + Return: + float: compound annual growth rate + """ + return copy.deepcopy(self.cagr) + + def get_average(self): + """ + Get average income statement. Only need initialized Income_Statement_Table + Return: + dictionary: dict of average income statement, Keys are income statement items. + Description: + output_dict = {'year': None, 'revenue': 95000.0, ... ,'noi': 35166.666666666657} + """ + # current_year = 'Average' + current_income_statement = Income_Statement() + average_revenue = mean(list(i_s.revenue for i_s in self.hist_table)) + annual_bills = { + 'electricity': mean(list(i_s.electricity_opex for i_s in self.hist_table)), + 'gas': mean(list(i_s.gas_opex for i_s in self.hist_table)), + 'oil': mean(list(i_s.oil_opex for i_s in self.hist_table)), + 'water': mean(list(i_s.water_opex for i_s in self.hist_table)) + } + current_income_statement.put_average(average_revenue, annual_bills, self.characters) + + return convert_income_statement_class(current_income_statement) + + def get_single_year(self, year): + """ + Get single year income statement from self.table. Table can either contain or not contain projection + Args: + year (int): the year that need to be extracted + Return: + dict: single year income statement. Keys are income statement items. + Note: + if the target year is not in self.table, then return None. + """ + for current_income_statement in self.table: + if current_income_statement.year == year: + return { + 'year': current_income_statement.year, + 'revenue': current_income_statement.revenue, + 'utility_expense': current_income_statement.utility_expense, + 'energy_opex': current_income_statement.energy_opex, + 'electricity_opex': current_income_statement.electricity_opex, + 'gas_opex': current_income_statement.gas_opex, + 'oil_opex': current_income_statement.oil_opex, + 'water_opex': current_income_statement.water_opex, + 'other_utility': current_income_statement.other_utility, + 'non_utility_expense': current_income_statement.non_utility_expense, + 'net_non_energy_opex': current_income_statement.net_non_energy_opex, + 'total_opex': current_income_statement.total_opex, + 'noi': current_income_statement.noi} + return None + + def get_noi_dict(self): + """ + Get a dictionary of net operating income. + Return: + dictionary: net operating income for each year in income statement table. Key is year + """ + noi_dict = {} + for current_income_statement in self.table: + noi_dict[current_income_statement.year] = current_income_statement.noi + return noi_dict + + +# ************ ugly tests*********** +# IS_table = Income_Statement_Table(db.raw_income_input, db.bill_overview_organized) +# IS_table.project(-1.0, db.analysis_date, db.bill_overview_organized) +# assert IS_table.get_hist_table() == db.income_statement_full # test get_hist_table +# print('\n cagr =', IS_table.get_cagr()) # test get_cagr +# print('\n average hist =', IS_table.get_average()) # test get_average +# print('\n noi dict =', IS_table.get_noi_dict()) # test get_noi_dict +# print('\n 2017 income statement =', IS_table.get_single_year(2017)) # test get_single_year + +# ************ test draft*********** +# class test_stru(): +# def __init__(self, number): +# self.number = number + +# test_1 = test_stru(2.0) +# test_2 = test_stru(3.0) +# test_3 = test_stru(5.0) + +# test_list = [test_1, test_2, test_3] +# form_list = list(ob.number for ob in test_list) +# print(form_list) + + +# def get_income_statement_single_year(self, year, income_statement_table): +# for current_income_statement in income_statement_table: +# if current_income_statement.year == year: +# return { +# 'year': current_income_statement.year, +# 'revenue': current_income_statement.revenue, +# 'utility_expense': current_income_statement.utility_expense, +# 'energy_opex': current_income_statement.energy_opex, +# 'electricity_opex': current_income_statement.electricity_opex, +# 'gas_opex': current_income_statement.gas_opex, +# 'oil_opex': current_income_statement.oil_opex, +# 'water_opex': current_income_statement.water_opex, +# 'other_utility': current_income_statement.other_utility, +# 'non_utility_expense': current_income_statement.non_utility_expense, +# 'net_non_energy_opex': current_income_statement.net_non_energy_opex, +# 'total_opex': current_income_statement.total_opex, +# 'noi': current_income_statement.noi} +# return None + + +# ************* Loan *************** class Loan(): def __init__(self, loan_term): self.institute = loan_term['institute'] @@ -201,7 +587,7 @@ class Loan(): (1 + self.interest / 12) ** (self.duration) - 1) def put_terms(self, loan_start_date): - self.terms = bl.form_bill_calendar(loan_start_date, float(self.duration)/12)[1] + self.terms = form_bill_calendar(loan_start_date, float(self.duration)/12)[1] def originate(self): # if terms = None, return 'missing start_data' diff --git a/bpfin/financials/income_statement_form_hist.py b/bpfin/financials/income_statement_form_hist.py index 65ba995..88e8d2d 100644 --- a/bpfin/financials/income_statement_form_hist.py +++ b/bpfin/financials/income_statement_form_hist.py @@ -4,6 +4,7 @@ from bpfin.lib import other as lib def income_statement_character(income_statement_hist): """Determine annual growth rate, other_utility_percentage, non_utility_expense_percentage + !!!! This function should be deleted or rephrased in next version !! Args: income_statement_hist (dictionary): full income statement, with all items filled, for available years Return: @@ -41,12 +42,12 @@ def income_statement_character(income_statement_hist): return result_dict -def form_income_statement_hist(raw_income_input, bill_overview_organized, analysis_date): +def form_income_statement_hist(raw_income_input, bill_overview_organized): """ form income statement table with raw inputs from UI, and organized bill_overview. NO projection + !!!! This function should be deleted or rephrased in next version !! Args: raw_income_input (dictionary): dictionary of dictionary. raw inputs for income statement for available years bill_overview_organized (dictionary): dict of dict, 4 utility types, with blank charge filled with average - analysis_date (dictionary): proforma's starting date and the years of proforma Returns: Dictionary: dict of dict, full income statement for available years diff --git a/bpfin/financials/income_statement_next.py b/bpfin/financials/income_statement_next.py index d46c4ab..0faae06 100644 --- a/bpfin/financials/income_statement_next.py +++ b/bpfin/financials/income_statement_next.py @@ -7,6 +7,7 @@ from bpfin.financials.income_statement_form_hist import income_statement_charact def income_statement_next(income_statement_hist, bill_overview_organized, growth_rate_flag): """Project income statement for UI input. inputs are whatever bill and financial statements are available + !!!! This function should be deleted or rephrased in next version !! Args: income_statement_hist (dictionary): historical income statement, with all items filled, for available years bill_overview_organized (dictionary): annual bills for 4 utility types, with blank cells filled @@ -32,7 +33,6 @@ def income_statement_next(income_statement_hist, bill_overview_organized, growth income_next.assign_next(characters, bill_overview_organized) result_dict = fl.convert_income_statement_class(income_next) - print(result_dict) return result_dict # def income_statement_proj_input(income_statement_full, bill_overview_organized, growth_rate_flag): diff --git a/bpfin/financials/income_statement_projection.py b/bpfin/financials/income_statement_projection.py index ac4d589..221225d 100644 --- a/bpfin/financials/income_statement_projection.py +++ b/bpfin/financials/income_statement_projection.py @@ -8,6 +8,7 @@ import pprint def income_statement_projection(income_statement_hist, bill_overview_organized, growth_rate_flag, analysis_date): """ + !!!! This function should be deleted or rephrased in next version !! Project income statement tp future multiple years. inputs are historical income statements and annual bills. Args: income_statement_hist (dictionary): historical income statement, with all items filled, for available years diff --git a/bpfin/financials/liability.py b/bpfin/financials/liability.py index d0aaa20..01e5ceb 100644 --- a/bpfin/financials/liability.py +++ b/bpfin/financials/liability.py @@ -93,7 +93,8 @@ def final_liability_dict(start_date, liability_dictionary, months): pro_date.year, pro_date.month)[1])] = [] final_dict[(pro_date.year, pro_date.month, calendar.monthrange( - pro_date.year, pro_date.month)[1])].append(debt_date_dict[debt_date]) + pro_date.year, pro_date.month)[1] + )].append(debt_date_dict[debt_date]) for pro_date in pro_forma_calendar: if (pro_date.year, pro_date.month, calendar.monthrange( pro_date.year, pro_date.month)[1]) not in final_dict: @@ -107,4 +108,13 @@ def final_liability_dict(start_date, liability_dictionary, months): for key, value in final_dict.items(): real_dict[datetime(key[0], key[1], key[2])] = value - return real_dict + annual_liability_dict = {} + for full_date, liability in real_dict.items(): + if full_date.year not in annual_liability_dict: + annual_liability_dict[full_date.year] = [] + annual_liability_dict[full_date.year].append(liability) + sum_dict = {} + for year, value in annual_liability_dict.items(): + sum_dict[year] = sum(value) + + return sum_dict diff --git a/bpfin/tests/test_financials/test_financial_lib.py b/bpfin/tests/test_financials/test_financial_lib.py index 05e201e..a4ee637 100644 --- a/bpfin/tests/test_financials/test_financial_lib.py +++ b/bpfin/tests/test_financials/test_financial_lib.py @@ -1,8 +1,9 @@ import datetime import bpfin.financials.financial_lib as fl from bpfin.tests.testdata import sample_data as db -from bpfin.financials.income_statement_form_hist import form_income_statement_hist from bpfin.financials.financial_lib import Loan_List +from bpfin.financials.financial_lib import Income_Statement_Table +# from bpfin.financials.income_statement_form_hist import form_income_statement_hist def test_organize_bill_overview(): @@ -21,6 +22,25 @@ def test_income_statement_single_year(): assert output_dict == result_dict +def test_Income_Statement_Table(): + input_raw_income_input = db.raw_income_input + input_annual_bill_table = db.bill_overview_organized + output_cagr = 0.05409255338945984 + output_hist_table = db.income_statement_full + output_average_table = db.income_statement_average + output_noi_dict = db.noi_dict_average + output_single_year = db.income_statement_2017_avg + + IS_table = Income_Statement_Table(input_raw_income_input, input_annual_bill_table) + IS_table.project(-1.0, db.analysis_date, db.bill_overview_organized) + + assert IS_table.get_cagr() == output_cagr # test get_cagr + assert IS_table.get_hist_table() == output_hist_table # test get_hist_table + assert IS_table.get_average() == output_average_table # test get_average + assert IS_table.get_noi_dict() == output_noi_dict # test get_noi_dict + assert IS_table.get_single_year(2017) == output_single_year # test get_single_year + + def test_Loan_List(): input_loan_input_list = db.loan_input_list input_customer_preference = db.customer_preference @@ -28,7 +48,6 @@ def test_Loan_List(): input_req_dscr = db.req_dscr input_loan_start_date = datetime.date(2017, 1, 12) output_loan1_end_balance = db.loan1_end_balance - sample_loan_list = Loan_List(input_loan_input_list) result_loan_list = sample_loan_list.get_schedule( input_customer_preference, diff --git a/bpfin/tests/test_financials/test_income_statement_form_hist.py b/bpfin/tests/test_financials/test_income_statement_form_hist.py index 9114f18..5680325 100644 --- a/bpfin/tests/test_financials/test_income_statement_form_hist.py +++ b/bpfin/tests/test_financials/test_income_statement_form_hist.py @@ -19,10 +19,8 @@ def test_income_statement_character(): def test_form_income_statement_hist(): input_raw_income_input = db.raw_income_input input_bill_overview_organized = db.bill_overview_organized - input_analysis_date = db.analysis_date output_dict = db.income_statement_full result_dict = form_income_statement_hist( input_raw_income_input, - input_bill_overview_organized, - input_analysis_date) + input_bill_overview_organized) assert output_dict == result_dict diff --git a/bpfin/tests/test_financials/test_liability.py b/bpfin/tests/test_financials/test_liability.py index 9d8f26c..606f621 100644 --- a/bpfin/tests/test_financials/test_liability.py +++ b/bpfin/tests/test_financials/test_liability.py @@ -8,31 +8,9 @@ def test_final_liability_dict(): input_dict = db.liability_dictionary input_duration = db.analysis_date['proforma_duration'] output = { - datetime(2012, 1, 31): 0, - datetime(2012, 2, 29): 0, - datetime(2012, 3, 31): 0, - datetime(2012, 4, 30): 0, - datetime(2012, 5, 31): 0, - datetime(2012, 6, 30): 0, - datetime(2012, 7, 31): 0, - datetime(2012, 8, 31): 100, - datetime(2012, 9, 30): 100, - datetime(2012, 10, 31): 100, - datetime(2012, 11, 30): 100, - datetime(2012, 12, 31): 250, - datetime(2013, 1, 31): 250, - datetime(2013, 2, 28): 250, - datetime(2013, 3, 31): 250, - datetime(2013, 4, 30): 250, - datetime(2013, 5, 31): 250, - datetime(2013, 6, 30): 250, - datetime(2013, 7, 31): 250, - datetime(2013, 8, 31): 250, - datetime(2013, 9, 30): 250, - datetime(2013, 10, 31): 100, - datetime(2013, 11, 30): 100, - datetime(2013, 12, 31): 100, - datetime(2014, 1, 31): 100 + 2012: 650, + 2013: 2550, + 2014: 100 } result = final_liability_dict(input_start_date, input_dict, input_duration) diff --git a/bpfin/tests/testdata/sample_data.py b/bpfin/tests/testdata/sample_data.py index 42be891..0dd355d 100644 --- a/bpfin/tests/testdata/sample_data.py +++ b/bpfin/tests/testdata/sample_data.py @@ -90,7 +90,7 @@ annual_bill_electricity = { 2028: 43325.235463581725, 2029: 44211.298795796341, 2030: 45097.667796776521, 2031: 45986.665329650692, 2032: 46886.305428683903, 2033: 47806.578908870637, 2034: 48751.551094031129, 2035: 49722.255450564422, 2036: 50718.832984188753} -annual_bill_gas = {2015: 1020, 2016: 1220, 2017: 1520} +annual_bill_gas = {2014: 0, 2015: 1020, 2016: 1220, 2017: 1520} annual_bill_oil = {2015: 1010, 2016: 1210, 2017: 1510} annual_bill_water = {2015: 0, 2016: 0, 2017: 0} @@ -192,6 +192,26 @@ income_statement_full = { 'total_opex': 63000.0, 'noi': 37000.0} } +income_statement_average = { + 'year': None, 'revenue': 95000.0, 'utility_expense': 56666.666666666672, 'energy_opex': 35880.343493666973, + 'electricity_opex': 33561.454604778082, 'gas_opex': 1164.4444444444443, 'oil_opex': 1154.4444444444443, + 'water_opex': 0.0, 'other_utility': 20786.323172999699, 'non_utility_expense': 3166.6666666666665, + 'net_non_energy_opex': 23952.989839666367, 'total_opex': 59833.333333333343, 'noi': 35166.666666666657} + +noi_dict_average = { + 2014: 31500.0, 2015: 37000.0, 2016: 37000.0, 2017: 33493.73724516164, 2018: 33165.377210606544, + 2019: 32298.852732150932, 2020: 31450.146124428546, 2021: 30663.261480034096, 2022: 29946.57845606045, + 2023: 29242.418277114877, 2024: 28522.26581429284, 2025: 27777.914231279196, 2026: 26974.67650825437, + 2027: 26110.42490293656, 2028: 25225.108030085234, 2029: 24339.044697870617, 2030: 23452.675696890437, + 2031: 22563.678164016266, 2032: 21664.038064983062, 2033: 20743.76458479633, 2034: 19798.79239963583, + 2035: 18828.088043102543, 2036: 17831.510509478205} + +income_statement_2017_avg = { + 'year': 2017, 'revenue': 95000.0, 'utility_expense': 58339.59608817169, 'energy_opex': 37553.27291517199, + 'electricity_opex': 34523.27291517199, 'gas_opex': 1520, 'oil_opex': 1510, 'water_opex': 0, + 'other_utility': 20786.3231729997, 'non_utility_expense': 3166.6666666666665, + 'net_non_energy_opex': 23952.989839666367, 'total_opex': 61506.26275483836, 'noi': 33493.73724516164} + # income_input_next = { # 'year': 2017, 'revenue': 105409.25533894598, 'utility_expense': 60617.176566756985, 'energy_opex': 37553.27291517199, # 'electricity_opex': 34523.27291517199, 'gas_opex': 1520, 'oil_opex': 1510, 'water_opex': 0, -- GitLab From 62739864b250bcdde8a9a4315d7f4b19df996972 Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Fri, 28 Apr 2017 17:09:46 -0400 Subject: [PATCH 18/28] Add descriptions for Loan and Loan_List classes --- bpfin/financials/financial_lib.py | 82 +++++++++++++++++-- bpfin/financials/loan_allocation.py | 9 +- .../test_financials/test_financial_lib.py | 6 ++ bpfin/tests/testdata/sample_data.py | 5 +- 4 files changed, 91 insertions(+), 11 deletions(-) diff --git a/bpfin/financials/financial_lib.py b/bpfin/financials/financial_lib.py index ca32cbd..0c82041 100644 --- a/bpfin/financials/financial_lib.py +++ b/bpfin/financials/financial_lib.py @@ -553,6 +553,15 @@ class Income_Statement_Table(): # ************* Loan *************** class Loan(): def __init__(self, loan_term): + """ + Initiate a single loan, with inputs of basic terms, and calculate payback as a key character + Args: + loan_term (dictionary): characters of a loan. + Description: + loan_term = {'institute': 'NYSERDA', 'max_amount': 5000.00, 'interest': 0.08, 'duration': 120} + To Do: + verify interest rate != 0 + """ self.institute = loan_term['institute'] self.max_amount = loan_term['max_amount'] self.interest = loan_term['interest'] @@ -571,6 +580,11 @@ class Loan(): self.debt_service_due = [] def get_loan_terms(self): + """ + Get loan terms in dictionary format. + Return: + dictionary: a dict containing all characters, not schedule + """ return { 'institute': self.institute, 'max_amount': self.max_amount, @@ -582,15 +596,33 @@ class Loan(): 'debt_service': self.debt_service} def put_amount(self, amount): + """ + Put loan amount to loan amount, calculate monthly debt service. + Args: + amount (float): loan amount that calculated or suggested to borrow + """ self.amount = amount self.debt_service = self.amount * ((self.interest / 12) * (1 + self.interest / 12) ** (self.duration)) / ( (1 + self.interest / 12) ** (self.duration) - 1) def put_terms(self, loan_start_date): + """ + Put loan start date and calculate schedule terms. + Args: + loan_start_date(datetime.date): the date that the loan will be deployed + """ self.terms = form_bill_calendar(loan_start_date, float(self.duration)/12)[1] def originate(self): - # if terms = None, return 'missing start_data' + """ + Origniate loan, calculating its schedule + Note: + if amount == 0, but terms != None, the schedule will still be calculated + if terms == None, the function returns None + """ + if not self.terms: + # print('missing start_data') + return None debt_service = self.debt_service principal_start_balance = [] principal_end_balance = [] @@ -617,24 +649,64 @@ class Loan(): class Loan_List(): + """ + Create a list of loan objectives, in order to allocate loan amount among loans, and calculate schedules for loans + Attributes: + loan_list (list): list of loan objectives + """ def __init__(self, loan_input_list): + """ + Initiate loan_list with inputs. + Args: + loan_input_list (list): list of dictionary of loan basic terms. + """ self.loan_list = [] for current_loan in loan_input_list: temp_loan = None temp_loan = Loan(current_loan) self.loan_list.append(temp_loan) - # loan_list[loan_id] = temp_loan.convert_loan_class() - # return loan_list def allocate(self, customer_preference, scenario, req_dscr): + """ + !!! The functions is not finished yet !!! + + Allocate loan amount based on project cost, loan terms, income statements, scenario, other requirements. + Apply linear programming method to allocate loan amount among loans, create financing plan. + Args: + customer_preference (dictionary): dict of customer preference, { + 'downpayment_max': float, upper limit of self finance amount, + 'expected_payback': int, months that customer expect the project can payback, + 'cust_saving_dscr': float, customer required lower limit of (saving / debt service) ratio + } + scenario (dictionary): dict of project economics and saving scenario, { + 'cost_estimation': float, estimated project cost + 'post_income_statement': objective of Income_Statement_Table , projected income_statement with savings + } + req_dscr (dictionary): dict of required debt service coverage ratios, { + 'req_noi_dscr': 1.15, + 'req_cash_dscr': 1.15, + 'req_saving_dscr': 1.10 + } + Returns: + list: list of Loan objectives, with amount assigned, no schedule assigned + """ current_loan_list = copy.deepcopy(self.loan_list) allocation = loan_allocation( current_loan_list, customer_preference, scenario, req_dscr) for current_loan, current_amount in zip(current_loan_list, allocation): current_loan.put_amount(current_amount) - return current_loan_list # a list of objectives of loans, with amount, not schedule + return current_loan_list def get_schedule(self, customer_preference, scenario, req_dscr, loan_start_date): + """ + !!! The functions is not finished yet !!! Because self.allocate needs improve + + Get the schedule of all loans, after allocate loan amounts + Args: + + Returns: + list: list of Loan objectives, with amounts and schedules assigned + """ allocated_loan_list = self.allocate(customer_preference, scenario, req_dscr) for current_loan in allocated_loan_list: current_loan.put_terms(loan_start_date) @@ -642,7 +714,7 @@ class Loan_List(): return allocated_loan_list -# ****** ugly tests ********** +# ****** ugly tests Loan and Loan_List class ********** # loan_list = Loan_List(db.loan_input_list) # allocated = loan_list.get_schedule( # db.customer_preference, diff --git a/bpfin/financials/loan_allocation.py b/bpfin/financials/loan_allocation.py index 923881d..6c07378 100644 --- a/bpfin/financials/loan_allocation.py +++ b/bpfin/financials/loan_allocation.py @@ -19,6 +19,8 @@ import pprint def loan_allocation(loan_list, customer_preference, scenario, req_dscr): """ + !!! The functions is not finished yet !!! + Take inputs of available financing options(loans and self-finance), and other constrains, determine the best financing plan, with minimum debt service. Args: @@ -42,7 +44,7 @@ def loan_allocation(loan_list, customer_preference, scenario, req_dscr): customer_preference = { 'downpayment_max': 5000, 'expected_payback': 120, # in months - 'cust_noi_dscr': 1.15} + 'cust_saving_dscr': 1.15} Note: dscr == debt service coverage ratio @@ -76,7 +78,7 @@ def loan_allocation(loan_list, customer_preference, scenario, req_dscr): first_year_saving / req_dscr['req_saving_dscr'], first_year_noi / req_dscr['req_noi_dscr'], first_year_cash / req_dscr['req_cash_dscr'] - # 0 - first_year_noi / customer_preference['cust_noi_dscr'], + # 0 - first_year_noi / customer_preference['cust_saving_dscr'], )) print('\n', A, '<=', b) # x variables bounds, 0 <= x[i] <= loan[i]_max_amount @@ -87,8 +89,7 @@ def loan_allocation(loan_list, customer_preference, scenario, req_dscr): print('x1=', res.x[0], 'x2=', res.x[1], 'x3=', res.x[2]) return res.x - # !!! think of loan term structure - # run a linear programming to determine the result. + # # run a linear programming to determine the result. # ****pre-request is: # quoted_cost <= sum_loan_max_amount + downpayment_max # diff --git a/bpfin/tests/test_financials/test_financial_lib.py b/bpfin/tests/test_financials/test_financial_lib.py index a4ee637..f6c339a 100644 --- a/bpfin/tests/test_financials/test_financial_lib.py +++ b/bpfin/tests/test_financials/test_financial_lib.py @@ -23,6 +23,12 @@ def test_income_statement_single_year(): def test_Income_Statement_Table(): + """ + !!! To do: this test file need update when allocation calculation is finished !! + + test Loan and Loan_List classes. + by calling Loan_List.get_schedule(), all calculation functions are called and tested. + """ input_raw_income_input = db.raw_income_input input_annual_bill_table = db.bill_overview_organized output_cagr = 0.05409255338945984 diff --git a/bpfin/tests/testdata/sample_data.py b/bpfin/tests/testdata/sample_data.py index 0dd355d..8d8d277 100644 --- a/bpfin/tests/testdata/sample_data.py +++ b/bpfin/tests/testdata/sample_data.py @@ -531,12 +531,13 @@ req_dscr = { customer_preference = { 'downpayment_max': 5000.00, 'expected_payback': 120, # in months - 'cust_noi_dscr': 1.15 + 'cust_saving_dscr': 1.15 } # Engineering Scenarios scenario = { - 'cost_estimation': 150000.00 + 'cost_estimation': 150000.00, + # 'post_income_statement': post_income_statement } -- GitLab From bf1d622156155bc501561898c33e1639e834a32d Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Fri, 28 Apr 2017 17:13:33 -0400 Subject: [PATCH 19/28] Add attributes descriptions for Loan. Not finished yet. --- bpfin/financials/financial_lib.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/bpfin/financials/financial_lib.py b/bpfin/financials/financial_lib.py index 0c82041..3dc95ea 100644 --- a/bpfin/financials/financial_lib.py +++ b/bpfin/financials/financial_lib.py @@ -552,6 +552,26 @@ class Income_Statement_Table(): # ************* Loan *************** class Loan(): + """ + Single loan, with terms and schedule + Attributes: + self.institute = loan_term['institute'] + self.max_amount = loan_term['max_amount'] + self.interest = loan_term['interest'] + self.duration = loan_term['duration'] # in month + # todo interest rate!=0 + # calculate loan's (loan amount/debt service), similar to payback year + self.payback = ((1 + self.interest) ** (self.duration / 12) - 1) / (self.interest * ( + 1 + self.interest) ** (self.duration / 12)) + self.amount = 0 + self.debt_service = 0 + self.terms = [] + self.principal_start_balance = [] + self.principal_end_balance = [] + self.interest_due = [] + self.principal_due = [] + self.debt_service_due = [] + """ def __init__(self, loan_term): """ Initiate a single loan, with inputs of basic terms, and calculate payback as a key character @@ -570,9 +590,9 @@ class Loan(): # calculate loan's (loan amount/debt service), similar to payback year self.payback = ((1 + self.interest) ** (self.duration / 12) - 1) / (self.interest * ( 1 + self.interest) ** (self.duration / 12)) - self.terms = [] self.amount = 0 self.debt_service = 0 + self.terms = [] self.principal_start_balance = [] self.principal_end_balance = [] self.interest_due = [] -- GitLab From b25c79b5b44d5905c1786117193256cab8bdfb69 Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Fri, 28 Apr 2017 17:53:42 -0400 Subject: [PATCH 20/28] Add description for Loan class --- bpfin/financials/financial_lib.py | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/bpfin/financials/financial_lib.py b/bpfin/financials/financial_lib.py index 3dc95ea..bf0624b 100644 --- a/bpfin/financials/financial_lib.py +++ b/bpfin/financials/financial_lib.py @@ -555,22 +555,19 @@ class Loan(): """ Single loan, with terms and schedule Attributes: - self.institute = loan_term['institute'] - self.max_amount = loan_term['max_amount'] - self.interest = loan_term['interest'] - self.duration = loan_term['duration'] # in month - # todo interest rate!=0 - # calculate loan's (loan amount/debt service), similar to payback year - self.payback = ((1 + self.interest) ** (self.duration / 12) - 1) / (self.interest * ( - 1 + self.interest) ** (self.duration / 12)) - self.amount = 0 - self.debt_service = 0 - self.terms = [] - self.principal_start_balance = [] - self.principal_end_balance = [] - self.interest_due = [] - self.principal_due = [] - self.debt_service_due = [] + institute (string): name of loan lender + max_amount (float): upper limit of loan amount that can be borrowed + interest (float): annual interest rate. 0.025 = 2.5% + duration (int): loan duration scheduled be repaid, in month + payback (float): ratio of (loan amount/annual debt service), similar to but NOT the payback period, in year + amount (float): amount borrowed or suggested to be borrowed from this lender + debt_service (float): monthly debt service need to be paid. Now we use amortization calculation + terms (list): list of datetime.date, the months that scheduled to get repay. Dates are end of month + principal_start_balance (list): list of float, begining balance of schedule outstanding principal + principal_end_balance (list): list of float, ending balance of schedule outstanding principal + interest_due (list): list of float, scheduled interest repayment due + principal_due (list): list of float, scheduled principal repayment due + debt_service_due (list): list of float, scheduled interest + principal repayment due """ def __init__(self, loan_term): """ -- GitLab From 642bb2f6e5c4e44760a3a1cad9760924bb17a1a4 Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Fri, 28 Apr 2017 18:26:19 -0400 Subject: [PATCH 21/28] Create working process test in data_generation --- bpfin/utilbills/data_generation.py | 33 ++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/bpfin/utilbills/data_generation.py b/bpfin/utilbills/data_generation.py index 13ce46c..85e70bb 100644 --- a/bpfin/utilbills/data_generation.py +++ b/bpfin/utilbills/data_generation.py @@ -1,7 +1,10 @@ -# from bpfin.utilbills.bill_month_normalize_rough import bill_month_normalize_rough -# from bpfin.utilbills.bill_prior_proj_rough import bill_prior_proj_rough -# from bpfin.utilbills import bill_lib as bl -# from bpfin.tests.testdata import sample_data as db +from bpfin.utilbills.bill_month_normalize_rough import bill_month_normalize_rough +from bpfin.utilbills.bill_prior_proj_rough import bill_prior_proj_rough +from bpfin.utilbills import bill_lib as bl +from bpfin.tests.testdata import sample_data as db +import bpfin.financials.financial_lib as fl +import bpfin.lib.other as lib +import copy # raw_bill = db.raw_bill @@ -10,3 +13,25 @@ # annual_bill_electricity = bl.annualizing_projection(prior_bill_electricity['date_to'], prior_bill_electricity['charge']) # print(annual_bill_electricity) + +prior_energy_bill = db.bill_overview_organized +raw_income_input = db.raw_income_input + +IS_prior = fl.Income_Statement_Table(raw_income_input, prior_energy_bill) +IS_prior.project(-2.0, db.analysis_date, prior_energy_bill) +# print(IS_prior.get_noi_dict()) + +post_energy_bill = db.bill_overview_organized +post_energy_bill['electricity'][0] = bl.annualizing_projection( + db.post_bill_rough['date_to'], db.post_proj_rough_charge) +print(post_energy_bill) + +IS_post = fl.Income_Statement_Table(raw_income_input, post_energy_bill) +IS_post.project(-2.0, db.analysis_date, post_energy_bill) +# print(IS_post.get_noi_dict()) + +prior_noi = (IS_prior.get_noi_dict().values()) +post_noi = (IS_post.get_noi_dict().values()) + +# print(lib.sublist(post_noi, prior_noi)) +# now we have problem. no saving was projected in this practice. fix that!!! -- GitLab From ca12ee7139e11b30d26a170ae1fedcbc52b6ac6d Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Sat, 29 Apr 2017 12:43:49 -0400 Subject: [PATCH 22/28] Create exe sample for income statement prior and post projection, in data_generation --- bpfin/utilbills/bill_post_proj_rough.py | 2 ++ bpfin/utilbills/data_generation.py | 10 +++------- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/bpfin/utilbills/bill_post_proj_rough.py b/bpfin/utilbills/bill_post_proj_rough.py index 5a975f4..99a0719 100644 --- a/bpfin/utilbills/bill_post_proj_rough.py +++ b/bpfin/utilbills/bill_post_proj_rough.py @@ -19,6 +19,8 @@ def bill_post_proj_rough(prior_bill, saving_percent): {'date_from': post_bill_start, 'date_to': post_bill_end, 'usage': post_monthly_usage, 'charge': post_monthly_charge, 'price': post_monthly_price} + + To Do: add construction_date to project bill. Maybe create a separate function. for rough and reg """ post_bill_start = prior_bill['date_from'] post_bill_end = prior_bill['date_to'] diff --git a/bpfin/utilbills/data_generation.py b/bpfin/utilbills/data_generation.py index 85e70bb..5e418d1 100644 --- a/bpfin/utilbills/data_generation.py +++ b/bpfin/utilbills/data_generation.py @@ -12,26 +12,22 @@ import copy # prior_bill_electricity = bill_prior_proj_rough(norm_bill, raw_bill, db.analysis_date, db.inflation_coeff_dict) # annual_bill_electricity = bl.annualizing_projection(prior_bill_electricity['date_to'], prior_bill_electricity['charge']) -# print(annual_bill_electricity) prior_energy_bill = db.bill_overview_organized raw_income_input = db.raw_income_input IS_prior = fl.Income_Statement_Table(raw_income_input, prior_energy_bill) IS_prior.project(-2.0, db.analysis_date, prior_energy_bill) -# print(IS_prior.get_noi_dict()) -post_energy_bill = db.bill_overview_organized -post_energy_bill['electricity'][0] = bl.annualizing_projection( +post_energy_bill = copy.deepcopy(db.bill_overview_organized) +post_energy_bill['electricity'] = bl.annualizing_projection( db.post_bill_rough['date_to'], db.post_proj_rough_charge) -print(post_energy_bill) IS_post = fl.Income_Statement_Table(raw_income_input, post_energy_bill) IS_post.project(-2.0, db.analysis_date, post_energy_bill) -# print(IS_post.get_noi_dict()) prior_noi = (IS_prior.get_noi_dict().values()) post_noi = (IS_post.get_noi_dict().values()) -# print(lib.sublist(post_noi, prior_noi)) +print(lib.sublist(prior_noi, post_noi)) # now we have problem. no saving was projected in this practice. fix that!!! -- GitLab From e7cbfac5bcd7c4a05d0b7172c91fb7f8bcfc0e44 Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Mon, 1 May 2017 23:23:15 -0400 Subject: [PATCH 23/28] Add saving item in scenario in sample_data. Improve loan_allocation function description. --- bpfin/financials/loan_allocation.py | 15 ++++++++------- bpfin/tests/testdata/sample_data.py | 1 + 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/bpfin/financials/loan_allocation.py b/bpfin/financials/loan_allocation.py index 6c07378..1ec4436 100644 --- a/bpfin/financials/loan_allocation.py +++ b/bpfin/financials/loan_allocation.py @@ -27,16 +27,13 @@ def loan_allocation(loan_list, customer_preference, scenario, req_dscr): loan_list (list): list of objectives of loans customer_preference (dictionary): customer preference for financing plan req_dscr(dictionary): required dscr - construction_date - construction_cost + commissioning_date (date): construction finish date. Saving start at NEXT month + scenario (dictionary): package of energy conservation measures (ECMs) + income_statement_prior income_statement_post cash_balance liability - available_loans - required_cash_DSCR - required_noi_DSCR - required_saving_DSCR Return: dictonary: {{loan_id: amount}, {loan_id: amount}, {loan_id: amount}} can be list of loan objectives, or get from loan class @@ -45,12 +42,16 @@ def loan_allocation(loan_list, customer_preference, scenario, req_dscr): 'downpayment_max': 5000, 'expected_payback': 120, # in months 'cust_saving_dscr': 1.15} + scenario = { + 'construction_cost': date, total construction cost, estimated or quoted + 'saving': dictionary of Saving objectives, indicating saving and post retrofit bill + } Note: dscr == debt service coverage ratio """ first_year_saving = 28000 #!!!! need IS work, actually can be min_annual_saving - first_year_noi = 28000 #!!!! need IS work, actually can be min_annual_noi + first_year_noi = 28000 #!!!! need IS work, actually can be min_annual_noi, after saving noi first_year_cash = 100000 #!!!! need IS work, actually can be min_annual_cash sum_loan_max_amount = sum(list(current_loan.max_amount for current_loan in loan_list)) print('\n sum_loan_max_amount=', sum_loan_max_amount) diff --git a/bpfin/tests/testdata/sample_data.py b/bpfin/tests/testdata/sample_data.py index 8d8d277..63840b6 100644 --- a/bpfin/tests/testdata/sample_data.py +++ b/bpfin/tests/testdata/sample_data.py @@ -537,6 +537,7 @@ customer_preference = { # Engineering Scenarios scenario = { 'cost_estimation': 150000.00, + # 'saving': saving_dict, # 'post_income_statement': post_income_statement } -- GitLab From 897c7bb5bb06c8ad4993b8ebf43192628111fe3d Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Wed, 3 May 2017 21:11:39 -0400 Subject: [PATCH 24/28] FInalize loan_allocate func, and its description. --- bpfin/financials/financial_lib.py | 29 ++-- bpfin/financials/loan_allocation.py | 155 ++++++------------ .../test_financials/test_financial_lib.py | 6 +- 3 files changed, 67 insertions(+), 123 deletions(-) diff --git a/bpfin/financials/financial_lib.py b/bpfin/financials/financial_lib.py index bf0624b..b59b4fa 100644 --- a/bpfin/financials/financial_lib.py +++ b/bpfin/financials/financial_lib.py @@ -309,8 +309,8 @@ class Income_Statement_Table(): """ Create a income statement table, containing multiple years, including historical and projected data Key Attributes are: - hist_table (list): list of single year income_statement objectives, containing historical data - table(list): list of single year income_statement objectives, containing historical and projected data + hist_table (list): list of single year income_statement objects, containing historical data + table(list): list of single year income_statement objects, containing historical and projected data """ def __init__(self, raw_income_input, annual_bill_table): """ @@ -323,8 +323,8 @@ class Income_Statement_Table(): cagr (float): compound annual growth rate other_utility_percent (float): percentage, (average other_utility / average revenue) non_utility_expense_percent (float): percentage, (average non_utility_expense_percent / average revenue) - hist_table (list): list of single year income_statement objectives, containing historical data - table(list): list of single year income_statement objectives, containing historical and projected data + hist_table (list): list of single year income_statement objects, containing historical data + table(list): list of single year income_statement objects, containing historical and projected data characters (dictionary): contains key characters determined from historical data. Is used to project Description: @@ -387,13 +387,13 @@ class Income_Statement_Table(): def project(self, growth_rate_flag, analysis_date, annual_bill_table): """ - Project future income statement. Append multiple single year income_statement objectives to self.table + Project future income statement. Append multiple single year income_statement objects to self.table Args: growth_rate_flag (float): indicating assumed growth rate, -2.0 == cagr, -1.0 == historical average analysis_date (dictionary): proforma's starting date and the years of proforma annual_bill_table (dictionary): dictionary of dictionary of annual bills, for 4 utility_types Return: - list: list of single year income_statement objectives, containing historical and projection + list: list of single year income_statement objects, containing historical and projection Note: project() overwrites existing projection data """ @@ -408,7 +408,6 @@ class Income_Statement_Table(): current_income_statement = Income_Statement_Next( year, last_revenue, growth_rate_flag, self.characters, annual_bill_table) current_table.append(current_income_statement) - # print(current_income_statement.revenue, current_income_statement.noi) self.table = current_table return current_table @@ -667,9 +666,9 @@ class Loan(): class Loan_List(): """ - Create a list of loan objectives, in order to allocate loan amount among loans, and calculate schedules for loans + Create a list of loan objects, in order to allocate loan amount among loans, and calculate schedules for loans Attributes: - loan_list (list): list of loan objectives + loan_list (list): list of loan objects """ def __init__(self, loan_input_list): """ @@ -683,7 +682,7 @@ class Loan_List(): temp_loan = Loan(current_loan) self.loan_list.append(temp_loan) - def allocate(self, customer_preference, scenario, req_dscr): + def allocate(self, customer_preference, scenario, req_dscr, first_year_saving, first_year_noi, first_year_cash): """ !!! The functions is not finished yet !!! @@ -705,16 +704,16 @@ class Loan_List(): 'req_saving_dscr': 1.10 } Returns: - list: list of Loan objectives, with amount assigned, no schedule assigned + list: list of Loan objects, with amount assigned, no schedule assigned """ current_loan_list = copy.deepcopy(self.loan_list) allocation = loan_allocation( - current_loan_list, customer_preference, scenario, req_dscr) + current_loan_list, customer_preference, scenario, req_dscr, first_year_saving, first_year_noi, first_year_cash) for current_loan, current_amount in zip(current_loan_list, allocation): current_loan.put_amount(current_amount) return current_loan_list - def get_schedule(self, customer_preference, scenario, req_dscr, loan_start_date): + def get_schedule(self, customer_preference, scenario, req_dscr, loan_start_date, first_year_saving, first_year_noi, first_year_cash): """ !!! The functions is not finished yet !!! Because self.allocate needs improve @@ -722,9 +721,9 @@ class Loan_List(): Args: Returns: - list: list of Loan objectives, with amounts and schedules assigned + list: list of Loan objects, with amounts and schedules assigned """ - allocated_loan_list = self.allocate(customer_preference, scenario, req_dscr) + allocated_loan_list = self.allocate(customer_preference, scenario, req_dscr, first_year_saving, first_year_noi, first_year_cash) for current_loan in allocated_loan_list: current_loan.put_terms(loan_start_date) current_loan.originate() diff --git a/bpfin/financials/loan_allocation.py b/bpfin/financials/loan_allocation.py index 1ec4436..11fcd97 100644 --- a/bpfin/financials/loan_allocation.py +++ b/bpfin/financials/loan_allocation.py @@ -1,42 +1,53 @@ # import bpfin.financials.financial_lib as fl -import bpfin.tests.testdata.sample_data as db -import bpfin.utilbills.bill_lib as bl -from bpfin.lib import other as lib +# import bpfin.tests.testdata.sample_data as db +# import bpfin.utilbills.bill_lib as bl +# from bpfin.lib import other as lib from scipy.optimize import linprog -import pprint +# import pprint -# def form_loan_list(loan_term_dict): -# # maybe this should happen at backend, by passing in Loan class directly -# loan_list = [] -# for loan_id in loan_term_dict: -# temp_loan = None -# temp_loan = fl.Loan(loan_term_dict[loan_id]) -# loan_list.append(temp_loan) -# # loan_list[loan_id] = temp_loan.convert_loan_class() -# return loan_list - - -def loan_allocation(loan_list, customer_preference, scenario, req_dscr): +def loan_allocation( + loan_list, + customer_preference, + scenario, + req_dscr, + first_year_saving, + first_year_noi, + first_year_cash): """ - !!! The functions is not finished yet !!! - Take inputs of available financing options(loans and self-finance), and other constrains, determine the best financing plan, with minimum debt service. + Run a linear programming (LP) to determine the result, which is how much to borrow from each lender + Math process is as following: + **** pre-request is: + quoted_cost <= sum_loan_max_amount + downpayment_max + **** LP constrains are: + 1. objective function, determine minimum annual total DS. + x1/payback1, x2/payback2, x3/payback3 = minimum + * x is loan amount, no self-finance + 2. x variables constraint + x1 + x2 + x3 <= max + max = min(project_cost, sum_loan_max_amount, saving/dscr, noi/dscr, cash/dscr) + * default assumption is customer wants to self-finance but use loan first + self-finance amount = project_cost - sum_x + ** if customer wants to use self-fiance first, then + project_cost = quoted_cost - self_finance_max + 3. x variables bounds + 0 <= x[i] <= loan[i]_max_amount + Args: loan_list (list): list of objectives of loans customer_preference (dictionary): customer preference for financing plan req_dscr(dictionary): required dscr - commissioning_date (date): construction finish date. Saving start at NEXT month scenario (dictionary): package of energy conservation measures (ECMs) - - income_statement_prior - income_statement_post - cash_balance - liability + first_year_saving (float): first year saving. # maybe it can be min_annual_saving + first_year_noi (float): first year noi, after commissioning date. # maybe it can be min_noi + first_year_cash (float): first year cash, after commissioning date + # liability: do we need this? it's already calculated in balance sheet + # commissioning_date (date): construction finish date. Saving start at NEXT month Return: - dictonary: {{loan_id: amount}, {loan_id: amount}, {loan_id: amount}} - can be list of loan objectives, or get from loan class + list: list of loan amount that borrowed from each lender + Description: customer_preference = { 'downpayment_max': 5000, @@ -44,15 +55,16 @@ def loan_allocation(loan_list, customer_preference, scenario, req_dscr): 'cust_saving_dscr': 1.15} scenario = { 'construction_cost': date, total construction cost, estimated or quoted - 'saving': dictionary of Saving objectives, indicating saving and post retrofit bill + # 'saving': dictionary of Saving objectives, indicating saving and post retrofit bill } Note: + noi == net operating income dscr == debt service coverage ratio + To do: calculate self_finance amount + if customer wants to self-finance, but loan first -> SF = cost - sum(xi) + if customer wants to S-F, but SF first -> max(xi) = cost - SF_max """ - first_year_saving = 28000 #!!!! need IS work, actually can be min_annual_saving - first_year_noi = 28000 #!!!! need IS work, actually can be min_annual_noi, after saving noi - first_year_cash = 100000 #!!!! need IS work, actually can be min_annual_cash sum_loan_max_amount = sum(list(current_loan.max_amount for current_loan in loan_list)) print('\n sum_loan_max_amount=', sum_loan_max_amount) # pre-request judge: project cost should be lower than total available financing @@ -61,10 +73,9 @@ def loan_allocation(loan_list, customer_preference, scenario, req_dscr): return None # linear programming (LP) - # LP constrains + # Set LP constrains. # objective function: x1/payback1, x2/payback2, x3/payback3 = minimum c = list(1 / current_loan.payback for current_loan in loan_list) - print('\n obj fun =', c) # x variables constraint, x1 + x2 + x3 >= max (financing amount >= project cost) # max = min(project_cost, sum_loan_max_amount, saving/dscr, noi/dscr, cash/dscr) A = [] @@ -74,92 +85,22 @@ def loan_allocation(loan_list, customer_preference, scenario, req_dscr): 0 - scenario['cost_estimation'] # 0 - sum_loan_max_amount, ) - A.append(c) + A.append(c) # sum(x_i/payback_i) <= required. aka. sum(debt_service) <= min_required_debt_service b.append(min( first_year_saving / req_dscr['req_saving_dscr'], first_year_noi / req_dscr['req_noi_dscr'], first_year_cash / req_dscr['req_cash_dscr'] # 0 - first_year_noi / customer_preference['cust_saving_dscr'], )) - print('\n', A, '<=', b) # x variables bounds, 0 <= x[i] <= loan[i]_max_amount bound_list = list((0, current_loan.max_amount) for current_loan in loan_list) - print('bound_list =', bound_list) # LP calculation. x[i] == loan amount from loan[i] res = linprog(c, A_ub=A, b_ub=b, bounds=bound_list, options={'disp': False}) - print('x1=', res.x[0], 'x2=', res.x[1], 'x3=', res.x[2]) - return res.x - # # run a linear programming to determine the result. - # ****pre-request is: - # quoted_cost <= sum_loan_max_amount + downpayment_max - # - # ****LP constrains are: - # 1. objective function, determine minimum annual total DS. - # x1/payback1, x2/payback2, x3/payback3 = minimum - # * x is loan amount, no self-finance - # 2. x variables constraint - # x1 + x2 + x3 <= max - # max = min(project_cost, sum_loan_max_amount, saving/dscr, noi/dscr, cash/dscr) - # * default assumption is customer wants to self-finance but use loan first - # self-finance amount = project_cost - sum_x - # ** if customer wants to use self-fiance first, then - # project_cost = quoted_cost - self_finance_max - # 3. x variables bounds - # 0 <= x[i] <= loan[i]_max_amount - - -# loan_list = print(form_loan_list(db.loan_term_dict)) # output is in objective format -# loan_allocation( -# form_loan_list(db.loan_term_dict), -# db.customer_preference, -# db.scenario, -# db.req_dscr) - - - # # cauclate loan amount should be borrowed from each bank, given assumed total cost and loan info - # def loan_allocate(total_cost, loan_list): - # sum_loan_max = 0 - # bound_list = [] - # c_base = [] - # for loan in loan_list: - # sum_loan_max += loan.max_amount - # c_base.append( - # 1 / loan.payback) # loan amount / ratio = DS, targeting minimum total DS, equivalent to longest payback - # bound_list.append((0, loan.max_amount)) - # c = c_base # c_base is the objective function in linear programming - # A = [-1] * len(loan_list) - # b = [max(0 - total_cost, 0 - sum_loan_max)] - # bounds = bound_list - # if total_cost > sum_loan_max: - # print('alert: upfront cost > available loan amount') - # res = linprog(c, A_ub=A, b_ub=b, bounds=bounds, options={'disp': False}) - # for i in range(len(loan_list)): - # loan_list[i].amount = res.x[i] - # # return res.x - - -# def buget_loan_allocation(): -# """ -# Take inputs of available financing options, and other constrains, -# determine the best financing plan, with minimum debt service. -# Args: -# construction_date -# construction_cost -# income_statement_prior -# income_statement_prior -# cash_balance -# liability -# available_loan -# customer_preference -# required_cash_DSCR -# required_noi_DSCR -# required_saving_DSCR -# Return: -# dictonary: {{loan_id: amount}, {loan_id: amount}, {loan_id: amount}} - -# Note: in return, loan_id == 0 means self_finance -# """ + + +# ******** the following is draft for old work, for budge calculator development ******** + # ## LP function, return total budget and variables # def budget_LP(Financial_list,Loan_list,saving_percentage,other_loan,exp_pb,SF_fin,is_loan_first,saving_contigency,min_DSCR): # # loan information ************* diff --git a/bpfin/tests/test_financials/test_financial_lib.py b/bpfin/tests/test_financials/test_financial_lib.py index 43519a4..184a0e7 100644 --- a/bpfin/tests/test_financials/test_financial_lib.py +++ b/bpfin/tests/test_financials/test_financial_lib.py @@ -59,5 +59,9 @@ def test_Loan_List(): input_customer_preference, input_scenario, input_req_dscr, - input_loan_start_date) + input_loan_start_date, + 28000.0, + 28000.0, + 100000.0) assert output_loan1_end_balance == result_loan_list[0].principal_end_balance +# , first_year_saving, first_year_noi, first_year_cash -- GitLab From 3838b413a4682dd44b23c94f803a3813ac2aeb55 Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Wed, 3 May 2017 21:24:53 -0400 Subject: [PATCH 25/28] Finalize Loan and Loan_List class, with loan_allocation updated --- bpfin/financials/financial_lib.py | 60 ++++++++++--------- bpfin/financials/loan_allocation.py | 25 ++++---- .../test_financials/test_financial_lib.py | 11 ++-- 3 files changed, 48 insertions(+), 48 deletions(-) diff --git a/bpfin/financials/financial_lib.py b/bpfin/financials/financial_lib.py index b59b4fa..bc2a617 100644 --- a/bpfin/financials/financial_lib.py +++ b/bpfin/financials/financial_lib.py @@ -529,26 +529,6 @@ class Income_Statement_Table(): # print(form_list) -# def get_income_statement_single_year(self, year, income_statement_table): -# for current_income_statement in income_statement_table: -# if current_income_statement.year == year: -# return { -# 'year': current_income_statement.year, -# 'revenue': current_income_statement.revenue, -# 'utility_expense': current_income_statement.utility_expense, -# 'energy_opex': current_income_statement.energy_opex, -# 'electricity_opex': current_income_statement.electricity_opex, -# 'gas_opex': current_income_statement.gas_opex, -# 'oil_opex': current_income_statement.oil_opex, -# 'water_opex': current_income_statement.water_opex, -# 'other_utility': current_income_statement.other_utility, -# 'non_utility_expense': current_income_statement.non_utility_expense, -# 'net_non_energy_opex': current_income_statement.net_non_energy_opex, -# 'total_opex': current_income_statement.total_opex, -# 'noi': current_income_statement.noi} -# return None - - # ************* Loan *************** class Loan(): """ @@ -684,8 +664,6 @@ class Loan_List(): def allocate(self, customer_preference, scenario, req_dscr, first_year_saving, first_year_noi, first_year_cash): """ - !!! The functions is not finished yet !!! - Allocate loan amount based on project cost, loan terms, income statements, scenario, other requirements. Apply linear programming method to allocate loan amount among loans, create financing plan. Args: @@ -703,27 +681,51 @@ class Loan_List(): 'req_cash_dscr': 1.15, 'req_saving_dscr': 1.10 } + first_year_saving (float): first year saving. # maybe it can be min_annual_saving + first_year_noi (float): first year noi, after commissioning date. # maybe it can be min_noi + first_year_cash (float): first year cash, after commissioning date Returns: list: list of Loan objects, with amount assigned, no schedule assigned """ current_loan_list = copy.deepcopy(self.loan_list) - allocation = loan_allocation( - current_loan_list, customer_preference, scenario, req_dscr, first_year_saving, first_year_noi, first_year_cash) + allocation = loan_allocation(current_loan_list, customer_preference, + scenario, req_dscr, first_year_saving, + first_year_noi, first_year_cash) for current_loan, current_amount in zip(current_loan_list, allocation): current_loan.put_amount(current_amount) return current_loan_list - def get_schedule(self, customer_preference, scenario, req_dscr, loan_start_date, first_year_saving, first_year_noi, first_year_cash): + def get_schedule(self, customer_preference, scenario, req_dscr, + loan_start_date, first_year_saving, first_year_noi, + first_year_cash): """ - !!! The functions is not finished yet !!! Because self.allocate needs improve - - Get the schedule of all loans, after allocate loan amounts + Get the schedule of all loans, after allocating loan amounts Args: + customer_preference (dictionary): dict of customer preference, { + 'downpayment_max': float, upper limit of self finance amount, + 'expected_payback': int, months that customer expect the project can payback, + 'cust_saving_dscr': float, customer required lower limit of (saving / debt service) ratio + } + scenario (dictionary): dict of project economics and saving scenario, { + 'cost_estimation': float, estimated project cost + 'post_income_statement': objective of Income_Statement_Table , projected income_statement with savings + } + req_dscr (dictionary): dict of required debt service coverage ratios, { + 'req_noi_dscr': 1.15, + 'req_cash_dscr': 1.15, + 'req_saving_dscr': 1.10 + } + loan_start_date (date): the date when loan starts. Usually is when construction starts + first_year_saving (float): first year saving. # maybe it can be min_annual_saving + first_year_noi (float): first year noi, after commissioning date. # maybe it can be min_noi + first_year_cash (float): first year cash, after commissioning date Returns: list: list of Loan objects, with amounts and schedules assigned """ - allocated_loan_list = self.allocate(customer_preference, scenario, req_dscr, first_year_saving, first_year_noi, first_year_cash) + allocated_loan_list = self.allocate(customer_preference, scenario, + req_dscr, first_year_saving, + first_year_noi, first_year_cash) for current_loan in allocated_loan_list: current_loan.put_terms(loan_start_date) current_loan.originate() diff --git a/bpfin/financials/loan_allocation.py b/bpfin/financials/loan_allocation.py index 11fcd97..c0d8149 100644 --- a/bpfin/financials/loan_allocation.py +++ b/bpfin/financials/loan_allocation.py @@ -1,9 +1,4 @@ -# import bpfin.financials.financial_lib as fl -# import bpfin.tests.testdata.sample_data as db -# import bpfin.utilbills.bill_lib as bl -# from bpfin.lib import other as lib from scipy.optimize import linprog -# import pprint def loan_allocation( @@ -49,13 +44,19 @@ def loan_allocation( list: list of loan amount that borrowed from each lender Description: - customer_preference = { - 'downpayment_max': 5000, - 'expected_payback': 120, # in months - 'cust_saving_dscr': 1.15} - scenario = { - 'construction_cost': date, total construction cost, estimated or quoted - # 'saving': dictionary of Saving objectives, indicating saving and post retrofit bill + customer_preference (dictionary): dict of customer preference, { + 'downpayment_max': float, upper limit of self finance amount, + 'expected_payback': int, months that customer expect the project can payback, + 'cust_saving_dscr': float, customer required lower limit of (saving / debt service) ratio + } + scenario (dictionary): dict of project economics and saving scenario, { + 'cost_estimation': float, estimated project cost + 'post_income_statement': objective of Income_Statement_Table , projected income_statement with savings + } + req_dscr (dictionary): dict of required debt service coverage ratios, { + 'req_noi_dscr': 1.15, + 'req_cash_dscr': 1.15, + 'req_saving_dscr': 1.10 } Note: noi == net operating income diff --git a/bpfin/tests/test_financials/test_financial_lib.py b/bpfin/tests/test_financials/test_financial_lib.py index 184a0e7..7d10684 100644 --- a/bpfin/tests/test_financials/test_financial_lib.py +++ b/bpfin/tests/test_financials/test_financial_lib.py @@ -23,12 +23,6 @@ def test_income_statement_single_year(): # should be deleted def test_Income_Statement_Table(): - """ - !!! To do: this test file need update when allocation calculation is finished !! - - test Loan and Loan_List classes. - by calling Loan_List.get_schedule(), all calculation functions are called and tested. - """ input_raw_income_input = db.raw_income_input input_annual_bill_table = db.bill_overview_organized output_cagr = 0.05409255338945984 @@ -48,6 +42,10 @@ def test_Income_Statement_Table(): def test_Loan_List(): + """ + test Loan and Loan_List classes. + by calling Loan_List.get_schedule(), all calculation functions are called and tested. + """ input_loan_input_list = db.loan_input_list input_customer_preference = db.customer_preference input_scenario = db.scenario @@ -64,4 +62,3 @@ def test_Loan_List(): 28000.0, 100000.0) assert output_loan1_end_balance == result_loan_list[0].principal_end_balance -# , first_year_saving, first_year_noi, first_year_cash -- GitLab From eca1f6365e8cc707704a30bc514209a11605fe35 Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Thu, 4 May 2017 10:42:34 -0400 Subject: [PATCH 26/28] Create Loan.py file. Clean Loan and Loan_List, and the test files --- bpfin/financials/financial_lib.py | 212 ----------------- bpfin/financials/loan.py | 214 ++++++++++++++++++ bpfin/financials/saving.py | 2 +- .../test_financials/test_financial_lib.py | 2 +- 4 files changed, 216 insertions(+), 214 deletions(-) create mode 100644 bpfin/financials/loan.py diff --git a/bpfin/financials/financial_lib.py b/bpfin/financials/financial_lib.py index 326a80b..8678dc7 100644 --- a/bpfin/financials/financial_lib.py +++ b/bpfin/financials/financial_lib.py @@ -698,215 +698,3 @@ class Balance_Sheet_Table(): # print(form_list) -# ************* Loan *************** -class Loan(): - """ - Single loan, with terms and schedule - Attributes: - institute (string): name of loan lender - max_amount (float): upper limit of loan amount that can be borrowed - interest (float): annual interest rate. 0.025 = 2.5% - duration (int): loan duration scheduled be repaid, in month - payback (float): ratio of (loan amount/annual debt service), similar to but NOT the payback period, in year - amount (float): amount borrowed or suggested to be borrowed from this lender - debt_service (float): monthly debt service need to be paid. Now we use amortization calculation - terms (list): list of datetime.date, the months that scheduled to get repay. Dates are end of month - principal_start_balance (list): list of float, begining balance of schedule outstanding principal - principal_end_balance (list): list of float, ending balance of schedule outstanding principal - interest_due (list): list of float, scheduled interest repayment due - principal_due (list): list of float, scheduled principal repayment due - debt_service_due (list): list of float, scheduled interest + principal repayment due - """ - def __init__(self, loan_term): - """ - Initiate a single loan, with inputs of basic terms, and calculate payback as a key character - Args: - loan_term (dictionary): characters of a loan. - Description: - loan_term = {'institute': 'NYSERDA', 'max_amount': 5000.00, 'interest': 0.08, 'duration': 120} - To Do: - verify interest rate != 0 - """ - self.institute = loan_term['institute'] - self.max_amount = loan_term['max_amount'] - self.interest = loan_term['interest'] - self.duration = loan_term['duration'] # in month - # todo interest rate!=0 - # calculate loan's (loan amount/debt service), similar to payback year - self.payback = ((1 + self.interest) ** (self.duration / 12) - 1) / (self.interest * ( - 1 + self.interest) ** (self.duration / 12)) - self.amount = 0 - self.debt_service = 0 - self.terms = [] - self.principal_start_balance = [] - self.principal_end_balance = [] - self.interest_due = [] - self.principal_due = [] - self.debt_service_due = [] - - def get_loan_terms(self): - """ - Get loan terms in dictionary format. - Return: - dictionary: a dict containing all characters, not schedule - """ - return { - 'institute': self.institute, - 'max_amount': self.max_amount, - 'interest': self.interest, - 'duration': self.duration, - 'payback': self.payback, - 'terms': self.terms, - 'amount': self.amount, - 'debt_service': self.debt_service} - - def put_amount(self, amount): - """ - Put loan amount to loan amount, calculate monthly debt service. - Args: - amount (float): loan amount that calculated or suggested to borrow - """ - self.amount = amount - self.debt_service = self.amount * ((self.interest / 12) * (1 + self.interest / 12) ** (self.duration)) / ( - (1 + self.interest / 12) ** (self.duration) - 1) - - def put_terms(self, loan_start_date): - """ - Put loan start date and calculate schedule terms. - Args: - loan_start_date(datetime.date): the date that the loan will be deployed - """ - self.terms = form_bill_calendar(loan_start_date, float(self.duration)/12)[1] - - def originate(self): - """ - Origniate loan, calculating its schedule - Note: - if amount == 0, but terms != None, the schedule will still be calculated - if terms == None, the function returns None - """ - if not self.terms: - # print('missing start_data') - return None - debt_service = self.debt_service - principal_start_balance = [] - principal_end_balance = [] - interest_due = [] - principal_due = [] - debt_service_due = [] - - start_balance = self.amount - for termID in self.terms: - interest_amount = start_balance * self.interest / 12 - principal_amount = debt_service - interest_amount - principal_start_balance.append(start_balance) - interest_due.append(interest_amount) - debt_service_due.append(debt_service) - principal_due.append(principal_amount) - principal_end_balance.append( - start_balance - principal_amount if (start_balance - principal_amount) >= 10e-5 else 0) - start_balance = start_balance - principal_amount - self.principal_start_balance = principal_start_balance - self.principal_end_balance = principal_end_balance - self.interest_due = interest_due - self.principal_due = principal_due - self.debt_service_due = debt_service_due - - -class Loan_List(): - """ - Create a list of loan objects, in order to allocate loan amount among loans, and calculate schedules for loans - Attributes: - loan_list (list): list of loan objects - """ - def __init__(self, loan_input_list): - """ - Initiate loan_list with inputs. - Args: - loan_input_list (list): list of dictionary of loan basic terms. - """ - self.loan_list = [] - for current_loan in loan_input_list: - temp_loan = None - temp_loan = Loan(current_loan) - self.loan_list.append(temp_loan) - - def allocate(self, customer_preference, scenario, req_dscr, first_year_saving, first_year_noi, first_year_cash): - """ - Allocate loan amount based on project cost, loan terms, income statements, scenario, other requirements. - Apply linear programming method to allocate loan amount among loans, create financing plan. - Args: - customer_preference (dictionary): dict of customer preference, { - 'downpayment_max': float, upper limit of self finance amount, - 'expected_payback': int, months that customer expect the project can payback, - 'cust_saving_dscr': float, customer required lower limit of (saving / debt service) ratio - } - scenario (dictionary): dict of project economics and saving scenario, { - 'cost_estimation': float, estimated project cost - 'post_income_statement': objective of Income_Statement_Table , projected income_statement with savings - } - req_dscr (dictionary): dict of required debt service coverage ratios, { - 'req_noi_dscr': 1.15, - 'req_cash_dscr': 1.15, - 'req_saving_dscr': 1.10 - } - first_year_saving (float): first year saving. # maybe it can be min_annual_saving - first_year_noi (float): first year noi, after commissioning date. # maybe it can be min_noi - first_year_cash (float): first year cash, after commissioning date - Returns: - list: list of Loan objects, with amount assigned, no schedule assigned - """ - current_loan_list = copy.deepcopy(self.loan_list) - allocation = loan_allocation(current_loan_list, customer_preference, - scenario, req_dscr, first_year_saving, - first_year_noi, first_year_cash) - for current_loan, current_amount in zip(current_loan_list, allocation): - current_loan.put_amount(current_amount) - return current_loan_list - - def get_schedule(self, customer_preference, scenario, req_dscr, - loan_start_date, first_year_saving, first_year_noi, - first_year_cash): - """ - Get the schedule of all loans, after allocating loan amounts - Args: - customer_preference (dictionary): dict of customer preference, { - 'downpayment_max': float, upper limit of self finance amount, - 'expected_payback': int, months that customer expect the project can payback, - 'cust_saving_dscr': float, customer required lower limit of (saving / debt service) ratio - } - scenario (dictionary): dict of project economics and saving scenario, { - 'cost_estimation': float, estimated project cost - 'post_income_statement': objective of Income_Statement_Table , projected income_statement with savings - } - req_dscr (dictionary): dict of required debt service coverage ratios, { - 'req_noi_dscr': 1.15, - 'req_cash_dscr': 1.15, - 'req_saving_dscr': 1.10 - } - loan_start_date (date): the date when loan starts. Usually is when construction starts - first_year_saving (float): first year saving. # maybe it can be min_annual_saving - first_year_noi (float): first year noi, after commissioning date. # maybe it can be min_noi - first_year_cash (float): first year cash, after commissioning date - - Returns: - list: list of Loan objects, with amounts and schedules assigned - """ - allocated_loan_list = self.allocate(customer_preference, scenario, - req_dscr, first_year_saving, - first_year_noi, first_year_cash) - for current_loan in allocated_loan_list: - current_loan.put_terms(loan_start_date) - current_loan.originate() - return allocated_loan_list - - -# ****** ugly tests Loan and Loan_List class ********** -# loan_list = Loan_List(db.loan_input_list) -# allocated = loan_list.get_schedule( -# db.customer_preference, -# db.scenario, -# db.req_dscr, -# datetime.date(2017, 1, 12)) -# print(allocated[0].terms) -# print(allocated[0].principal_end_balance) diff --git a/bpfin/financials/loan.py b/bpfin/financials/loan.py new file mode 100644 index 0000000..7b34897 --- /dev/null +++ b/bpfin/financials/loan.py @@ -0,0 +1,214 @@ +import copy +from bpfin.utilbills.bill_lib import form_bill_calendar +from bpfin.financials.loan_allocation import loan_allocation + + +class Loan(): + """ + Single loan, with terms and schedule + Attributes: + institute (string): name of loan lender + max_amount (float): upper limit of loan amount that can be borrowed + interest (float): annual interest rate. 0.025 = 2.5% + duration (int): loan duration scheduled be repaid, in month + payback (float): ratio of (loan amount/annual debt service), similar to but NOT the payback period, in year + amount (float): amount borrowed or suggested to be borrowed from this lender + debt_service (float): monthly debt service need to be paid. Now we use amortization calculation + terms (list): list of datetime.date, the months that scheduled to get repay. Dates are end of month + principal_start_balance (list): list of float, begining balance of schedule outstanding principal + principal_end_balance (list): list of float, ending balance of schedule outstanding principal + interest_due (list): list of float, scheduled interest repayment due + principal_due (list): list of float, scheduled principal repayment due + debt_service_due (list): list of float, scheduled interest + principal repayment due + """ + def __init__(self, loan_term): + """ + Initiate a single loan, with inputs of basic terms, and calculate payback as a key character + Args: + loan_term (dictionary): characters of a loan. + Description: + loan_term = {'institute': 'NYSERDA', 'max_amount': 5000.00, 'interest': 0.08, 'duration': 120} + To Do: + verify interest rate != 0 + """ + self.institute = loan_term['institute'] + self.max_amount = loan_term['max_amount'] + self.interest = loan_term['interest'] + self.duration = loan_term['duration'] # in month + self.payback = ((1 + self.interest) ** (self.duration / 12) - 1) / (self.interest * ( + 1 + self.interest) ** (self.duration / 12)) + self.amount = 0 + self.debt_service = 0 + self.terms = [] + self.principal_start_balance = [] + self.principal_end_balance = [] + self.interest_due = [] + self.principal_due = [] + self.debt_service_due = [] + + def get_loan_terms(self): + """ + Get loan terms in dictionary format. + Return: + dictionary: a dict containing all characters, not schedule + """ + return { + 'institute': self.institute, + 'max_amount': self.max_amount, + 'interest': self.interest, + 'duration': self.duration, + 'payback': self.payback, + 'terms': self.terms, + 'amount': self.amount, + 'debt_service': self.debt_service} + + def put_amount(self, amount): + """ + Put loan amount to loan amount, calculate monthly debt service. + Args: + amount (float): loan amount that calculated or suggested to borrow + """ + self.amount = amount + self.debt_service = self.amount * ((self.interest / 12) * (1 + self.interest / 12) ** (self.duration)) / ( + (1 + self.interest / 12) ** (self.duration) - 1) + + def put_terms(self, loan_start_date): + """ + Put loan start date and calculate schedule terms. + Args: + loan_start_date(datetime.date): the date that the loan will be deployed + """ + self.terms = form_bill_calendar(loan_start_date, float(self.duration)/12)[1] + + def originate(self): + """ + Origniate loan, calculating its schedule + Note: + if amount == 0, but terms != None, the schedule will still be calculated + if terms == None, the function returns None + """ + if not self.terms: + # print('missing start_data') + return None + debt_service = self.debt_service + principal_start_balance = [] + principal_end_balance = [] + interest_due = [] + principal_due = [] + debt_service_due = [] + + start_balance = self.amount + for termID in self.terms: + interest_amount = start_balance * self.interest / 12 + principal_amount = debt_service - interest_amount + principal_start_balance.append(start_balance) + interest_due.append(interest_amount) + debt_service_due.append(debt_service) + principal_due.append(principal_amount) + principal_end_balance.append( + start_balance - principal_amount if (start_balance - principal_amount) >= 10e-5 else 0) + start_balance = start_balance - principal_amount + self.principal_start_balance = principal_start_balance + self.principal_end_balance = principal_end_balance + self.interest_due = interest_due + self.principal_due = principal_due + self.debt_service_due = debt_service_due + + +class Loan_List(): + """ + Create a list of loan objects, in order to allocate loan amount among loans, and calculate schedules for loans + Attributes: + loan_list (list): list of loan objects + """ + def __init__(self, loan_input_list): + """ + Initiate loan_list with inputs. + Args: + loan_input_list (list): list of dictionary of loan basic terms. + """ + self.loan_list = [] + for current_loan in loan_input_list: + temp_loan = None + temp_loan = Loan(current_loan) + self.loan_list.append(temp_loan) + + def allocate(self, customer_preference, scenario, req_dscr, first_year_saving, first_year_noi, first_year_cash): + """ + Allocate loan amount based on project cost, loan terms, income statements, scenario, other requirements. + Apply linear programming method to allocate loan amount among loans, create financing plan. + Args: + customer_preference (dictionary): dict of customer preference, { + 'downpayment_max': float, upper limit of self finance amount, + 'expected_payback': int, months that customer expect the project can payback, + 'cust_saving_dscr': float, customer required lower limit of (saving / debt service) ratio + } + scenario (dictionary): dict of project economics and saving scenario, { + 'cost_estimation': float, estimated project cost + 'post_income_statement': objective of Income_Statement_Table , projected income_statement with savings + } + req_dscr (dictionary): dict of required debt service coverage ratios, { + 'req_noi_dscr': 1.15, + 'req_cash_dscr': 1.15, + 'req_saving_dscr': 1.10 + } + first_year_saving (float): first year saving. # maybe it can be min_annual_saving + first_year_noi (float): first year noi, after commissioning date. # maybe it can be min_noi + first_year_cash (float): first year cash, after commissioning date + Returns: + list: list of Loan objects, with amount assigned, no schedule assigned + """ + current_loan_list = copy.deepcopy(self.loan_list) + allocation = loan_allocation(current_loan_list, customer_preference, + scenario, req_dscr, first_year_saving, + first_year_noi, first_year_cash) + for current_loan, current_amount in zip(current_loan_list, allocation): + current_loan.put_amount(current_amount) + return current_loan_list + + def get_schedule(self, customer_preference, scenario, req_dscr, + loan_start_date, first_year_saving, first_year_noi, + first_year_cash): + """ + Get the schedule of all loans, after allocating loan amounts + Args: + customer_preference (dictionary): dict of customer preference, { + 'downpayment_max': float, upper limit of self finance amount, + 'expected_payback': int, months that customer expect the project can payback, + 'cust_saving_dscr': float, customer required lower limit of (saving / debt service) ratio + } + scenario (dictionary): dict of project economics and saving scenario, { + 'cost_estimation': float, estimated project cost + 'post_income_statement': objective of Income_Statement_Table , projected income_statement with savings + } + req_dscr (dictionary): dict of required debt service coverage ratios, { + 'req_noi_dscr': 1.15, + 'req_cash_dscr': 1.15, + 'req_saving_dscr': 1.10 + } + loan_start_date (date): the date when loan starts. Usually is when construction starts + first_year_saving (float): first year saving. # maybe it can be min_annual_saving + first_year_noi (float): first year noi, after commissioning date. # maybe it can be min_noi + first_year_cash (float): first year cash, after commissioning date + + Returns: + list: list of Loan objects, with amounts and schedules assigned + """ + allocated_loan_list = self.allocate(customer_preference, scenario, + req_dscr, first_year_saving, + first_year_noi, first_year_cash) + for current_loan in allocated_loan_list: + current_loan.put_terms(loan_start_date) + current_loan.originate() + return allocated_loan_list + + +# ****** ugly tests Loan and Loan_List class ********** +# loan_list = Loan_List(db.loan_input_list) +# allocated = loan_list.get_schedule( +# db.customer_preference, +# db.scenario, +# db.req_dscr, +# datetime.date(2017, 1, 12)) +# print(allocated[0].terms) +# print(allocated[0].principal_end_balance) diff --git a/bpfin/financials/saving.py b/bpfin/financials/saving.py index 02aa18e..befb92a 100644 --- a/bpfin/financials/saving.py +++ b/bpfin/financials/saving.py @@ -4,7 +4,7 @@ from bpfin.utilbills.bill_lib import cal_last_day from bpfin.utilbills.bill_lib import annualizing_projection from bpfin.utilbills.bill_lib import form_year_calendar from bpfin.utilbills.bill_lib import form_bill_calendar -from bpfin.tests.testdata import sample_data as db +# from bpfin.tests.testdata import sample_data as db from bpfin.utilbills.bill_post_proj_rough import bill_post_proj_rough diff --git a/bpfin/tests/test_financials/test_financial_lib.py b/bpfin/tests/test_financials/test_financial_lib.py index 50d5bee..f853cc7 100644 --- a/bpfin/tests/test_financials/test_financial_lib.py +++ b/bpfin/tests/test_financials/test_financial_lib.py @@ -1,7 +1,7 @@ import datetime import bpfin.financials.financial_lib as fl from bpfin.tests.testdata import sample_data as db -from bpfin.financials.financial_lib import Loan_List +from bpfin.financials.loan import Loan_List from bpfin.financials.financial_lib import Income_Statement_Table from bpfin.financials.financial_lib import Balance_Sheet_Table -- GitLab From 4d9893218522d071cc42eab5116fb5570223fec0 Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Thu, 4 May 2017 10:44:26 -0400 Subject: [PATCH 27/28] CLean imports in financial_lib --- bpfin/financials/financial_lib.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/bpfin/financials/financial_lib.py b/bpfin/financials/financial_lib.py index 8678dc7..4479bcf 100644 --- a/bpfin/financials/financial_lib.py +++ b/bpfin/financials/financial_lib.py @@ -1,16 +1,9 @@ -# import datetime -# import calendar -# from bpfin.lib import other as lib import copy from bpfin.lib import other as lib from bpfin.utilbills.bill_lib import form_bill_year -from bpfin.utilbills.bill_lib import form_bill_calendar from numpy import mean -from bpfin.financials.loan_allocation import loan_allocation # from bpfin.tests.testdata import sample_data as db -# import bpfin.utilbills.bill_lib as bl # import pprint -# import datetime def organize_bill_overview(bill_overview, analysis_date): -- GitLab From d69d7606ec1fb391f413d855405c7bb9f9b16cc6 Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Thu, 4 May 2017 10:47:30 -0400 Subject: [PATCH 28/28] Remove To Do from bill_post_proj_rough. Was a reminder to add in construction_date. Now the job is done by Saving class --- bpfin/utilbills/bill_post_proj_rough.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bpfin/utilbills/bill_post_proj_rough.py b/bpfin/utilbills/bill_post_proj_rough.py index 99a0719..5a975f4 100644 --- a/bpfin/utilbills/bill_post_proj_rough.py +++ b/bpfin/utilbills/bill_post_proj_rough.py @@ -19,8 +19,6 @@ def bill_post_proj_rough(prior_bill, saving_percent): {'date_from': post_bill_start, 'date_to': post_bill_end, 'usage': post_monthly_usage, 'charge': post_monthly_charge, 'price': post_monthly_price} - - To Do: add construction_date to project bill. Maybe create a separate function. for rough and reg """ post_bill_start = prior_bill['date_from'] post_bill_end = prior_bill['date_to'] -- GitLab