diff --git a/openbb_terminal/stocks/fundamental_analysis/dcf_view.py b/openbb_terminal/stocks/fundamental_analysis/dcf_view.py index 330f113af2f..31b0c900391 100644 --- a/openbb_terminal/stocks/fundamental_analysis/dcf_view.py +++ b/openbb_terminal/stocks/fundamental_analysis/dcf_view.py @@ -34,6 +34,7 @@ class CreateExcelFA: len_pred: int = 10, max_similars: int = 3, no_filter: bool = False, + growth: bool = False, ): """ Creates a detailed DCF for a given company @@ -49,9 +50,11 @@ class CreateExcelFA: len_pred : int The number of years to make predictions for before assuming a terminal value max_similars : int - The maximum number of similar companies to show, will be less if there are not enough similar companies + The maximum number of similar companies to show, will be less if there are not enough no_filter : bool Disable filtering of similar companies to being in the same market cap category + growth : bool + When true this turns the revenue assumption from linear regression to percentage growth """ self.info: Dict[str, Any] = { "len_data": 0, @@ -64,6 +67,7 @@ class CreateExcelFA: "no_filter": no_filter, } self.letter: int = 0 + self.growth = growth self.starts: Dict[str, int] = {"IS": 4, "BS": 18, "CF": 47} self.wb: Workbook = Workbook() self.ws: Dict[int, Any] = { @@ -215,12 +219,13 @@ class CreateExcelFA: ) dcf_model.set_cell(self.ws[1], f"{dcf_static.letters[col]}4", "m") dcf_model.set_cell(self.ws[1], f"{dcf_static.letters[col+1]}4", "b") - self.get_linear("Date", "Revenue") - self.get_linear("Revenue", "Cost of Revenue") + rev_pred_type = "growth" if self.growth else "linear" + self.get_growth("Date", "Revenue", pred_type=rev_pred_type) + self.get_growth("Revenue", "Cost of Revenue") self.get_sum("Gross Profit", "Revenue", [], ["Cost of Revenue"]) - self.get_linear("Revenue", "Selling, General & Admin", True) - self.get_linear("Revenue", "Research & Development", True) - self.get_linear("Revenue", "Other Operating Expenses") + self.get_growth("Revenue", "Selling, General & Admin", True) + self.get_growth("Revenue", "Research & Development", True) + self.get_growth("Revenue", "Other Operating Expenses") self.get_sum( "Operating Income", "Gross Profit", @@ -231,10 +236,10 @@ class CreateExcelFA: "Other Operating Expenses", ], ) - self.get_linear("Revenue", "Preferred Dividends") - self.get_linear("Revenue", "Interest Expense / Income") - self.get_linear("Revenue", "Other Expense / Income") - self.get_linear("Operating Income", "Income Tax") + self.get_growth("Revenue", "Preferred Dividends") + self.get_growth("Revenue", "Interest Expense / Income") + self.get_growth("Revenue", "Other Expense / Income") + self.get_growth("Operating Income", "Income Tax") self.get_sum( "Net Income", "Operating Income", @@ -245,27 +250,27 @@ class CreateExcelFA: "Preferred Dividends", "Preferred Dividends are not important in a DCF so we do not attempt to predict them.", ) - self.get_linear("Revenue", "Cash & Equivalents", True) - self.get_linear("Revenue", "Short-Term Investments", True) + self.get_growth("Revenue", "Cash & Equivalents", True) + self.get_growth("Revenue", "Short-Term Investments", True) self.get_sum( "Cash & Cash Equivalents", "Cash & Equivalents", ["Short-Term Investments"], [], ) - self.get_linear("Revenue", "Receivables", True) - self.get_linear("Revenue", "Inventory", True) - self.get_linear("Revenue", "Other Current Assets") + self.get_growth("Revenue", "Receivables", True) + self.get_growth("Revenue", "Inventory", True) + self.get_growth("Revenue", "Other Current Assets") self.get_sum( "Total Current Assets", "Cash & Cash Equivalents", ["Receivables", "Inventory", "Other Current Assets"], [], ) - self.get_linear("Revenue", "Property, Plant & Equipment", True) - self.get_linear("Revenue", "Long-Term Investments", True) - self.get_linear("Revenue", "Goodwill and Intangibles", True) - self.get_linear("Revenue", "Other Long-Term Assets") + self.get_growth("Revenue", "Property, Plant & Equipment", True) + self.get_growth("Revenue", "Long-Term Investments", True) + self.get_growth("Revenue", "Goodwill and Intangibles", True) + self.get_growth("Revenue", "Other Long-Term Assets") self.get_sum( "Total Long-Term Assets", "Property, Plant & Equipment", @@ -279,10 +284,10 @@ class CreateExcelFA: self.get_sum( "Total Assets", "Total Current Assets", ["Total Long-Term Assets"], [] ) - self.get_linear("Revenue", "Accounts Payable") - self.get_linear("Revenue", "Deferred Revenue") - self.get_linear("Revenue", "Current Debt") - self.get_linear("Revenue", "Other Current Liabilities") + self.get_growth("Revenue", "Accounts Payable") + self.get_growth("Revenue", "Deferred Revenue") + self.get_growth("Revenue", "Current Debt") + self.get_growth("Revenue", "Other Current Liabilities") self.get_sum( "Total Current Liabilities", "Accounts Payable", @@ -303,7 +308,7 @@ class CreateExcelFA: "nceinstitute.com/resources/questions/model-questions/financial-modeling-plug/" ), ) # This is the plug - self.get_linear("Revenue", "Other Long-Term Liabilities") + self.get_growth("Revenue", "Other Long-Term Liabilities") self.get_sum( "Total Long-Term Liabilities", "Long-Term Debt", @@ -316,7 +321,7 @@ class CreateExcelFA: ["Total Long-Term Liabilities"], [], ) - self.get_linear("Revenue", "Common Stock") + self.get_growth("Revenue", "Common Stock") col = self.info["len_data"] + 1 rer = self.title_to_row("Retained Earnings") nir = self.title_to_row("Net Income") @@ -328,7 +333,7 @@ class CreateExcelFA: num_form="[$$-409]#,##0.00;[RED]-[$$-409]#,##0.00", ) - self.get_linear("Revenue", "Comprehensive Income") + self.get_growth("Revenue", "Comprehensive Income") self.get_sum( "Shareholders' Equity", "Common Stock", @@ -771,38 +776,27 @@ class CreateExcelFA: ) @log_start_end(log=logger) - def get_linear(self, x_ind: str, y_ind: str, no_neg: bool = False): + def get_growth( + self, x_ind: str, y_ind: str, no_neg: bool = False, pred_type: str = "linear" + ): + """Add growth to a column. Usually this is linear but other options are allowed + + x_ind: str + The x variable to use. This is unused if growth is the pred_type + y_ind: str + The y variable to use + no_neg: bool + Whether or not the value can have negative numbers (profit can be revenue cannot) + pred_type: str + This is assumed to be linear but growth is allowed as well + """ + x_type = "IS" if x_ind in self.df["IS"].index else "BS" y_type = "IS" if y_ind in self.df["IS"].index else "BS" x_df = self.df["IS"] if x_type == "IS" else self.df["BS"] y_df = self.df["IS"] if y_type == "IS" else self.df["BS"] - pre_x = ( - x_df.columns.to_numpy() if x_ind == "Date" else x_df.loc[x_ind].to_numpy() - ) - - vfunc = np.vectorize(dcf_model.string_float) - pre_x = vfunc(pre_x) - - if x_ind == "Date": - pre_x = pre_x - np.min(pre_x) - x = pre_x.reshape((-1, 1)) - pre_y = y_df.loc[y_ind].to_numpy() - y = vfunc(pre_y) - model = LinearRegression().fit(x, y) - r_sq = model.score(x, y) - r = abs(r_sq**0.5) - - if r > 0.9: - strength = "very strong" - elif r > 0.7: - strength = "strong" - elif r > 0.5: - strength = "moderate" - elif r > 0.3: - strength = "weak" - else: - strength = "very weak" + analy_message = "" row1 = ( y_df.index.get_loc(y_ind) + 1 @@ -810,38 +804,101 @@ class CreateExcelFA: ) col = self.info["len_pred"] + self.info["len_data"] + 3 - dcf_model.set_cell( - self.ws[1], f"{dcf_static.letters[col]}{row1}", float(model.coef_) - ) - dcf_model.set_cell( - self.ws[1], f"{dcf_static.letters[col+1]}{row1}", float(model.intercept_) - ) - dcf_model.set_cell( - self.ws[1], - f"{dcf_static.letters[col+2]}{row1}", - dcf_static.letters[self.letter], - font=dcf_static.red, - ) - dcf_model.set_cell( - self.ws[3], - f"A{self.letter+4}", - dcf_static.letters[self.letter], - font=dcf_static.red, - ) - dcf_model.set_cell( - self.ws[3], - f"B{self.letter+4}", - ( + if pred_type == "linear": + pre_x = ( + x_df.columns.to_numpy() + if x_ind == "Date" + else x_df.loc[x_ind].to_numpy() + ) + + vfunc = np.vectorize(dcf_model.string_float) + pre_x = vfunc(pre_x) + + if x_ind == "Date": + pre_x = pre_x - np.min(pre_x) + x = pre_x.reshape((-1, 1)) + pre_y = y_df.loc[y_ind].to_numpy() + y = vfunc(pre_y) + model = LinearRegression().fit(x, y) + r_sq = model.score(x, y) + r = abs(r_sq**0.5) + + if r > 0.9: + strength = "very strong" + elif r > 0.7: + strength = "strong" + elif r > 0.5: + strength = "moderate" + elif r > 0.3: + strength = "weak" + else: + strength = "very weak" + analy_message = ( f"The correlation between {x_ind.lower()} and {y_ind.lower()}" f" is {strength} with a correlation coefficient of {r:.4f}." - ), - ) + ) + dcf_model.set_cell( + self.ws[1], f"{dcf_static.letters[col]}{row1}", float(model.coef_) + ) + dcf_model.set_cell( + self.ws[1], + f"{dcf_static.letters[col+1]}{row1}", + float(model.intercept_), + ) + dcf_model.set_cell( + self.ws[1], + f"{dcf_static.letters[col+2]}{row1}", + dcf_static.letters[self.letter], + font=dcf_static.red, + ) + dcf_model.set_cell( + self.ws[3], + f"A{self.letter+4}", + dcf_static.letters[self.letter], + font=dcf_static.red, + ) + elif pred_type == "growth": + pre_y = y_df.loc[y_ind].to_numpy() + fin_y = [] + for item in pre_y: + try: + fin_y.append(float(item)) + except TypeError: + fin_y.append(0) + overall_growth = (fin_y[-1] - fin_y[0]) / fin_y[0] + yearly_growth = overall_growth / len(fin_y) + analy_message = ( + f"Assuming growth percentage of {(yearly_growth * 100):.2f}%" + ) + dcf_model.set_cell( + self.ws[1], f"{dcf_static.letters[col]}{row1}", yearly_growth + ) + dcf_model.set_cell( + self.ws[1], + f"{dcf_static.letters[col+2]}{row1}", + dcf_static.letters[self.letter], + font=dcf_static.red, + ) + + dcf_model.set_cell(self.ws[3], f"B{self.letter+4}", analy_message) col = self.info["len_data"] + 1 for i in range(self.info["len_pred"]): - if x_ind == "Date": + if pred_type == "growth": + row_n = ( + y_df.index.get_loc(y_ind) + 1 + self.starts["IS"] + if y_type == "IS" + else self.starts["BS"] + ) base = ( - f"(({dcf_static.letters[col+i]}4-B4)*{dcf_static.letters[col+self.info['len_pred']+2]}" + f"({dcf_static.letters[col+i-1]}{row1}* (1+" + f"{dcf_static.letters[col+self.info['len_pred']+2]}{row1}))" + ) + + elif x_ind == "Date": + base = ( + f"(({dcf_static.letters[col+i]}4-B4)*" + f"{dcf_static.letters[col+self.info['len_pred']+2]}" f"{row1})+{dcf_static.letters[col+self.info['len_pred']+3]}{row1}" ) else: @@ -851,7 +908,8 @@ class CreateExcelFA: else self.starts["BS"] ) base = ( - f"({dcf_static.letters[col+i]}{row_n}*{dcf_static.letters[col+self.info['len_pred']+2]}{row1})" + f"({dcf_static.letters[col+i]}{row_n}*" + f"{dcf_static.letters[col+self.info['len_pred']+2]}{row1})" f"+{dcf_static.letters[col+self.info['len_pred']+3]}{row1}" ) dcf_model.set_cell( diff --git a/openbb_terminal/stocks/fundamental_analysis/fa_controller.py b/openbb_terminal/stocks/fundamental_analysis/fa_controller.py index 64459275006..8337d0adf9f 100644 --- a/openbb_terminal/stocks/fundamental_analysis/fa_controller.py +++ b/openbb_terminal/stocks/fundamental_analysis/fa_controller.py @@ -1262,17 +1262,26 @@ class FundamentalAnalysisController(StockBaseController): default=6, help="Number of similar companies to generate ratios for.", ) + parser.add_argument( + "-g", + "--growth", + action="store_true", + dest="growth", + default=False, + help="Whether to replace a linear regression estimate with a growth estimate.", + ) ns_parser = self.parse_known_args_and_warn( parser, other_args, EXPORT_ONLY_RAW_DATA_ALLOWED ) if ns_parser: dcf = dcf_view.CreateExcelFA( - self.ticker, - ns_parser.audit, - ns_parser.ratios, - ns_parser.prediction, - ns_parser.similar, + ticker=self.ticker, + audit=ns_parser.audit, + ratios=ns_parser.ratios, + len_pred=ns_parser.prediction, + max_similars=ns_parser.similar, + growth=ns_parser.growth, ) dcf.create_workbook()