From fdd0a8dab306823b85675949da866b832fd34029 Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Tue, 27 Jun 2017 16:24:12 -0400 Subject: [PATCH 01/13] Add SF for loan_allocation --- bpfin/financials/loan_allocation.py | 102 ++++++++++++++++++++++------ 1 file changed, 81 insertions(+), 21 deletions(-) diff --git a/bpfin/financials/loan_allocation.py b/bpfin/financials/loan_allocation.py index 4423c34..fee98e5 100644 --- a/bpfin/financials/loan_allocation.py +++ b/bpfin/financials/loan_allocation.py @@ -1,4 +1,6 @@ from scipy.optimize import linprog +import copy +from bpfin.lib.other import sumproduct, multiply_list def loan_allocation( @@ -67,39 +69,97 @@ def loan_allocation( 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 """ + is_loan_first = False # !!!! current version assume customer prefer loan first. Flexibility will hould be built + downpayment_max = customer_preference['downpayment_max'] + expected_payback = customer_preference['expected_payback'] + + # # pre-requet check: at least one financing source is indicated: + loan_list = ([] if loan_list is None else copy.deepcopy(loan_list)) + downpayment_max = (0 if downpayment_max is None else copy.deepcopy(downpayment_max)) + # # pre-requet check: all dscr should not be None. If so, replace it with 0 + for dscr in req_dscr: + if not req_dscr[dscr]: + req_dscr[dscr] = 0 + + # # pre-request check: project cost should be lower than total available financing sum_loan_max_amount = sum(list(current_loan.max_amount for current_loan in loan_list)) - # pre-request judge: project cost should be lower than total available financing - if (sum_loan_max_amount + customer_preference['downpayment_max']) <= cost: - # print('alert: not financiable') + if (sum_loan_max_amount + downpayment_max) <= cost: + # print('\nalert: not financiable') return [0] * len(loan_list) + # linear programming (LP) # Set LP constrains. - # objective function: x1/payback1, x2/payback2, x3/payback3 = minimum - c = list(1 / current_loan.payback for current_loan in loan_list) + # objective function: x1/payback1 + x2/payback2 + x3/payback3 + sf/expected_pb = minimum + c_formula = list(1 / current_loan.payback for current_loan in loan_list) + # c_formula.append(1 / expected_payback * 12 if expected_payback else 0) + c_formula.append(0) + # 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 = [] - A.append([-1] * len(loan_list)) # -x1 -x2 -x3 <= -max - b = [] - b.append( - 0 - cost - # 0 - sum_loan_max_amount, - ) - 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'], - )) + A_matrix = [] + a_0 = list(1 / current_loan.payback for current_loan in loan_list) + a_1 = copy.deepcopy(a_0) + a_2 = copy.deepcopy(a_0) + a_3 = copy.deepcopy(a_0) + a_1.append(1 / expected_payback * 12 if expected_payback else 0) + a_2.append(0) + a_3.append(0) + a_4 = ([-1] * (len(loan_list) + 1)) # -x1 -x2 -x3 -sf <= -max + + A_matrix.append(multiply_list(a_1, (req_dscr['req_saving_dscr'] if first_year_saving is not None else 0))) + A_matrix.append(multiply_list(a_2, (req_dscr['req_noi_dscr'] if first_year_noi is not None else 0))) + A_matrix.append(multiply_list(a_2, (req_dscr['req_cash_dscr'] if first_year_cash is not None else 0))) + A_matrix.append(a_4) + + b_list = [] + b_list.append(first_year_saving if first_year_saving else 0) + b_list.append(first_year_noi if first_year_noi else 0) + b_list.append(first_year_cash if first_year_cash else 0) + b_list.append(0 - cost) + # x variables bounds, 0 <= x[i] <= loan[i]_max_amount bound_list = list((0, current_loan.max_amount) for current_loan in loan_list) + bound_list.append((0, downpayment_max if not is_loan_first else 0)) + # print('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']) - # print('\nLP constraints =', A, b, bound_list) + print('\nLP constraints =', A_matrix, b_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}) + res = linprog(c=c_formula, A_ub=A_matrix, b_ub=b_list, bounds=bound_list, options={'disp': False}) + total_ds = sumproduct(res.x, a_2) + + # Calculate self-finance amount if loan_fist is True + # if is_loan_first: + # if first_year_saving is not None and req_dscr['req_saving_dscr'] and expected_payback is not None: + # # self finance amount is capped by extra_saving, which is Savings/req_dscr - total loan burden + # extra_saving = float("{0:.4f}".format((first_year_saving/req_dscr['req_saving_dscr'] - total_ds))) + # print('\n extra saving =', extra_saving) + # downpayment = (min(max([0, extra_saving]) * expected_payback / 12, downpayment_max)) + # else: + # downpayment = downpayment_max + # res.x[-1] = downpayment + if res.success is False: return [0] * len(loan_list) return res.x +# **** ugly test **** +from bpfin.tests.testdata import feature_data as db +from bpfin.financials.loan import Loan_List +req_dscr = { + 'req_noi_dscr': 1.15, + 'req_cash_dscr': 1.05, + 'req_saving_dscr': 1.10 +} + +# test cases +print('\n Base Case:') +print(loan_allocation( + loan_list=Loan_List(db.raw_loan_input_list).loan_list, + customer_preference=db.customer_preference, + cost=80800, + req_dscr=req_dscr, + first_year_saving=14000, + first_year_noi=20000, + first_year_cash=20000, + )) -- GitLab From a898d95f000cb9a46e893edee033a3b1d1c455b8 Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Tue, 27 Jun 2017 18:23:29 -0400 Subject: [PATCH 02/13] Still need math work out --- bpfin/back_end_call/back_end_budget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bpfin/back_end_call/back_end_budget.py b/bpfin/back_end_call/back_end_budget.py index e2c6950..3feaeba 100644 --- a/bpfin/back_end_call/back_end_budget.py +++ b/bpfin/back_end_call/back_end_budget.py @@ -112,7 +112,7 @@ def budget_simulation( break budget_simulation_result = form_budget_simulation_result(loan_list) - print('\nbudget_simulation_result = ', budget_simulation_result) + # print('\nbudget_simulation_result = ', budget_simulation_result) # print('\n loan_list=', loan_list) # print('\n downpayment_max=', customer_preference['downpayment_max']) # print('\n expected_payback=', customer_preference['expected_payback']) -- GitLab From 01a08d31125b729a35ac15149dfcef59c534384a Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Wed, 28 Jun 2017 12:33:34 -0400 Subject: [PATCH 03/13] Add SF calculation math in loan_allocation. Need to solve sme extreme case --- bpfin/financials/loan_allocation.py | 32 ++++++++++++++++++---------- bpfin/tests/testdata/feature_data.py | 2 +- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/bpfin/financials/loan_allocation.py b/bpfin/financials/loan_allocation.py index fee98e5..ffea06f 100644 --- a/bpfin/financials/loan_allocation.py +++ b/bpfin/financials/loan_allocation.py @@ -89,10 +89,14 @@ def loan_allocation( # linear programming (LP) # Set LP constrains. - # objective function: x1/payback1 + x2/payback2 + x3/payback3 + sf/expected_pb = minimum + # objective function: x1/payback1 + x2/payback2 + x3/payback3 + sf/adjust_payback = minimum + # if loan_first: pretend SF more expensive than loans, adjust_payback < min(loan paybacks) + # if self-finance first: pretent SF cheaper than loans, adjust_payback -> infinity c_formula = list(1 / current_loan.payback for current_loan in loan_list) - # c_formula.append(1 / expected_payback * 12 if expected_payback else 0) - c_formula.append(0) + if is_loan_first is True: + c_formula.append(max(c_formula) + 0.1) + else: + c_formula.append(0) # !!! maybe, if loan_list is empty, put 1 here # 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) @@ -119,12 +123,13 @@ def loan_allocation( # x variables bounds, 0 <= x[i] <= loan[i]_max_amount bound_list = list((0, current_loan.max_amount) for current_loan in loan_list) - bound_list.append((0, downpayment_max if not is_loan_first else 0)) + bound_list.append((0, downpayment_max)) - # print('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']) - print('\nLP constraints =', A_matrix, b_list, bound_list) + print('\nLP obj_func =', c_formula) + print('\nLP constraint matrix =', A_matrix, b_list) + print('\nLP bounds =', bound_list) # LP calculation. x[i] == loan amount from loan[i] - res = linprog(c=c_formula, A_ub=A_matrix, b_ub=b_list, bounds=bound_list, options={'disp': False}) + res = linprog(c=c_formula, A_ub=A_matrix, b_ub=b_list, bounds=bound_list, options={'disp': True}) total_ds = sumproduct(res.x, a_2) # Calculate self-finance amount if loan_fist is True @@ -137,6 +142,9 @@ def loan_allocation( # else: # downpayment = downpayment_max # res.x[-1] = downpayment + print('\nLP cond 1 =', sumproduct(A_matrix[0], res.x)) + print('\nLP cond 2 =', sumproduct(A_matrix[1], res.x)) + print('\nLP cond 3 =', sumproduct(A_matrix[2], res.x)) if res.success is False: return [0] * len(loan_list) @@ -144,6 +152,7 @@ def loan_allocation( # **** ugly test **** +# what if no loan_list and SF first? any SF > cost will work... from bpfin.tests.testdata import feature_data as db from bpfin.financials.loan import Loan_List req_dscr = { @@ -155,11 +164,12 @@ req_dscr = { # test cases print('\n Base Case:') print(loan_allocation( - loan_list=Loan_List(db.raw_loan_input_list).loan_list, + # loan_list=Loan_List(db.raw_loan_input_list).loan_list, + loan_list=None, customer_preference=db.customer_preference, - cost=80800, + cost=5800, req_dscr=req_dscr, - first_year_saving=14000, - first_year_noi=20000, + first_year_saving=40000, + first_year_noi=18412, first_year_cash=20000, )) diff --git a/bpfin/tests/testdata/feature_data.py b/bpfin/tests/testdata/feature_data.py index f30f2dc..894e6bf 100644 --- a/bpfin/tests/testdata/feature_data.py +++ b/bpfin/tests/testdata/feature_data.py @@ -39,7 +39,7 @@ req_dscr = { # Customer Preference customer_preference = { 'downpayment_max': 50000.00, - 'expected_payback': 120, # in months + 'expected_payback': 120, # in months, default 120, extreme case 24 'cust_saving_dscr': 1.15 } -- GitLab From d50f4c953103f61f0a3de409015cfd7622baad9e Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Wed, 28 Jun 2017 12:42:05 -0400 Subject: [PATCH 04/13] Solve extreme case, no loan inputs. Need to solve is_loan_first option, and return SF part --- bpfin/financials/loan_allocation.py | 69 ++++++++++++----------------- 1 file changed, 29 insertions(+), 40 deletions(-) diff --git a/bpfin/financials/loan_allocation.py b/bpfin/financials/loan_allocation.py index ffea06f..7e8307a 100644 --- a/bpfin/financials/loan_allocation.py +++ b/bpfin/financials/loan_allocation.py @@ -96,7 +96,7 @@ def loan_allocation( if is_loan_first is True: c_formula.append(max(c_formula) + 0.1) else: - c_formula.append(0) # !!! maybe, if loan_list is empty, put 1 here + c_formula.append(0) # 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) @@ -123,53 +123,42 @@ def loan_allocation( # x variables bounds, 0 <= x[i] <= loan[i]_max_amount bound_list = list((0, current_loan.max_amount) for current_loan in loan_list) - bound_list.append((0, downpayment_max)) + bound_list.append((0, min(cost, downpayment_max))) + + # print('\nLP obj_func =', c_formula) + # print('\nLP constraint matrix =', A_matrix, b_list) + # print('\nLP bounds =', bound_list) - print('\nLP obj_func =', c_formula) - print('\nLP constraint matrix =', A_matrix, b_list) - print('\nLP bounds =', bound_list) # LP calculation. x[i] == loan amount from loan[i] res = linprog(c=c_formula, A_ub=A_matrix, b_ub=b_list, bounds=bound_list, options={'disp': True}) total_ds = sumproduct(res.x, a_2) - # Calculate self-finance amount if loan_fist is True - # if is_loan_first: - # if first_year_saving is not None and req_dscr['req_saving_dscr'] and expected_payback is not None: - # # self finance amount is capped by extra_saving, which is Savings/req_dscr - total loan burden - # extra_saving = float("{0:.4f}".format((first_year_saving/req_dscr['req_saving_dscr'] - total_ds))) - # print('\n extra saving =', extra_saving) - # downpayment = (min(max([0, extra_saving]) * expected_payback / 12, downpayment_max)) - # else: - # downpayment = downpayment_max - # res.x[-1] = downpayment - print('\nLP cond 1 =', sumproduct(A_matrix[0], res.x)) - print('\nLP cond 2 =', sumproduct(A_matrix[1], res.x)) - print('\nLP cond 3 =', sumproduct(A_matrix[2], res.x)) + # print('\nLP cond 1 =', sumproduct(A_matrix[0], res.x)) + # print('\nLP cond 2 =', sumproduct(A_matrix[1], res.x)) + # print('\nLP cond 3 =', sumproduct(A_matrix[2], res.x)) if res.success is False: - return [0] * len(loan_list) + return [0] * (len(loan_list) + 1) return res.x # **** ugly test **** -# what if no loan_list and SF first? any SF > cost will work... -from bpfin.tests.testdata import feature_data as db -from bpfin.financials.loan import Loan_List -req_dscr = { - 'req_noi_dscr': 1.15, - 'req_cash_dscr': 1.05, - 'req_saving_dscr': 1.10 -} - -# test cases -print('\n Base Case:') -print(loan_allocation( - # loan_list=Loan_List(db.raw_loan_input_list).loan_list, - loan_list=None, - customer_preference=db.customer_preference, - cost=5800, - req_dscr=req_dscr, - first_year_saving=40000, - first_year_noi=18412, - first_year_cash=20000, - )) +# from bpfin.tests.testdata import feature_data as db +# from bpfin.financials.loan import Loan_List +# req_dscr = { +# 'req_noi_dscr': 1.15, +# 'req_cash_dscr': 1.05, +# 'req_saving_dscr': 1.10 +# } + +# # test cases +# print('\n Base Case:') +# print(loan_allocation( +# loan_list=Loan_List(db.raw_loan_input_list).loan_list, +# customer_preference=db.customer_preference, +# cost=db.cost_estimation, +# req_dscr=req_dscr, +# first_year_saving=40000, +# first_year_noi=18412, +# first_year_cash=20000, +# )) -- GitLab From 20227b023b0f8eab8c57d9b2d01eb098661b069d Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Wed, 28 Jun 2017 12:45:02 -0400 Subject: [PATCH 05/13] Add todo in description --- bpfin/financials/loan_allocation.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bpfin/financials/loan_allocation.py b/bpfin/financials/loan_allocation.py index 7e8307a..e4f7de6 100644 --- a/bpfin/financials/loan_allocation.py +++ b/bpfin/financials/loan_allocation.py @@ -65,9 +65,10 @@ def loan_allocation( 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 + To do: + write description to consider SF option + modify other functions because current return includes SF amount + put is_loan_first as an option in the function """ is_loan_first = False # !!!! current version assume customer prefer loan first. Flexibility will hould be built downpayment_max = customer_preference['downpayment_max'] -- GitLab From 7c8ef6b097a13cd1d8937afd7fa092a4a433c366 Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Wed, 28 Jun 2017 16:59:15 -0400 Subject: [PATCH 06/13] Update scenario, saving and back-end call to incorporate loan allocation method --- bpfin/back_end_call/back_end_prelim.py | 66 ++++++++++++++++++-------- bpfin/financials/financial_saving.py | 27 ++++++++++- bpfin/financials/loan.py | 9 +++- bpfin/financials/loan_allocation.py | 8 ++-- bpfin/financials/scenario.py | 59 ++++++++++++++++++----- bpfin/lib/other.py | 16 +++++++ bpfin/tests/testdata/feature_data.py | 2 +- 7 files changed, 150 insertions(+), 37 deletions(-) diff --git a/bpfin/back_end_call/back_end_prelim.py b/bpfin/back_end_call/back_end_prelim.py index 22efd7a..c7e53bd 100644 --- a/bpfin/back_end_call/back_end_prelim.py +++ b/bpfin/back_end_call/back_end_prelim.py @@ -4,9 +4,6 @@ from bpfin.financials.financial_income import Income_Statement_Table from bpfin.financials.financial_balance import Balance_Sheet_Table from bpfin.financials.scenario import Scenario from bpfin.back_end_call.back_end_inputs import monthly_bill, annual_bill -# delete following imports when online -# from bpfin.tests.testdata import feature_data as db -# import pprint def prelim_scenario(raw_bill_table, raw_annual_bill_table, @@ -64,10 +61,41 @@ def prelim_scenario(raw_bill_table, raw_annual_bill_table, # Define raw_bill_table as a "get" of prior_bill # Delete prior_annual_bill_table + # the following is front-end formatting building: + loan_showcase_list = [ + 'Lender', + 'Amount Borrowed', + 'Amount Upper Limit', + 'Annual Interest Rate', + 'Duration, months', + 'Monthly Debt Service'] + loan_item_list = [ + 'institute', + 'amount', + 'max_amount', + 'interest', + 'duration', + 'debt_service'] + loan_summary = [loan_showcase_list] + for current_loan_dict in scenario_ob.get_loan_summary(): + current_loan_summary = [] + for item in loan_item_list: + current_loan_summary.append(current_loan_dict[item]) + loan_summary.append(current_loan_summary) + + downpayment = scenario_ob.get_financing_plan()[-1] + + budget_plot = (scenario_ob.get_budget_plot()) + return scenario_ob.get_graph_dict(), scenario_ob.get_economics() + # return scenario_ob.get_graph_dict(), scenario_ob.get_economics( + # ), scenario_ob.get_annual_debt_service(), scenario_ob.get_loan_summary( + # ), loan_summary, downpayment, budget_plot # **** ugly test **** +from bpfin.tests.testdata import feature_data as db +import pprint # print(db.raw_bill_table) # print('\nmonthly_bill =', monthly_bill(db.raw_bill_table, db.analysis_date)) # print('\nannual_bill =', annual_bill(db.raw_bill_table, db.raw_annual_bill_table, db.analysis_date)) @@ -93,19 +121,19 @@ def prelim_scenario(raw_bill_table, raw_annual_bill_table, # f.write(writein) # f.close() -# print(prelim_scenario( -# raw_bill_table=db.raw_bill_table, -# raw_annual_bill_table=db.raw_annual_bill_table, -# raw_income_input=db.raw_income_input, -# growth_rate_flag=-2.0, -# raw_liability_input=db.raw_liability_input, -# raw_cash_balance=db.raw_cash_balance, -# raw_loan_input_list=db.raw_loan_input_list, -# analysis_date=db.analysis_date, -# commission_date=db.commission_date, -# construction_cost=db.cost_estimation, -# percent_saving_dict=db.percent_saving_dict, -# full_saving_dict=db.full_saving_dict, -# req_dscr=db.req_dscr, -# customer_preference=db.customer_preference -# )) +print(prelim_scenario( + raw_bill_table=db.raw_bill_table, + raw_annual_bill_table=db.raw_annual_bill_table, + raw_income_input=db.raw_income_input, + growth_rate_flag=-2.0, + raw_liability_input=db.raw_liability_input, + raw_cash_balance=db.raw_cash_balance, + raw_loan_input_list=db.raw_loan_input_list, + analysis_date=db.analysis_date, + commission_date=db.commission_date, + construction_cost=db.cost_estimation, + percent_saving_dict=db.percent_saving_dict, + full_saving_dict=db.full_saving_dict, + req_dscr=db.req_dscr, + customer_preference=db.customer_preference + )) diff --git a/bpfin/financials/financial_saving.py b/bpfin/financials/financial_saving.py index 82bd4f7..9cda01c 100644 --- a/bpfin/financials/financial_saving.py +++ b/bpfin/financials/financial_saving.py @@ -6,7 +6,7 @@ from bpfin.utilbills.bill_lib import form_year_calendar from bpfin.utilbills.bill_lib import form_bill_calendar from bpfin.utilbills.bill_post_proj_rough import bill_post_proj_rough from bpfin.tests.testdata import sample_data as db -from bpfin.lib.other import UTILITY_TYPE_LIST +from bpfin.lib.other import UTILITY_TYPE_LIST, add_year_dictionary class Saving(): @@ -374,6 +374,31 @@ class Saving_Overview(): saving_dict[year] = prior_charge_dict[year] - post_charge_dict[year] return saving_dict + def get_total_annual_saving_charge(self): + """ + Get the proforma annual saving on charge for all utility_type + Return: + dictionary: {year: saving on charge} + """ + savings_dict = {} + for utility_type in UTILITY_TYPE_LIST: + savings_dict = add_year_dictionary( + savings_dict, + copy.deepcopy(self.get_utility_annual_saving_charge(utility_type)) + ) + return savings_dict + + # total_saving_dict = {} + # for year in copy.deepcopy(self.prior_annual_bill_table['electricity']): + # total_saving_dict[year] = 0 + + # for utility in UTILITY_TYPE_LIST: + # current_saving_dict = self.get_utility_annual_saving_charge(utility) + # for year in total_saving_dict: + # total_saving_dict[year] += current_saving_dict[year] + + # return total_saving_dict + diff --git a/bpfin/financials/loan.py b/bpfin/financials/loan.py index e8c7046..3273712 100644 --- a/bpfin/financials/loan.py +++ b/bpfin/financials/loan.py @@ -20,6 +20,7 @@ class Loan(): 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): """ @@ -120,6 +121,7 @@ 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 + downpayment(float): self-finance amount calculated by loan_allocation """ def __init__(self, loan_input_list): """ @@ -127,6 +129,7 @@ class Loan_List(): Args: loan_input_list (list): list of dictionary of loan basic terms. """ + self.downpayment = 0 loan_input_list = ([] if not loan_input_list else loan_input_list) self.loan_list = [] for current_loan in loan_input_list: @@ -138,6 +141,7 @@ class Loan_List(): """ 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. + Assign value to self.downpayment, which is the result from loan_allocation Args: customer_preference (dictionary): dict of customer preference, { 'downpayment_max': float, upper limit of self finance amount, @@ -160,8 +164,9 @@ class Loan_List(): allocation = loan_allocation(current_loan_list, customer_preference, cost, req_dscr, first_year_saving, first_year_noi, first_year_cash) - for current_loan, current_amount in zip(current_loan_list, allocation): + for current_loan, current_amount in zip(current_loan_list, allocation[0]): current_loan.put_amount(current_amount) + self.downpayment = allocation[1] return current_loan_list def get_schedule(self, customer_preference, cost, req_dscr, @@ -200,6 +205,8 @@ class Loan_List(): def get_loan_list(self): return self.loan_list + def get_downpayment(self): + return self.downpayment # ****** ugly tests Loan and Loan_List class ********** # loan_list = Loan_List(db.loan_input_list) diff --git a/bpfin/financials/loan_allocation.py b/bpfin/financials/loan_allocation.py index e4f7de6..af7795a 100644 --- a/bpfin/financials/loan_allocation.py +++ b/bpfin/financials/loan_allocation.py @@ -70,7 +70,7 @@ def loan_allocation( modify other functions because current return includes SF amount put is_loan_first as an option in the function """ - is_loan_first = False # !!!! current version assume customer prefer loan first. Flexibility will hould be built + is_loan_first = True downpayment_max = customer_preference['downpayment_max'] expected_payback = customer_preference['expected_payback'] @@ -131,7 +131,7 @@ def loan_allocation( # print('\nLP bounds =', bound_list) # LP calculation. x[i] == loan amount from loan[i] - res = linprog(c=c_formula, A_ub=A_matrix, b_ub=b_list, bounds=bound_list, options={'disp': True}) + res = linprog(c=c_formula, A_ub=A_matrix, b_ub=b_list, bounds=bound_list, options={'disp': False}) total_ds = sumproduct(res.x, a_2) # print('\nLP cond 1 =', sumproduct(A_matrix[0], res.x)) @@ -139,8 +139,8 @@ def loan_allocation( # print('\nLP cond 3 =', sumproduct(A_matrix[2], res.x)) if res.success is False: - return [0] * (len(loan_list) + 1) - return res.x + return [0] * len(loan_list), 0 + return list((res.x[loanid] for loanid in (range(len(res.x) - 1)))), res.x[-1] # **** ugly test **** diff --git a/bpfin/financials/scenario.py b/bpfin/financials/scenario.py index 3e4405b..ccdc195 100644 --- a/bpfin/financials/scenario.py +++ b/bpfin/financials/scenario.py @@ -3,15 +3,7 @@ from bpfin.financials.financial_saving import Saving_Overview from bpfin.financials.loan import Loan_List from bpfin.lib.other import add_year_dictionary, divide_dscr_dict, min_none_list from bpfin.utilbills.bill_lib import form_bill_year, annualizing_projection -from bpfin.lib.other import UTILITY_TYPE_LIST -# may delete from here after sample built -# from bpfin.tests.testdata import feature_data as db -# from bpfin.tests.testdata import sample_data as sdb -# from bpfin.lib.back_end_call import monthly_bill -# from bpfin.financials.liability import final_liability_dict -# from bpfin.financials.balance_sheet_projection import balance_sheet_projection -# from bpfin.financials.financial_income import Income_Statement_Table -# from bpfin.financials.financial_balance import Balance_Sheet_Table +from bpfin.lib.other import UTILITY_TYPE_LIST, set_dict_starting_year class Scenario(): @@ -35,6 +27,7 @@ class Scenario(): loan_list (object): Loan_List object, used to store loan options, and result of financing planning saving_overview (object): Saving_Overview object, calculate and store scenario based saving results scheduled_loan_list (list): list of Loan objects. Store financing plans for each loan option + downpayment(float): self-finance amount calculated by loan_allocation """ @@ -88,6 +81,7 @@ class Scenario(): analysis_date=copy.deepcopy(analysis_date), commissioning_date=copy.deepcopy(commission_date)) self.scheduled_loan_list = None + self.downpayment = 0 def prelim_analysis(self, prior_month_bill, percent_saving_dict, full_saving_dict, growth_rate_flag, req_dscr, @@ -148,7 +142,7 @@ class Scenario(): self.analysis_date, self.other_debt_service, post_saving_noi_dict) self.post_balance_sheet_table = post_balance_sheet_table - # allocate loan and get loan schedules + # generate inputs for loan allocate allocation first_year_saving = copy.deepcopy(self.saving_overview.get_total_first_year_saving()) first_year_noi = copy.deepcopy( self.post_income_statement_table.get_first_year_noi( @@ -167,6 +161,7 @@ class Scenario(): first_year_noi=first_year_noi, first_year_cash=first_year_cash) self.scheduled_loan_list = scheduled_loan_list + self.downpayment = self.loan_list.get_downpayment() def get_post_energy_bill(self): """ @@ -246,7 +241,9 @@ class Scenario(): """ noi_dict = copy.deepcopy(self.post_income_statement_table.get_noi_dict()) cash_dict = copy.deepcopy(self.post_balance_sheet_table.get_cash_dict()) - debt_dict = copy.deepcopy(self.get_annual_debt_service()) + debt_dict = set_dict_starting_year( + copy.deepcopy(self.get_annual_debt_service()), + self.commission_date.year + 1) savings_dict = {} for utility_type in UTILITY_TYPE_LIST: @@ -286,8 +283,48 @@ class Scenario(): economics_overview['min_cash_dscr'] = min_none_list(dscr_dict['cash_dscr'].values()) return economics_overview + def get_financing_plan(self): + """ + Get Financing Plan for the scenario, showcase amount borrowed from each lender including self-finance + Return: + list: list of float. elements [0] through [-2] are amount from lenders, [-1] is self-finance + To Do: + figure out if we need this function + """ + financing_plan = [] + for current_loan in copy.deepcopy(self.scheduled_loan_list): + financing_plan.append(current_loan.amount) + financing_plan.append(copy.deepcopy(self.downpayment)) + return financing_plan + + def get_loan_summary(self): + """ + Get allocated loan summary, including their institution names, all kinds of terms, and amount borrowed + Return: + list: list of dictionary. each element is a loan with its keys as line itmes + """ + loan_summary = [] + for current_loan in copy.deepcopy(self.scheduled_loan_list): + loan_summary.append(current_loan.get_loan_terms()) + return loan_summary + + def get_budget_plot(self): + """ + Get the budget plot (saving in %, cost) + Return: + tuple: (float, float) + """ + return (self.saving_overview.get_total_saving_percent(), self.construction_cost) + # # ***** ugly test that can be a guide for front end dev ***** +# from bpfin.tests.testdata import feature_data as db +# from bpfin.lib.back_end_call import monthly_bill +# from bpfin.financials.liability import final_liability_dict +# from bpfin.financials.balance_sheet_projection import balance_sheet_projection +# from bpfin.financials.financial_income import Income_Statement_Table +# from bpfin.financials.financial_balance import Balance_Sheet_Table + # growth_toggle = 0.01 # prior_IS_table = Income_Statement_Table( # db.raw_income_input, diff --git a/bpfin/lib/other.py b/bpfin/lib/other.py index a643c73..3730e6a 100644 --- a/bpfin/lib/other.py +++ b/bpfin/lib/other.py @@ -1,6 +1,7 @@ import datetime import calendar import numpy as np +import copy from scipy.optimize import linprog @@ -26,6 +27,21 @@ def divide_dscr_dict(dict_1, dict_2): return ratio_dict +def set_dict_starting_year(dict_1, start_year): + """ + Force the values before start_year to be 0 + + Args: + dict_1(dictionary): keys are years, values are floats + start_year(int): the first, in this year, value is NOT forced to 0 + """ + current_dict = copy.deepcopy(dict_1) + for year in sorted(current_dict): + if year < start_year: + current_dict[year] = 0 + return current_dict + + def min_none_list(list_1): new_list = [] for element in list_1: diff --git a/bpfin/tests/testdata/feature_data.py b/bpfin/tests/testdata/feature_data.py index 894e6bf..af2781b 100644 --- a/bpfin/tests/testdata/feature_data.py +++ b/bpfin/tests/testdata/feature_data.py @@ -18,7 +18,7 @@ analysis_date = { # Engineering Scenarios and project economics -cost_estimation = 150000.00 +cost_estimation = 150000.00 # 190000.00 makes self-finance needed commission_date = date(2017, 3, 14) -- GitLab From 362744017e9b89e84ee008332de91cb1293e8afe Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Wed, 28 Jun 2017 17:20:32 -0400 Subject: [PATCH 07/13] update feature data for back_end_budget --- bpfin/back_end_call/back_end_budget.py | 8 ++-- bpfin/tests/testdata/feature_data.py | 62 ++++++++++---------------- 2 files changed, 28 insertions(+), 42 deletions(-) diff --git a/bpfin/back_end_call/back_end_budget.py b/bpfin/back_end_call/back_end_budget.py index 3feaeba..4b6aeb8 100644 --- a/bpfin/back_end_call/back_end_budget.py +++ b/bpfin/back_end_call/back_end_budget.py @@ -154,7 +154,7 @@ finalresult = budget_simulation( db.raw_bill_table, db.raw_annual_bill_table, db.raw_income_input, - -2.0, + db.growth_rate_flag, db.raw_liability_input, db.raw_cash_balance, db.raw_loan_input_list @@ -168,6 +168,6 @@ finalresult = budget_simulation( # if no raw_income, noi returns None, it's done. # but if no raw_cash, cash returns 0, which is not exactly what we expect -# import pprint -# pp = pprint.PrettyPrinter(width=120, indent=4, compact=True) -# pp.pprint(finalresult) +import pprint +pp = pprint.PrettyPrinter(width=120, indent=4, compact=True) +pp.pprint(finalresult) diff --git a/bpfin/tests/testdata/feature_data.py b/bpfin/tests/testdata/feature_data.py index af2781b..1ec4290 100644 --- a/bpfin/tests/testdata/feature_data.py +++ b/bpfin/tests/testdata/feature_data.py @@ -710,44 +710,30 @@ historical_cagr = 0.01242283656582921 growth_rate_flag = -2.0 budget_simulation_result = [ - [['saving_percentage', 0.1, 0.2, 0.30000000000000004, 0.4, 0.5], [ - 'Budget', 37723.783022506694, 68031.962268808478, 68031.962268808478, - 68031.962268808478, 68031.962268808478 - ], ['NYSERDA', 0.0, 0.0, 0.0, 0.0, 0.0], - ['Joe Fund', 37723.783022506694, 50000.0, 50000.0, 50000.0, 50000.0], [ - 'Tooraj Capital', 0.0, 18031.962268808486, 18031.962268808486, - 18031.962268808486, 18031.962268808486 - ], ['Self Finance', 0.0, 0.0, 0.0, 0.0, 0.0]], - [['saving_percentage', 0.1, 0.2, 0.30000000000000004, 0.4, 0.5], [ - 'Budget', 37723.783022506701, 77213.635473036207, 118031.96226880848, - 118031.96226880848, 118031.96226880848 - ], ['NYSERDA', 0.0, 0.0, 0.0, 0.0, 0.0], - ['Joe Fund', 37723.783022506694, 50000.0, 50000.0, 50000.0, 50000.0], [ - 'Tooraj Capital', 0.0, 18031.962268808486, 18031.962268808486, - 18031.962268808486, 18031.962268808486 - ], [ - 'Self Finance', 9.0949470177292824e-12, 9181.6732042277363, 50000.0, - 50000.0, 50000.0 - ]], - [['saving_percentage', 0.1, 0.2, 0.30000000000000004, 0.4, 0.5], [ - 'Budget', 52184.67464428641, 89908.457666793111, 118031.96226880848, - 118031.96226880848, 118031.96226880848 - ], ['NYSERDA', 0.0, 0.0, 0.0, 0.0, 0.0], [ - 'Joe Fund', 2184.6746442864069, 39908.457666793111, 50000.0, 50000.0, - 50000.0 - ], [ - 'Tooraj Capital', 0.0, 0.0, 18031.962268808486, 18031.962268808486, - 18031.962268808486 - ], ['Self Finance', 50000.0, 50000.0, 50000.0, 50000.0, 50000.0]], - [['saving_percentage', 0.1, 0.2, 0.30000000000000004, 0.4, 0.5], [ - 'Budget', 87723.783022506686, 118031.96226880848, 118031.96226880848, - 118031.96226880848, 118031.96226880848 - ], ['NYSERDA', 0.0, 0.0, 0.0, 0.0, - 0.0], - ['Joe Fund', 37723.783022506694, 50000.0, 50000.0, 50000.0, 50000.0], [ - 'Tooraj Capital', 0.0, 18031.962268808486, 18031.962268808486, - 18031.962268808486, 18031.962268808486 - ], ['Self Finance', 50000.0, 50000.0, 50000.0, 50000.0, 50000.0]] + [['saving_percentage', 0.1, 0.2, 0.30000000000000004, 0.4, 0.5], + ['Budget', 37723.783022506694, 68031.962268808478, 68031.962268808478, 68031.962268808478, 68031.962268808478], + ['NYSERDA', 0.0, 0.0, 0.0, 0.0, 0.0], + ['Joe Fund', 37723.783022506694, 50000.0, 50000.0, 50000.0, 50000.0], + ['Tooraj Capital', 0.0, 18031.962268808486, 18031.962268808486, 18031.962268808486, 18031.962268808486], + ['Self Finance', 0.0, 0.0, 0.0, 0.0, 0.0]], + [['saving_percentage', 0.1, 0.2, 0.30000000000000004, 0.4, 0.5], + ['Budget', 37723.783022506694, 77213.635268808473, 118031.96226880848, 118031.96226880848, 118031.96226880848], + ['NYSERDA', 0.0, 0.0, 0.0, 0.0, 0.0], + ['Joe Fund', 37723.783022506694, 50000.0, 50000.0, 50000.0, 50000.0], + ['Tooraj Capital', 0.0, 18031.962268808486, 18031.962268808486, 18031.962268808486, 18031.962268808486], + ['Self Finance', 0.0, 9181.6730000000007, 50000.0, 50000.0, 50000.0]], + [['saving_percentage', 0.1, 0.2, 0.30000000000000004, 0.4, 0.5], + ['Budget', 52184.67464428641, 89908.457666793111, 118031.96226880848, 118031.96226880848, 118031.96226880848], + ['NYSERDA', 0.0, 0.0, 0.0, 0.0, 0.0], + ['Joe Fund', 2184.6746442864069, 39908.457666793111, 50000.0, 50000.0, 50000.0], + ['Tooraj Capital', 0.0, 0.0, 18031.962268808486, 18031.962268808486, 18031.962268808486], + ['Self Finance', 50000.0, 50000.0, 50000.0, 50000.0, 50000.0]], + [['saving_percentage', 0.1, 0.2, 0.30000000000000004, 0.4, 0.5], + ['Budget', 87723.783022506686, 118031.96226880848, 118031.96226880848, 118031.96226880848, 118031.96226880848], + ['NYSERDA', 0.0, 0.0, 0.0, 0.0, 0.0], + ['Joe Fund', 37723.783022506694, 50000.0, 50000.0, 50000.0, 50000.0], + ['Tooraj Capital', 0.0, 18031.962268808486, 18031.962268808486, 18031.962268808486, 18031.962268808486], + ['Self Finance', 50000.0, 50000.0, 50000.0, 50000.0, 50000.0]] ] post_annual_bill = { -- GitLab From 1eaba4f062ffbda1bdf223e7c975ca7cc6455723 Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Wed, 28 Jun 2017 18:36:30 -0400 Subject: [PATCH 08/13] Write description and clean up code in loan_allocation --- bpfin/financials/loan_allocation.py | 53 ++++++++++++++--------------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/bpfin/financials/loan_allocation.py b/bpfin/financials/loan_allocation.py index af7795a..fad950d 100644 --- a/bpfin/financials/loan_allocation.py +++ b/bpfin/financials/loan_allocation.py @@ -19,32 +19,37 @@ def loan_allocation( **** 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 + 1. object function, determine minimum annual total DS. + x_1/payback_1 + x_2/payback_2 + x_3/payback_3 + ... + x_n/payback_n = min + * x_i is loan amount, x_n is self-finance amount + * if loan_first: pretend SF more expensive than loans, payback_n < min(payback_i) + * if self-finance first: pretent SF cheaper than loans, payback_n -> infinity 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 + sum(x_i / payback_i) <= saving/dscr + * sum of annual debt service and self-finance payback <= saving/dscr + sum(x_i / payback_i) - (x_n / payback_n) <= noi/dscr + * sum of loan debt service <= noi constraint, making project financially feasibile + sum(x_i / payback_i) - (x_n / payback_n) <= cash/dscr + * sum of loan debt service <= cash constraint, making project financially feasibile + sum(x_i) >= project_cost + * ideally sum(x) == project cost, but it makes no difference because object func chases minimum 3. x variables bounds - 0 <= x[i] <= loan[i]_max_amount + 0 <= x_i <= loan[i]_max_amount + 0 <= x_n <= min(cost, downpayment_max) + * this is to ensure that when no loan_list input and when SF_first, x_n > cost dosen't work Args: loan_list (list): list of objectives of loans customer_preference (dictionary): customer preference for financing plan - req_dscr(dictionary): required dscr cost (float): construction cost, can be estimated or quoted + req_dscr(dictionary): required dscr 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 - # scenario (dictionary): package of energy conservation measures (ECMs) + Return: list: list of loan amount that borrowed from each lender + float: amount borrowed from self-finance (downpayment amount) Description: customer_preference (dictionary): dict of customer preference, { @@ -57,17 +62,12 @@ def loan_allocation( 'req_cash_dscr': 1.15, 'req_saving_dscr': 1.10 } - # 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 - # } + Note: noi == net operating income dscr == debt service coverage ratio To do: - write description to consider SF option - modify other functions because current return includes SF amount put is_loan_first as an option in the function """ is_loan_first = True @@ -91,16 +91,13 @@ def loan_allocation( # linear programming (LP) # Set LP constrains. # objective function: x1/payback1 + x2/payback2 + x3/payback3 + sf/adjust_payback = minimum - # if loan_first: pretend SF more expensive than loans, adjust_payback < min(loan paybacks) - # if self-finance first: pretent SF cheaper than loans, adjust_payback -> infinity c_formula = list(1 / current_loan.payback for current_loan in loan_list) if is_loan_first is True: c_formula.append(max(c_formula) + 0.1) else: c_formula.append(0) - # 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) + # x variables constraints, A_matrix * [x_i] <= b_list A_matrix = [] a_0 = list(1 / current_loan.payback for current_loan in loan_list) a_1 = copy.deepcopy(a_0) @@ -109,7 +106,7 @@ def loan_allocation( a_1.append(1 / expected_payback * 12 if expected_payback else 0) a_2.append(0) a_3.append(0) - a_4 = ([-1] * (len(loan_list) + 1)) # -x1 -x2 -x3 -sf <= -max + a_4 = ([-1] * (len(loan_list) + 1)) # -x1 -x2 -x3 -sf <= -cost A_matrix.append(multiply_list(a_1, (req_dscr['req_saving_dscr'] if first_year_saving is not None else 0))) A_matrix.append(multiply_list(a_2, (req_dscr['req_noi_dscr'] if first_year_noi is not None else 0))) @@ -122,7 +119,7 @@ def loan_allocation( b_list.append(first_year_cash if first_year_cash else 0) b_list.append(0 - cost) - # x variables bounds, 0 <= x[i] <= loan[i]_max_amount + # x variables bounds bound_list = list((0, current_loan.max_amount) for current_loan in loan_list) bound_list.append((0, min(cost, downpayment_max))) @@ -132,7 +129,7 @@ def loan_allocation( # LP calculation. x[i] == loan amount from loan[i] res = linprog(c=c_formula, A_ub=A_matrix, b_ub=b_list, bounds=bound_list, options={'disp': False}) - total_ds = sumproduct(res.x, a_2) + # total_ds = sumproduct(res.x, a_2) # total_ds = sum(debt service) # print('\nLP cond 1 =', sumproduct(A_matrix[0], res.x)) # print('\nLP cond 2 =', sumproduct(A_matrix[1], res.x)) @@ -140,7 +137,7 @@ def loan_allocation( if res.success is False: return [0] * len(loan_list), 0 - return list((res.x[loanid] for loanid in (range(len(res.x) - 1)))), res.x[-1] + return list(res.x[loanid] for loanid in range(len(res.x) - 1)), res.x[-1] # **** ugly test **** -- GitLab From e25a31c31ca850aee2b437e2db99b78680909faf Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Wed, 28 Jun 2017 18:42:46 -0400 Subject: [PATCH 09/13] Clean back_end_budget code, adjust To Do lists in some functions --- bpfin/back_end_call/back_end_budget.py | 38 ++++++++------- bpfin/back_end_call/back_end_prelim.py | 64 ++++++++++---------------- bpfin/financials/financial_saving.py | 3 +- 3 files changed, 45 insertions(+), 60 deletions(-) diff --git a/bpfin/back_end_call/back_end_budget.py b/bpfin/back_end_call/back_end_budget.py index 4b6aeb8..d41d7dc 100644 --- a/bpfin/back_end_call/back_end_budget.py +++ b/bpfin/back_end_call/back_end_budget.py @@ -51,9 +51,7 @@ def budget_simulation( 'water': None} To Do: - Need to refactor when merge from saving-scenario refactoring - Need to validate data, to allow a lot of empty inputs - Write test file + finish description saving_interval will be read from frond-end in next version """ # data validation and empty conversion: @@ -145,20 +143,20 @@ def budget_simulation( # **** ugly test **** -from bpfin.tests.testdata import feature_data as db -finalresult = budget_simulation( - db.analysis_date, - db.commission_date, - db.customer_preference, - db.req_dscr, - db.raw_bill_table, - db.raw_annual_bill_table, - db.raw_income_input, - db.growth_rate_flag, - db.raw_liability_input, - db.raw_cash_balance, - db.raw_loan_input_list - ) +# from bpfin.tests.testdata import feature_data as db +# finalresult = budget_simulation( +# db.analysis_date, +# db.commission_date, +# db.customer_preference, +# db.req_dscr, +# db.raw_bill_table, +# db.raw_annual_bill_table, +# db.raw_income_input, +# db.growth_rate_flag, +# db.raw_liability_input, +# db.raw_cash_balance, +# db.raw_loan_input_list +# ) # if energy bill is 0, first_year_noi will be lower than the case with energy bill, because no saving # to confirm this hypothesis, ran income and compared noi for energy bill not 0 vs 0 @@ -168,6 +166,6 @@ finalresult = budget_simulation( # if no raw_income, noi returns None, it's done. # but if no raw_cash, cash returns 0, which is not exactly what we expect -import pprint -pp = pprint.PrettyPrinter(width=120, indent=4, compact=True) -pp.pprint(finalresult) +# import pprint +# pp = pprint.PrettyPrinter(width=120, indent=4, compact=True) +# pp.pprint(finalresult) diff --git a/bpfin/back_end_call/back_end_prelim.py b/bpfin/back_end_call/back_end_prelim.py index c7e53bd..516fa27 100644 --- a/bpfin/back_end_call/back_end_prelim.py +++ b/bpfin/back_end_call/back_end_prelim.py @@ -25,11 +25,12 @@ def prelim_scenario(raw_bill_table, raw_annual_bill_table, cash_balance_dict = cash_balance(analysis_date, raw_cash_balance) liability_dict = final_liability_dict(analysis_date, raw_liability_input) - balance_sheet_input = { - 'cash_dictionary': cash_balance_dict, - 'other_debt_service_dictionary': liability_dict, - 'net_income_dictionary': prior_income_table.get_noi_dict() - } + # this input might not be needed anymore because Balance_Sheet_Table got updated, right? + # balance_sheet_input = { + # 'cash_dictionary': cash_balance_dict, + # 'other_debt_service_dictionary': liability_dict, + # 'net_income_dictionary': prior_income_table.get_noi_dict() + # } prior_income_table.project(growth_rate_flag, prior_annual_bill_table) @@ -94,46 +95,31 @@ def prelim_scenario(raw_bill_table, raw_annual_bill_table, # **** ugly test **** -from bpfin.tests.testdata import feature_data as db -import pprint -# print(db.raw_bill_table) -# print('\nmonthly_bill =', monthly_bill(db.raw_bill_table, db.analysis_date)) -# print('\nannual_bill =', annual_bill(db.raw_bill_table, db.raw_annual_bill_table, db.analysis_date)) -# prior_income_statement_result = prior_income_statement( -# print('\nprior_income_statement =', prior_income_statement_table( -# db.raw_income_input, -# db.raw_bill_table, -# db.raw_annual_bill_table, -# db.analysis_date, -# -2.0) +# from bpfin.tests.testdata import feature_data as db +# import pprint + +# print(prelim_scenario( +# raw_bill_table=db.raw_bill_table, +# raw_annual_bill_table=db.raw_annual_bill_table, +# raw_income_input=db.raw_income_input, +# growth_rate_flag=-2.0, +# raw_liability_input=db.raw_liability_input, +# raw_cash_balance=db.raw_cash_balance, +# raw_loan_input_list=db.raw_loan_input_list, +# analysis_date=db.analysis_date, +# commission_date=db.commission_date, +# construction_cost=db.cost_estimation, +# percent_saving_dict=db.percent_saving_dict, +# full_saving_dict=db.full_saving_dict, +# req_dscr=db.req_dscr, +# customer_preference=db.customer_preference +# )) # pp = pprint.PrettyPrinter(width=120, indent=4, compact=True) # pp.pprint(prior_income_statement_result) -# result = prior_income_statement_table( -# db.raw_income_input, -# db.raw_bill_table, -# db.raw_annual_bill_table, -# db.analysis_date, -# -2.0) # writein = str(result) # f = open('data_generation.py', 'w') # f.write(writein) # f.close() -print(prelim_scenario( - raw_bill_table=db.raw_bill_table, - raw_annual_bill_table=db.raw_annual_bill_table, - raw_income_input=db.raw_income_input, - growth_rate_flag=-2.0, - raw_liability_input=db.raw_liability_input, - raw_cash_balance=db.raw_cash_balance, - raw_loan_input_list=db.raw_loan_input_list, - analysis_date=db.analysis_date, - commission_date=db.commission_date, - construction_cost=db.cost_estimation, - percent_saving_dict=db.percent_saving_dict, - full_saving_dict=db.full_saving_dict, - req_dscr=db.req_dscr, - customer_preference=db.customer_preference - )) diff --git a/bpfin/financials/financial_saving.py b/bpfin/financials/financial_saving.py index 9cda01c..d88f62b 100644 --- a/bpfin/financials/financial_saving.py +++ b/bpfin/financials/financial_saving.py @@ -5,7 +5,6 @@ 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.utilbills.bill_post_proj_rough import bill_post_proj_rough -from bpfin.tests.testdata import sample_data as db from bpfin.lib.other import UTILITY_TYPE_LIST, add_year_dictionary @@ -404,6 +403,8 @@ class Saving_Overview(): # **** ugly test **** +# from bpfin.tests.testdata import sample_data as db + # so = Saving_Overview(db.bill_overview, db.bill_overview_organized, db.analysis_date, datetime.date(2017, 3, 14)) # so.put_saving(db.prior_month_bill, db.percent_saving_dict, db.full_saving_dict) -- GitLab From b5e5b027d542f99907ee9f495aa27c902b741eaa Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Thu, 29 Jun 2017 11:01:39 -0400 Subject: [PATCH 10/13] Add description for back_end_prelim --- bpfin/back_end_call/back_end_prelim.py | 74 +++++++++++++++++++++++++- bpfin/financials/liability.py | 8 ++- bpfin/financials/loan_allocation.py | 4 +- 3 files changed, 82 insertions(+), 4 deletions(-) diff --git a/bpfin/back_end_call/back_end_prelim.py b/bpfin/back_end_call/back_end_prelim.py index 516fa27..7228e47 100644 --- a/bpfin/back_end_call/back_end_prelim.py +++ b/bpfin/back_end_call/back_end_prelim.py @@ -14,7 +14,79 @@ def prelim_scenario(raw_bill_table, raw_annual_bill_table, percent_saving_dict, full_saving_dict, req_dscr, customer_preference): """ - To Do: write unit test file for this scenario and for prelim_scenario back_end_call + Conduct Preliminary Financial Analysis + + Args: + raw_bill_table (dictionary): dict of dict of raw_bill. Keys are utility types + raw_annual_bill_table (dictionary): dictionary of dictionary of annual bills, at lease 0 year data is required + raw_income_input (dictionary): dict of dict of incomplete income statement, for historical years. Key = year + growth_rate_flag (float): indicating assumed growth rate, -2.0 == cagr, -1.0 == historical average + raw_liability_input (dictionary): dict of existing debts. Key is debt ID, value see description + raw_cash_balance (dictionary): dict of cash balance. Key is date, value is tuple, see description + raw_loan_input_list (list): list of dictionary of loan basic terms, see description + analysis_date (dictionary): proforma's starting date and the years of proforma + commission_date (date): date(year, month, date), when construction work complete and saving starts in next month + construction_cost (float): estimated construction coost + percent_saving_dict (dictionary): dict of float, percentage saving for 4 utility types, keys are utility types + full_saving_dict (dictionary): dict of undefined saving, from engineering saving analysis. can be None + req_dscr (dictionary): dict of required debt service coverage ratios, see description + customer_preference (dictionary): customer preference for financing plan + + Returns: + project_economics_table + loan_financing_plan + self-finance_downpayment + annual_financial_projection_table + monthly_financial_projection_table + graphic_dictionary + + + Description: + raw_income_input = {2014: {'revenue': 90.0, 'utility_expense': 55.0, 'non_utility_expense': 35.0}, 2015:{},} + raw_bill_table = { + 'electricity': { + 'date_from': list of date, + 'date_to', list of date, + 'usage', list of float, + 'charge', list of float + }, + 'gas': None, + 'oil': None, + 'water': None + } + raw_annual_bill_table = { + 'electricity': None + 'gas': {2014: 100, 2015:200, ...}, + 'oil': {2014: 100, 2015:200, ...}, + 'water': dict of water_bill + } + raw_liability_input = { + 'debt1': (monthly debt service, lender name, remaining month, record date), + 'debt2': (float, string, int, date(year, month, date)) + } + raw_cash_balance = {date(year, month, date):(cash value, year)} + raw_loan_input_list = [{'institute': str, 'max_amount': float, 'interest': float, 'duration': int}, {}] + percent_saving_dict = { + 'electricity': 0.25, + 'gas': 0.10, + 'oil': 0.80, + 'water': 0.0 + } + req_dscr (dictionary): { + 'req_noi_dscr': 1.15, + 'req_cash_dscr': 1.15, + 'req_saving_dscr': 1.10 + } + 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 + } + + + To Do: + determine proper returns to be displayed on front-end + write unit test file for this scenario and for prelim_scenario back_end_call """ prior_annual_bill_table, manual_input_dict = annual_bill( raw_bill_table, raw_annual_bill_table, analysis_date diff --git a/bpfin/financials/liability.py b/bpfin/financials/liability.py index 53a36f1..0295991 100644 --- a/bpfin/financials/liability.py +++ b/bpfin/financials/liability.py @@ -5,9 +5,15 @@ from datetime import date def define_date_start(liability_dictionary): """Return latest date from dictionary of debts with associated dates. Args: - liability_dictionary (dictionary): dictionary of debts + liability_dictionary (dictionary): dict of existing debts. Key is debt ID, value as description Return: date: max date + Description: + liability_dictionary = { + 'debt1': (monthly debt service, lender name, remaining month, record date), + 'debt2': (float, string, int, date(year, month, date)) + } + """ date_list = [] for key, value in liability_dictionary.items(): diff --git a/bpfin/financials/loan_allocation.py b/bpfin/financials/loan_allocation.py index fad950d..6b4521d 100644 --- a/bpfin/financials/loan_allocation.py +++ b/bpfin/financials/loan_allocation.py @@ -42,7 +42,7 @@ def loan_allocation( loan_list (list): list of objectives of loans customer_preference (dictionary): customer preference for financing plan cost (float): construction cost, can be estimated or quoted - req_dscr(dictionary): required dscr + req_dscr(dictionary): dict of required debt service coverage ratios, see description 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 @@ -57,7 +57,7 @@ def loan_allocation( 'expected_payback': int, months that customer expect the project can payback, 'cust_saving_dscr': float, customer required lower limit of (saving / debt service) ratio } - req_dscr (dictionary): dict of required debt service coverage ratios, { + req_dscr = { 'req_noi_dscr': 1.15, 'req_cash_dscr': 1.15, 'req_saving_dscr': 1.10 -- GitLab From 32a3c7cdc16231085db88f97976a408cefc928c6 Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Thu, 29 Jun 2017 12:26:11 -0400 Subject: [PATCH 11/13] Write test file for loan allocation --- bpfin/financials/loan_allocation.py | 13 +++++++------ .../test_financials/test_loan_allocation.py | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 bpfin/tests/test_financials/test_loan_allocation.py diff --git a/bpfin/financials/loan_allocation.py b/bpfin/financials/loan_allocation.py index 6b4521d..f563140 100644 --- a/bpfin/financials/loan_allocation.py +++ b/bpfin/financials/loan_allocation.py @@ -69,6 +69,7 @@ def loan_allocation( To do: put is_loan_first as an option in the function + write multiple unit tests for different situation """ is_loan_first = True downpayment_max = customer_preference['downpayment_max'] @@ -143,11 +144,11 @@ def loan_allocation( # **** ugly test **** # from bpfin.tests.testdata import feature_data as db # from bpfin.financials.loan import Loan_List -# req_dscr = { -# 'req_noi_dscr': 1.15, -# 'req_cash_dscr': 1.05, -# 'req_saving_dscr': 1.10 -# } +# # req_dscr = { +# # 'req_noi_dscr': 1.15, +# # 'req_cash_dscr': 1.05, +# # 'req_saving_dscr': 1.10 +# # } # # test cases # print('\n Base Case:') @@ -155,7 +156,7 @@ def loan_allocation( # loan_list=Loan_List(db.raw_loan_input_list).loan_list, # customer_preference=db.customer_preference, # cost=db.cost_estimation, -# req_dscr=req_dscr, +# req_dscr=db.req_dscr, # first_year_saving=40000, # first_year_noi=18412, # first_year_cash=20000, diff --git a/bpfin/tests/test_financials/test_loan_allocation.py b/bpfin/tests/test_financials/test_loan_allocation.py new file mode 100644 index 0000000..1e664bf --- /dev/null +++ b/bpfin/tests/test_financials/test_loan_allocation.py @@ -0,0 +1,17 @@ +from bpfin.tests.testdata import feature_data as db +from bpfin.financials.loan import Loan_List +from bpfin.financials.loan_allocation import loan_allocation + + +def test_loan_allocation(): + result = loan_allocation( + loan_list=Loan_List(db.raw_loan_input_list).loan_list, + customer_preference=db.customer_preference, + cost=db.cost_estimation, + req_dscr=db.req_dscr, + first_year_saving=40000, + first_year_noi=18412, + first_year_cash=20000) + + expected_outcome = ([0.0, 50000.0, 60800.314080022246], 39199.685919977754) + assert result == expected_outcome -- GitLab From d1bc9a24e8d1e71f758d6b39b0fadd6f1b73e59c Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Thu, 29 Jun 2017 15:48:44 -0400 Subject: [PATCH 12/13] Figure out the proper current return for back_end_prelim --- bpfin/back_end_call/back_end_prelim.py | 58 +++++++++++++++++++------- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/bpfin/back_end_call/back_end_prelim.py b/bpfin/back_end_call/back_end_prelim.py index 7228e47..f6d09b5 100644 --- a/bpfin/back_end_call/back_end_prelim.py +++ b/bpfin/back_end_call/back_end_prelim.py @@ -33,13 +33,17 @@ def prelim_scenario(raw_bill_table, raw_annual_bill_table, customer_preference (dictionary): customer preference for financing plan Returns: - project_economics_table - loan_financing_plan - self-finance_downpayment - annual_financial_projection_table - monthly_financial_projection_table - graphic_dictionary - + scenario_ob.get_economics(), + loan_summary, + downpayment, + scenario_ob.get_graph_dict(), + budget_plot + + project_economics (dictionary): overview of project economics of this scenario, see description + loan_summary (list): list of list, showing loan financed financing plan. see description + downpayment (float): self-finance_downpayment amount + graphic_dictionary (dictionary): dict of dict, showcase energy saving, energy debt service, net savings, see description + scenario_plot (tuple): (saving%, cost), to draw a dot of scenario in budget simulator graph Description: raw_income_input = {2014: {'revenue': 90.0, 'utility_expense': 55.0, 'non_utility_expense': 35.0}, 2015:{},} @@ -83,10 +87,32 @@ def prelim_scenario(raw_bill_table, raw_annual_bill_table, 'cust_saving_dscr': float, customer required lower limit of (saving / debt service) ratio } + project_economics = { + 'estimated_cost': 150000.0, + 'overall_saving': 0.46592312148565274, + 'first_year_saving': 28667.507248011731, + 'simple_payback': 5.2324047117980035, + 'min_saving_dscr': 1.2340762713995275, + 'min_noi_dscr': 1.7556178946382468, + 'min_cash_dscr': 5.6110894382891772 + } + loan_financing_plan = [ + ['Lender', 'Amount Borrowed', 'Amount Upper Limit', 'Annual Interest Rate', 'Duration, months', 'Monthly Debt Service'], + ['NYSERDA', 25000.0, 500000, 0.08, 120, 303.31898588839437], + ['Joe Fund', 50000.0, 50000, 0.05, 108, 575.86365841823613], + ['Tooraj Capital', 75000.0, 75000, 0.07, 114, 902.56078079882502] + ] + graph_dictionary = { + 'energy_expenses': {2012: 55002.38772098512, 2013: 55616.154107144292, ...}, + 'total_loan': {2012: 0, 2013: 0, 2014: 0, 2015: 0, 2016: 0, 2017: 17817.434251054554, ...}, + 'net_savings': {2014: 0.0, 2015: 0.0, 2016: 0.0, 2017: 1399.2525568434357, ...} + } - To Do: - determine proper returns to be displayed on front-end - write unit test file for this scenario and for prelim_scenario back_end_call + TO DO: + add return of: + annual_financial_projection_table + monthly_financial_projection_table + write unit test file for scenario class and for prelim_scenario back_end_call """ prior_annual_bill_table, manual_input_dict = annual_bill( raw_bill_table, raw_annual_bill_table, analysis_date @@ -160,10 +186,14 @@ def prelim_scenario(raw_bill_table, raw_annual_bill_table, budget_plot = (scenario_ob.get_budget_plot()) - return scenario_ob.get_graph_dict(), scenario_ob.get_economics() - # return scenario_ob.get_graph_dict(), scenario_ob.get_economics( - # ), scenario_ob.get_annual_debt_service(), scenario_ob.get_loan_summary( - # ), loan_summary, downpayment, budget_plot + # return scenario_ob.get_graph_dict(), scenario_ob.get_economics(), scenario_ob.get_annual_debt_service() + return ( + scenario_ob.get_economics(), + loan_summary, + downpayment, + scenario_ob.get_graph_dict(), + budget_plot + ) # **** ugly test **** -- GitLab From 4b502664be280a5d8a5e7c2b7d29f72afc96a0ad Mon Sep 17 00:00:00 2001 From: chenzheng06 Date: Thu, 29 Jun 2017 16:49:54 -0400 Subject: [PATCH 13/13] modify according to PR review --- bpfin/back_end_call/back_end_prelim.py | 18 ++++++------------ bpfin/financials/financial_saving.py | 2 +- bpfin/financials/loan_allocation.py | 5 +++-- bpfin/financials/scenario.py | 12 ++++++++---- 4 files changed, 18 insertions(+), 19 deletions(-) diff --git a/bpfin/back_end_call/back_end_prelim.py b/bpfin/back_end_call/back_end_prelim.py index f6d09b5..a22d149 100644 --- a/bpfin/back_end_call/back_end_prelim.py +++ b/bpfin/back_end_call/back_end_prelim.py @@ -33,17 +33,12 @@ def prelim_scenario(raw_bill_table, raw_annual_bill_table, customer_preference (dictionary): customer preference for financing plan Returns: - scenario_ob.get_economics(), - loan_summary, - downpayment, - scenario_ob.get_graph_dict(), - budget_plot - project_economics (dictionary): overview of project economics of this scenario, see description - loan_summary (list): list of list, showing loan financed financing plan. see description - downpayment (float): self-finance_downpayment amount graphic_dictionary (dictionary): dict of dict, showcase energy saving, energy debt service, net savings, see description scenario_plot (tuple): (saving%, cost), to draw a dot of scenario in budget simulator graph + loan_summary (list): list of list, showing loan financed financing plan. see description + downpayment (float): self-finance_downpayment amount + Description: raw_income_input = {2014: {'revenue': 90.0, 'utility_expense': 55.0, 'non_utility_expense': 35.0}, 2015:{},} @@ -152,7 +147,6 @@ def prelim_scenario(raw_bill_table, raw_annual_bill_table, prior_month_bill=monthly_bill(raw_bill_table, analysis_date)[2], percent_saving_dict=percent_saving_dict, full_saving_dict=full_saving_dict, - growth_rate_flag=growth_rate_flag, req_dscr=req_dscr, customer_preference=customer_preference) @@ -189,10 +183,10 @@ def prelim_scenario(raw_bill_table, raw_annual_bill_table, # return scenario_ob.get_graph_dict(), scenario_ob.get_economics(), scenario_ob.get_annual_debt_service() return ( scenario_ob.get_economics(), - loan_summary, - downpayment, scenario_ob.get_graph_dict(), - budget_plot + budget_plot, + loan_summary, + downpayment ) diff --git a/bpfin/financials/financial_saving.py b/bpfin/financials/financial_saving.py index d88f62b..19a0524 100644 --- a/bpfin/financials/financial_saving.py +++ b/bpfin/financials/financial_saving.py @@ -384,7 +384,7 @@ class Saving_Overview(): savings_dict = add_year_dictionary( savings_dict, copy.deepcopy(self.get_utility_annual_saving_charge(utility_type)) - ) + ) return savings_dict # total_saving_dict = {} diff --git a/bpfin/financials/loan_allocation.py b/bpfin/financials/loan_allocation.py index f563140..6e1f36e 100644 --- a/bpfin/financials/loan_allocation.py +++ b/bpfin/financials/loan_allocation.py @@ -1,6 +1,6 @@ -from scipy.optimize import linprog import copy -from bpfin.lib.other import sumproduct, multiply_list +from scipy.optimize import linprog +from bpfin.lib.other import multiply_list def loan_allocation( @@ -144,6 +144,7 @@ def loan_allocation( # **** ugly test **** # from bpfin.tests.testdata import feature_data as db # from bpfin.financials.loan import Loan_List +# from bpfin.lib.other import sumproduct # # req_dscr = { # # 'req_noi_dscr': 1.15, # # 'req_cash_dscr': 1.05, diff --git a/bpfin/financials/scenario.py b/bpfin/financials/scenario.py index ccdc195..b9f6d38 100644 --- a/bpfin/financials/scenario.py +++ b/bpfin/financials/scenario.py @@ -18,6 +18,7 @@ class Scenario(): prior_annual_bill_table (dictionary): annual bill, for 4 utility_types, proir to saving other_debt_service (dict): dictionary of debt service values per year + growth_rate_flag (float): indicating assumed growth rate, -2.0 == cagr, -1.0 == historical average prior_income_statement_table (object): complete Income_Statement_Table, with prior_saving projection prior_balance_sheet_table (object): complete Balance_Sheet_Table, with prior_saving projection @@ -47,8 +48,10 @@ class Scenario(): commission_date (date): the date that construction work finished, saving starts at NEXT month construction_cost (float): project cost, estimated or quoted manual_input_dict (dictionary): dict of boolean values, indicating manual_input status of each utility - prior_annual_bill_table (dictionary): annual bill, for 4 utility_types, proir to saving prior_income_statement_table (object): complete Income_Statement_Table, with prior_saving projection + other_debt_service (dict): dictionary of debt service values per year + prior_annual_bill_table (dictionary): annual bill, for 4 utility_types, proir to saving + growth_rate_flag (float): indicating assumed growth rate, -2.0 == cagr, -1.0 == historical average prior_balance_sheet_table (object): complete Balance_Sheet_Table, with prior_saving projection loan_input_list (list): list of dictionary of loan basic terms. @@ -65,6 +68,8 @@ class Scenario(): self.manual_input_dict = copy.deepcopy(manual_input_dict) + self.growth_rate_flag = growth_rate_flag + self.prior_annual_bill_table = copy.deepcopy(prior_annual_bill_table) self.prior_income_statement_table = copy.deepcopy(prior_income_statement_table) @@ -84,7 +89,7 @@ class Scenario(): self.downpayment = 0 def prelim_analysis(self, prior_month_bill, percent_saving_dict, - full_saving_dict, growth_rate_flag, req_dscr, + full_saving_dict, req_dscr, customer_preference): """ Conduct preliminary analysis for given scenario. @@ -97,7 +102,6 @@ class Scenario(): prior_month_bill (dictionary): dictionary of lists of prior monthly bill (dates, use, price, charge) percent_saving_dict (dictionary): dict of float, percentage saving for 4 utility types full_saving_dict (dictionary): dict of undefined saving, from engineering saving analysis. can be None - growth_rate_flag (float): indicating assumed growth rate, -2.0 == cagr, -1.0 == historical average req_dscr (dictionary): dict of required debt service coverage ratios customer_preference (dictionary): dict of customer preference @@ -132,7 +136,7 @@ class Scenario(): post_income_statement_table = copy.deepcopy( self.prior_income_statement_table) post_income_statement_table.project( - growth_rate_flag, post_annual_bill_table) + self.growth_rate_flag, post_annual_bill_table) self.post_income_statement_table = post_income_statement_table # generate post_saving balance_sheet_table object -- GitLab