diff --git a/bpfin/back_end_call/back_end_budget.py b/bpfin/back_end_call/back_end_budget.py index 81d3fd6b92899dfbaef654c228e5aabe750ea01f..3feaebad91f04519c5a894635827fea14abc3b0a 100644 --- a/bpfin/back_end_call/back_end_budget.py +++ b/bpfin/back_end_call/back_end_budget.py @@ -22,12 +22,62 @@ def budget_simulation( raw_loan_input_list): """ Generate budget simulation data, to draw graph and to put in table + + Inputs Validation: + analysis_date is needed + commission_date is needed + customer_preference['downpayment_max'] can be None or 0. Convert None to 0 in inner function + customer_preference['expected_payback'] cane be None or 0, meanings are different + req_dscr keys can be None or 0 + raw_bill_table is allowed to be None. Convert None to its structure + raw_annual_bill_table is allowed to be None. Convert None to its structure + raw_income_input is allowed to be None. + growth_rate_flag is needed + raw_liability_input is allowed to be None. Convert None to empty dictionary + raw_cash_balance is allowed to be None. Convert None to empty dictionary !! need further work + raw_loan_input_list is allowed to be None or empty list + + Description: + raw_bill_table = { + 'gas': raw_gas_bill_demo, + 'electricity': raw_elec_bill_demo, + 'oil': raw_oil_bill_demo, + 'water': None} + + raw_annual_bill_table = { + 'electricity': None, + 'gas': None, + 'oil': None, + '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 saving_interval will be read from frond-end in next version """ + # data validation and empty conversion: + if raw_bill_table is None: + raw_bill_table = { + 'electricity': None, + 'gas': None, + 'oil': None, + 'water': None} + + if raw_annual_bill_table is None: + raw_annual_bill_table = { + 'electricity': None, + 'gas': None, + 'oil': None, + 'water': None} + + if raw_liability_input is None: + raw_liability_input = {} + + if raw_cash_balance is None: + raw_cash_balance = {} + + # function variable building up preference_list = ['loan_only', 'loan_first', 'sf_first', 'sf_max'] annual_bill_table = annual_bill(raw_bill_table, raw_annual_bill_table, analysis_date)[0] @@ -42,7 +92,10 @@ def budget_simulation( loan_list = Loan_List(raw_loan_input_list).get_loan_list() - first_year_energy = income_table.get_total_energy_dict()[commission_date.year + 1] + if not raw_income_input: + first_year_energy = None + else: + first_year_energy = income_table.get_total_energy_dict()[commission_date.year + 1] first_year_cash = balance_sheet.get_first_year_cash(commission_date) first_year_noi = income_table.get_first_year_noi(commission_date) @@ -59,6 +112,14 @@ def budget_simulation( break budget_simulation_result = form_budget_simulation_result(loan_list) + # 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']) + # print('\n req_dscr=', req_dscr) + # print('\n first_year_noi=', first_year_noi) + # print('\n first_year_cash=', first_year_cash) + # print('\n first_year_energy=', first_year_energy) for saving_percent in saving_potential_list: financing_per_save = form_max_financing( @@ -68,7 +129,7 @@ def budget_simulation( req_dscr=req_dscr, first_year_noi=first_year_noi, first_year_cash=first_year_cash, - savings=first_year_energy * saving_percent + savings=(first_year_energy * saving_percent if first_year_energy else None) ) for preference in preference_list: amount_list = copy.deepcopy(financing_per_save[preference]) @@ -84,21 +145,28 @@ 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, -# -2.0, -# 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, + -2.0, + 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 +# reault: historical noi are same for energy bill not 0 vs 0 +# projection noi differ <5%, because bill not 0 is more accurate + +# 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) diff --git a/bpfin/back_end_call/back_end_inputs.py b/bpfin/back_end_call/back_end_inputs.py index e70c9901180926f6df9288dc930047ecce5e2ed0..bdd422cd72ba73fc941bd975b8216ec2e34d5b63 100644 --- a/bpfin/back_end_call/back_end_inputs.py +++ b/bpfin/back_end_call/back_end_inputs.py @@ -4,8 +4,6 @@ from bpfin.financials.cash_balance import cash_balance from bpfin.financials.liability import final_liability_dict from bpfin.financials.financial_income import Income_Statement_Table from bpfin.financials.financial_balance import Balance_Sheet_Table -# from bpfin.tests.testdata import feature_data as db -# import pprint # Bill Overview Monthly Input @@ -202,21 +200,23 @@ def form_prior_income_table( # **** ugly test **** +# import pprint +# from bpfin.tests.testdata import feature_data as db # 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)) -# print('\nprior_income_statement =', prior_income_statement_table( +# print('\nprior_income_statement =', form_prior_income_table( # db.raw_income_input, # db.raw_bill_table, # db.raw_annual_bill_table, # db.analysis_date, # -2.0)) -# result = prior_income_statement_table( +# result = form_prior_income_table( # db.raw_income_input, # db.raw_bill_table, # db.raw_annual_bill_table, # db.analysis_date, -# -2.0) +# -2.0)[0] # writein = str(result) # f = open('data_generation.py', 'w') diff --git a/bpfin/financials/cash_balance.py b/bpfin/financials/cash_balance.py index 28f48e9cc114fa2e1c31b009e302bd08fe48c6d9..cf342d0cbc3badf05532da311b1d48203e824351 100644 --- a/bpfin/financials/cash_balance.py +++ b/bpfin/financials/cash_balance.py @@ -11,7 +11,11 @@ def cash_balance(analysis_date, cash_dictionary): ValueError when more than 1 balance sheet for one year. Returns: dictionary: {datetime, cash value} + Input Validation: + cash_dictionary is allowed to be empty dictionary. If so, result is dictionary with cash values = 0 """ + if not cash_dictionary: + return None analysis_years = {} @@ -39,10 +43,12 @@ def cash_balance(analysis_date, cash_dictionary): sum_cash = [] for year in cash_balance_dictionary: sum_cash.append(cash_balance_dictionary[year]) + # cash_average = (sum(sum_cash)/len(sum_cash) if sum_cash else 0) cash_average = sum(sum_cash)/len(sum_cash) final_dict = {} for year in analysis_years: + # if cash_balance_dictionary: if year in cash_balance_dictionary: final_dict[year] = cash_balance_dictionary[year] if year < min(cash_balance_dictionary): @@ -50,6 +56,8 @@ def cash_balance(analysis_date, cash_dictionary): if year > min(cash_balance_dictionary) and year < max(cash_balance_dictionary): if year not in cash_balance_dictionary: final_dict[year] = cash_average + # else: + # final_dict[year] = 0 return final_dict diff --git a/bpfin/financials/financial_balance.py b/bpfin/financials/financial_balance.py index a56472bccc1de7e2467a60d83518370fcef04f92..ca3fad21c74137e8d619a53748dca757d92a0089 100644 --- a/bpfin/financials/financial_balance.py +++ b/bpfin/financials/financial_balance.py @@ -1,7 +1,5 @@ import copy -from bpfin.lib import other as lib from bpfin.utilbills.bill_lib import form_bill_year -from numpy import mean def convert_balance_sheet_class(balance_sheet_class): @@ -74,6 +72,9 @@ class Balance_Sheet_Table(): cash_balance_dictionary (dict): dictionary of known annual cash value items, key is year other_debt_service_dictionary (dict): dictionary of known annual liability value items, key is year net_income_dictionary (dict): dictionary of known annual net_income value items, key is year + + To Do: + data validation: if raw_cash inputs are empty, return empty tables or None, especially for first year cash """ self.hist_start_year = None @@ -81,23 +82,24 @@ class Balance_Sheet_Table(): self.hist_balance_sheet_table = [] # entire table, not just historical self.bs_table = [] - for year in sorted(cash_balance_dictionary): - current_balance_sheet = Balance_Sheet() - if year in net_income_dictionary: - net_income = net_income_dictionary[year] - else: - net_income = None - current_balance_sheet.put_hist_balance_sheet( - year, cash_balance_dictionary[year], - other_debt_service_dictionary[year], - net_income) - self.hist_balance_sheet_table.append(current_balance_sheet) - - sorted_balance_sheet_hist_year = sorted(cash_balance_dictionary) - self.hist_start_year = sorted_balance_sheet_hist_year[0] - self.hist_end_year = sorted_balance_sheet_hist_year[-1] - - self.bs_table = copy.deepcopy(self.hist_balance_sheet_table) + if cash_balance_dictionary: + for year in sorted(cash_balance_dictionary): + current_balance_sheet = Balance_Sheet() + if year in net_income_dictionary: + net_income = net_income_dictionary[year] + else: + net_income = None + current_balance_sheet.put_hist_balance_sheet( + year, cash_balance_dictionary[year], + other_debt_service_dictionary[year], + net_income) + self.hist_balance_sheet_table.append(current_balance_sheet) + + sorted_balance_sheet_hist_year = sorted(cash_balance_dictionary) + self.hist_start_year = sorted_balance_sheet_hist_year[0] + self.hist_end_year = sorted_balance_sheet_hist_year[-1] + + self.bs_table = copy.deepcopy(self.hist_balance_sheet_table) def project_balance_sheet(self, analysis_date, other_debt_service_dictionary, @@ -115,24 +117,28 @@ class Balance_Sheet_Table(): proforma_year = form_bill_year(analysis_date['proforma_start'], analysis_date['proforma_duration']) - current_table = copy.deepcopy(self.hist_balance_sheet_table) - for year in proforma_year: - last_year_cash = current_table[-1].cash - if year <= current_table[-1].year: - continue - current_other_debt_service = 0.00 - current_net_income = 0.00 - if year in other_debt_service_dictionary: - current_other_debt_service = other_debt_service_dictionary[ - year] - if year in net_income_dictionary: - current_net_income = net_income_dictionary[year] - current_balance_sheet = Balance_Sheet_Next( - year, last_year_cash, current_other_debt_service, - current_net_income) - current_table.append(current_balance_sheet) - self.bs_table = current_table - # return current_table + if self.hist_balance_sheet_table: + current_table = copy.deepcopy(self.hist_balance_sheet_table) + for year in proforma_year: + last_year_cash = current_table[-1].cash + if year <= current_table[-1].year: + continue + current_other_debt_service = 0.00 + current_net_income = 0.00 + if year in other_debt_service_dictionary: + current_other_debt_service = other_debt_service_dictionary[ + year] + if year in net_income_dictionary: + current_net_income = net_income_dictionary[year] + current_balance_sheet = Balance_Sheet_Next( + year, last_year_cash, current_other_debt_service, + current_net_income) + current_table.append(current_balance_sheet) + self.bs_table = current_table + return + else: + self.bs_table = {} + return def get_hist_balance_sheet_table(self): hist_balance_sheet_dict = {} @@ -163,9 +169,12 @@ class Balance_Sheet_Table(): def get_first_year_cash(self, commission_date): first_year = commission_date.year + 1 - for current_balance_sheet in self.bs_table: - if current_balance_sheet.year == first_year: - return current_balance_sheet.cash + if self.bs_table: + for current_balance_sheet in self.bs_table: + if current_balance_sheet.year == first_year: + return current_balance_sheet.cash + else: + return None def get_cash_dict(self): """ @@ -174,6 +183,9 @@ class Balance_Sheet_Table(): dictionary: cash for each year in balance sheet table. Key is year """ cash_dict = {} - for current_balance_sheet in self.bs_table: - cash_dict[current_balance_sheet.year] = current_balance_sheet.cash - return cash_dict + if self.bs_table: + for current_balance_sheet in self.bs_table: + cash_dict[current_balance_sheet.year] = current_balance_sheet.cash + return cash_dict + else: + return {} diff --git a/bpfin/financials/financial_budget_simulator.py b/bpfin/financials/financial_budget_simulator.py index 9a28e452d049459677450479d3a6f48d677df42c..6fd3826e64d6a1146e4e6be44c5f7b5456e186b7 100644 --- a/bpfin/financials/financial_budget_simulator.py +++ b/bpfin/financials/financial_budget_simulator.py @@ -4,9 +4,7 @@ run budget calculator and return table of results """ import copy from scipy.optimize import linprog -from bpfin.lib.other import sumproduct -from bpfin.tests.testdata import sample_data as db -from bpfin.financials.loan import Loan_List +from bpfin.lib.other import sumproduct, multiply_list def cal_max_budget( @@ -23,8 +21,6 @@ def cal_max_budget( determine the maximum budget for a project and its financing plan. 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 maximum budget. x_1 + x_2 + x_3 + ... + x_n = max @@ -32,8 +28,10 @@ def cal_max_budget( 2. x variables constraints 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) <= min (saving/dscr, noi/dscr, cash/dscr) - * sum of loan debt service <= financial constraints, making project financially feasibile + 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 3. x variables bounds 0 <= x_i <= loan[i]_max_amount 0 <= x_n <= downpayment_max @@ -51,16 +49,38 @@ def cal_max_budget( 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 - first_year_saving (float): first year saving. # maybe it can be min_annual_saving + downpayment_max (float): max amount that customer can make downpayment (self-finance part) + expected_payback: expected pay back on customer downpayment. if customer has no preference, it is infinity + req_dscr(dictionary): required dscr, kyes are: req_saving_dscr, req_noi_dscr, req_cash_dscr 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 + savings (float): first year saving. # maybe it can be min_annual_saving is_loan_first (boolean): 0 == client wants to reduce loan with self-finance, 1 == use as much loan as possible - # 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) + + Inputs Validation, Cases Explain if Inputs Are None or 0: + All inputs are allowed as None or 0, converting work is done inside function. + However, certain rules apply as following: + + loan_list: empty list or None is allowed, if no Loan options are available + if loan_list is empty, project budget = self-finance amount. result also shows no loan options + downpayment_max: 0 or None is allowed, if customer has no self-finance money or info is n/a + If None passed in, replace its value with 0 (done in this function) + This also ensures that at least one financing source is indicated, even it is 0 + If loan_list is empty and downpayment_max is 0/None, warning raised + expected_payback: 0 or None is allowed, but meanings differ + If customer hopes to get investment immediately, pass in 0, result of downpayment = 0, warning raised + If customer has no expectation on payback, pass in None, result of downpayment = downpayment_max + req_dscr: 0 or None is allowed. If a condition (one of the saving, noi, or cash) is not required, pass in None + if a dscr is None, it means no requirement on that condition, replace with 0 + first_year_noi: 0 and None is allowed, but meanings differ + If financial info available but noi is 0, pass in 0. If noi_dscr required, all budget = 0, warining raised + If customer has no financial info available, pass in None. Condition of noi will not be calculated + first_year_cash: 0 and None is allowed, but meanings differ + If financial info available but cash is 0, pass in 0. If cash_dscr required, all budget = 0, warining raised + If customer has no financial info available, pass in None. Condition of cash will not be calculated + savings: 0 and None is allowed, but meanings differ + If bill info but bill is 0, pass in 0. If saving_dscr is required, all budget = 0, warining raised + If customer has no bill info available, pass in None. Condition of cash will not be calculated Return: float: max budget with given conditions @@ -75,52 +95,68 @@ def cal_max_budget( Note: noi == net operating income dscr == debt service coverage ratio - - To do: calculate self_finance amount - rewrite descriptions """ - # sum_loan_max_amount = sum(list(current_loan.max_amount for current_loan in loan_list)) - # # pre-request judge: loan_list has a length - # print(len(loan_list)) + # sum_loan_max_amount = sum(list(current_loan.max_amount for current_loan in loan_list)) in fact, not needed !!!!!!! + + # # 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 # linear programming (LP) # Set LP constrains. # objective function: x1 + x2 + x3 +...+ xn = maximum c_formula = [-1] * (len(loan_list) + 1) + # x variables constraint, x1/payback1 + x2/payback2 +...+ xn/payback_n <= saving/dscr # x variables constraint, debt services <= required_debt_service 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_matrix.append(a_1) - A_matrix.append(a_2) + a_3.append(0) + b_list = [] - b_list.append( - savings / req_dscr['req_saving_dscr'] - ) - b_list.append(min( - savings / req_dscr['req_saving_dscr'], - first_year_noi / req_dscr['req_noi_dscr'], - first_year_cash / req_dscr['req_cash_dscr'] - )) + b_list.append(savings if savings 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) + + A_matrix.append(multiply_list(a_1, (req_dscr['req_saving_dscr'] if savings 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))) + + # print('\n A_matrix =', A_matrix) + # print('\n b_list =', b_list) + # 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('\ndscr_allow', savings / 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) # LP calculation. x[i] == loan amount from loan[i], x[-1] == self-finance amount res = linprog(c_formula, A_ub=A_matrix, b_ub=b_list, bounds=bound_list, options={'disp': False}) total_ds = sumproduct(res.x, a_2) if not res.success: return [0] * (len(loan_list) + 1) + + # Calculate self-finance amount if loan_fist is True if is_loan_first: - # print('\n saving allow =', (savings/req_dscr['req_saving_dscr'] - total_ds) * expected_payback / 12) - downpayment = min((savings/req_dscr['req_saving_dscr'] - total_ds) * expected_payback / 12, downpayment_max) + if savings 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((savings/req_dscr['req_saving_dscr'] - total_ds))) + downpayment = (min(max([0, extra_saving]) * expected_payback / 12, downpayment_max)) + else: + downpayment = downpayment_max res.x[-1] = downpayment + + # Return final result + # print('\n total budget =', sum(list(res.x))) return list(res.x) @@ -201,6 +237,30 @@ def form_max_financing( def form_budget_simulation_result(loan_list): + """ + generate a data structure to store result data from budget simulation calculation + + Args: + loan_list(list): list of Loan class object + Return: + dictionary: dict of lists. keys are financing level (loan_only, loan_first, sf_first, sf_max) + values are list of list, each inner list has one element, which is financing plan line items + + Description: + budget_simulation_result = { + 'loan_only': [ + ['saving_percentage'], ['Budget'], ['NYSERDA'], ['Joe Fund'], ['Tooraj Capital'], ['Self Finance'] + ], + 'loan_first': [ + ['saving_percentage'], ['Budget'], ['NYSERDA'], ['Joe Fund'], ['Tooraj Capital'], ['Self Finance'] + ], + 'sf_first': [ + ['saving_percentage'], ['Budget'], ['NYSERDA'], ['Joe Fund'], ['Tooraj Capital'], ['Self Finance'] + ], + 'sf_max': [ + ['saving_percentage'], ['Budget'], ['NYSERDA'], ['Joe Fund'], ['Tooraj Capital'], ['Self Finance'] + ]} + """ preference_list = ['loan_only', 'loan_first', 'sf_first', 'sf_max'] budget_simulation_result = {} for preference in preference_list: @@ -214,76 +274,207 @@ def form_budget_simulation_result(loan_list): return budget_simulation_result -# print(form_budget( +def validate_list_one_true(param_list): + """ + This func is not called now. Maybe it is useful in the future + validate the list elements, at least one element is True. + If all elements are None or 0, return False + Args: + param_list(list): list that need validation + Return: + boolean: True == at least one element is True + """ + for param in param_list: + if param: + return True + return False + + +# **** ugly test **** +# from bpfin.tests.testdata import sample_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(cal_max_budget( +# loan_list=Loan_List(db.loan_input_list).loan_list, +# downpayment_max=90000, +# expected_payback=120, +# req_dscr=req_dscr, +# first_year_noi=10000, +# first_year_cash=10000, +# savings=15000, +# is_loan_first=1)) + +# print('\n loan_list=None') +# print(cal_max_budget( +# loan_list=None, +# downpayment_max=90000, +# expected_payback=120, +# req_dscr=req_dscr, +# first_year_noi=10000, +# first_year_cash=10000, +# savings=15000, +# is_loan_first=1)) + +# print('\n loan_list = downpayment_max = None,') +# print(cal_max_budget( +# loan_list=None, +# downpayment_max=None, +# expected_payback=120, +# req_dscr=req_dscr, +# first_year_noi=10000, +# first_year_cash=10000, +# savings=15000, +# is_loan_first=1)) + +# print('\n expected_payback = None') +# print(cal_max_budget( +# loan_list=Loan_List(db.loan_input_list).loan_list, +# downpayment_max=90000, +# expected_payback=None, +# req_dscr=req_dscr, +# first_year_noi=10000, +# first_year_cash=10000, +# savings=15000, +# is_loan_first=1)) + +# print('\n expected_payback = None, is_loan_first=0') +# print(cal_max_budget( +# loan_list=Loan_List(db.loan_input_list).loan_list, +# downpayment_max=90000, +# expected_payback=None, +# req_dscr=req_dscr, +# first_year_noi=10000, +# first_year_cash=10000, +# savings=15000, +# is_loan_first=0)) + +# print('\n expected_payback = 0') +# print(cal_max_budget( +# loan_list=Loan_List(db.loan_input_list).loan_list, +# downpayment_max=90000, +# expected_payback=0, +# req_dscr=req_dscr, +# first_year_noi=10000, +# first_year_cash=10000, +# savings=15000, +# is_loan_first=1)) + +# print('\n req_noi_dscr = None, req_cash_dscr = None') +# print(cal_max_budget( +# loan_list=Loan_List(db.loan_input_list).loan_list, +# downpayment_max=90000, +# expected_payback=120, +# req_dscr={'req_noi_dscr': None, 'req_cash_dscr': None, 'req_saving_dscr': 1.10}, +# first_year_noi=10000, +# first_year_cash=10000, +# savings=15000, +# is_loan_first=1)) + +# print('\n req_saving_dscr = None') +# print(cal_max_budget( +# loan_list=Loan_List(db.loan_input_list).loan_list, +# downpayment_max=90000, +# expected_payback=120, +# req_dscr={'req_noi_dscr': 1.15, 'req_cash_dscr': 1.05, 'req_saving_dscr': None}, +# first_year_noi=10000, +# first_year_cash=10000, +# savings=15000, +# is_loan_first=1)) + +# print('\n req_noi_dscr = None, req_cash_dscr=0, req_saving_dscr = None') +# print(cal_max_budget( +# loan_list=Loan_List(db.loan_input_list).loan_list, +# downpayment_max=90000, +# expected_payback=120, +# req_dscr={'req_noi_dscr': None, 'req_cash_dscr': 0, 'req_saving_dscr': None}, +# first_year_noi=10000, +# first_year_cash=10000, +# savings=15000, +# is_loan_first=1)) + +# print('\n first_year_noi=None') +# print(cal_max_budget( +# loan_list=Loan_List(db.loan_input_list).loan_list, +# downpayment_max=90000, +# expected_payback=120, +# req_dscr={'req_noi_dscr': 1.15, 'req_cash_dscr': 1.05, 'req_saving_dscr': 1.10}, +# first_year_noi=None, +# first_year_cash=10000, +# savings=15000, +# is_loan_first=1)) + +# print('\n first_year_noi=0') +# print(cal_max_budget( +# loan_list=Loan_List(db.loan_input_list).loan_list, +# downpayment_max=90000, +# expected_payback=120, +# req_dscr={'req_noi_dscr': 1.15, 'req_cash_dscr': 1.05, 'req_saving_dscr': 1.10}, +# first_year_noi=0, +# first_year_cash=10000, +# savings=15000, +# is_loan_first=1)) + +# print('\n first_year_noi=0, req_noi_dscr: 0') +# print(cal_max_budget( +# loan_list=Loan_List(db.loan_input_list).loan_list, +# downpayment_max=90000, +# expected_payback=120, +# req_dscr={'req_noi_dscr': 0, 'req_cash_dscr': 1.05, 'req_saving_dscr': 1.10}, +# first_year_noi=0, +# first_year_cash=10000, +# savings=15000, +# is_loan_first=1)) + +# print('\n savings=None') +# print(cal_max_budget( # loan_list=Loan_List(db.loan_input_list).loan_list, # downpayment_max=90000, # expected_payback=120, -# req_dscr=db.req_dscr, +# req_dscr={'req_noi_dscr': 1.15, 'req_cash_dscr': 1.05, 'req_saving_dscr': 1.10}, # first_year_noi=10000, # first_year_cash=10000, -# savings=15000)) - - -# ******** 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 ************* -# 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] +# savings=None, +# is_loan_first=1)) + +# print('\n savings=0') +# print(cal_max_budget( +# loan_list=Loan_List(db.loan_input_list).loan_list, +# downpayment_max=90000, +# expected_payback=120, +# req_dscr={'req_noi_dscr': 1.15, 'req_cash_dscr': 1.05, 'req_saving_dscr': 1.10}, +# first_year_noi=10000, +# first_year_cash=10000, +# savings=0, +# is_loan_first=1)) + +# print('\n savings=0 , req_saving_dscr =0' ) +# print(cal_max_budget( +# loan_list=Loan_List(db.loan_input_list).loan_list, +# downpayment_max=90000, +# expected_payback=120, +# req_dscr={'req_noi_dscr': 1.15, 'req_cash_dscr': 1.05, 'req_saving_dscr': 0}, +# first_year_noi=10000, +# first_year_cash=10000, +# savings=0, +# is_loan_first=1)) + +# print('\n first_year_noi=None, first_year_cash=None, savings=None,' ) +# print(cal_max_budget( +# loan_list=Loan_List(db.loan_input_list).loan_list, +# downpayment_max=90000, +# expected_payback=120, +# req_dscr=req_dscr, +# first_year_noi=None, +# first_year_cash=None, +# savings=None, +# is_loan_first=1)) + + +# !!!! next step: write some warning message. Learn to run warning message diff --git a/bpfin/financials/financial_income.py b/bpfin/financials/financial_income.py index 06ab55386fb2737c61905354bfd570fe48f2c2d5..c3dca1e2c6b2633c2b727b545179e8f1cb4717ec 100644 --- a/bpfin/financials/financial_income.py +++ b/bpfin/financials/financial_income.py @@ -4,10 +4,9 @@ Income_Statement is incorporate with new Bill class, taking in bill_list as inpu """ import copy from bpfin.lib.other import cal_cagr -# from bpfin.lib import other as lib +from bpfin.lib.other import UTILITY_TYPE_LIST from bpfin.utilbills.bill_lib import form_bill_year from numpy import mean -from bpfin.lib.other import UTILITY_TYPE_LIST class Income_Statement(): @@ -227,8 +226,8 @@ class Income_Statement_Table(): annual_bill_table years should cover proforma years """ # validate raw_income_input - if not validate_empty(raw_income_input): - raise ValueError('Income_Statement_Table-init() raw_income_input is empty') + # if not validate_empty(raw_income_input): + # raise ValueError('Income_Statement_Table-init() raw_income_input is empty') if not validate_empty(analysis_date): raise ValueError('Income_Statement_Table-init() analysis_date is empty') # validate annual_bill_table @@ -245,19 +244,21 @@ class Income_Statement_Table(): self.table = [] self.characters = {} self.analysis_date = analysis_date + self.annual_bill_table = annual_bill_table for year in raw_income_input.keys(): 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] - self.cagr = 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) + if raw_income_input: + 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] + self.cagr = 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 @@ -267,11 +268,11 @@ class Income_Statement_Table(): 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 = copy.deepcopy(self.hist_table) + if raw_income_input: + 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 = copy.deepcopy(self.hist_table) self.characters = { 'start_year': self.hist_start_year, @@ -293,6 +294,10 @@ class Income_Statement_Table(): Note: project() overwrites existing projection data """ + if not self.hist_table: + self.table = [] + return + if not validate_growth_rate_flag(growth_rate_flag): raise ValueError('growth_rate input is not valid') proforma_year = form_bill_year(self.analysis_date['proforma_start'], @@ -307,7 +312,6 @@ class Income_Statement_Table(): annual_bill_table) current_table.append(current_income_statement) self.table = current_table - # return current_table def get_hist_table(self): """ @@ -395,7 +399,6 @@ class Income_Statement_Table(): 'total_opex': current_income_statement.total_opex, 'noi': current_income_statement.noi } - return None def get_noi_dict(self): """ @@ -451,11 +454,26 @@ class Income_Statement_Table(): return table_dict def get_total_energy_dict(self): + """ + Get total energy bill for each year + Return: + dictionary: keys are years in projected income statement, values are float, total energy expense(bill) + """ total_energy_dict = {} - for current_income_statement in self.table: - total_energy_dict[ - current_income_statement.year] = current_income_statement.energy_opex - return total_energy_dict + if self.table: + for current_income_statement in self.table: + total_energy_dict[ + current_income_statement.year] = current_income_statement.energy_opex + return total_energy_dict + else: + proforma_year = form_bill_year( + self.analysis_date['proforma_start'], + self.analysis_date['proforma_duration']) + for year in proforma_year: + total_energy_dict[year] = 0 + for utility in UTILITY_TYPE_LIST: + total_energy_dict[year] += self.annual_bill_table[utility][year] + return total_energy_dict # what is this??? no description, and it calls wrong attribute # def get_utility_opex_dict(self, energy_utility_opex): diff --git a/bpfin/financials/loan.py b/bpfin/financials/loan.py index 76b8faeb4c974c9d8162881b7691732239d9e8f3..e8c7046e1bb2c138fe4d528bb245422ec35e27d1 100644 --- a/bpfin/financials/loan.py +++ b/bpfin/financials/loan.py @@ -127,6 +127,7 @@ class Loan_List(): Args: loan_input_list (list): list of dictionary of loan basic terms. """ + loan_input_list = ([] if not loan_input_list else loan_input_list) self.loan_list = [] for current_loan in loan_input_list: temp_loan = None diff --git a/bpfin/tests/testdata/feature_data.py b/bpfin/tests/testdata/feature_data.py index 38964421b66323abd4b2d5a9a265bac4facfa425..f30f2dcd79883e504eb1addb62448f9789a1a653 100644 --- a/bpfin/tests/testdata/feature_data.py +++ b/bpfin/tests/testdata/feature_data.py @@ -29,6 +29,12 @@ req_dscr = { 'req_saving_dscr': 1.10 } +# req_dscr = { +# 'req_noi_dscr': 1.15, +# 'req_cash_dscr': 1.15, +# 'req_saving_dscr': None +# } + # Customer Preference customer_preference = { @@ -172,6 +178,12 @@ raw_bill_table = { 'oil': raw_oil_bill_demo, 'water': None} +# raw_bill_table = { +# 'gas': None, +# 'electricity': None, +# 'oil': None, +# 'water': None} + # raw annual_bill # annual_bill_gas = {2014: 0, 2015: 1020, 2016: 1220, 2017: 1520} # annual_bill_oil = {2015: 1010, 2016: 1210, 2017: 1510} @@ -181,12 +193,19 @@ annual_bill_water = {2014: 20000, 2015: 20500, 2016: 21000} # annual_bill_oil = {2015: 1010, 2016: 1210, 2017: 1510} raw_annual_bill_table = { - 'electricity': None, # True == Mannual Input + 'electricity': None, 'gas': None, 'oil': None, 'water': annual_bill_water } +# raw_annual_bill_table = { +# 'electricity': None, +# 'gas': None, +# 'oil': None, +# 'water': None +# } + manual_input_dict = { 'electricity': False, 'gas': False, @@ -266,6 +285,7 @@ loan_term_2 = {'institute': 'Joe Fund', 'max_amount': 50000, 'interest': 0.05, ' 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} raw_loan_input_list = [loan_term_1, loan_term_2, loan_term_3] +# raw_loan_input_list = None # ****** result *****