diff --git a/openbb_terminal/core/completer/choices.py b/openbb_terminal/core/completer/choices.py index 22363707d13..9793f7457f3 100644 --- a/openbb_terminal/core/completer/choices.py +++ b/openbb_terminal/core/completer/choices.py @@ -152,7 +152,7 @@ def _get_argument_parser( else: raise AssertionError( "One of these functions should be called once:\n" - " - parse_known_args_and_warn\n" + " - patch_parse_simple_args\n" " - parse_known_args_and_warn\n" ) @@ -201,7 +201,7 @@ def build_controller_choice_map(controller) -> dict: argument_parser=argument_parser ) except Exception as exception: - if environ["DEBUG_MODE"] == "true": + if environ.get("DEBUG_MODE", "false") == "true": raise exception return controller_choice_map diff --git a/openbb_terminal/stocks/behavioural_analysis/ba_controller.py b/openbb_terminal/stocks/behavioural_analysis/ba_controller.py index 44b5e31e9e7..9ee3f6cc156 100644 --- a/openbb_terminal/stocks/behavioural_analysis/ba_controller.py +++ b/openbb_terminal/stocks/behavioural_analysis/ba_controller.py @@ -769,9 +769,7 @@ class BehaviouralAnalysisController(StockBaseController): "[red]Ticker provided doesn't exist, load another one.\n[/red]" ) else: - console.print( - "[red]Words or sentences to be correlated against with, need to be provided.\n[/red]" - ) + console.print("[red]Please provide a phrase for analysis.\n[/red]") else: console.print( "[red]No ticker loaded. Please load using 'load '.\n[/red]" diff --git a/openbb_terminal/stocks/fundamental_analysis/av_model.py b/openbb_terminal/stocks/fundamental_analysis/av_model.py index e8a3e125730..98437b3bbfe 100644 --- a/openbb_terminal/stocks/fundamental_analysis/av_model.py +++ b/openbb_terminal/stocks/fundamental_analysis/av_model.py @@ -26,7 +26,7 @@ def check_premium_key(json_response: Dict) -> bool: "any of the premium plans at https://www.alphavantage.co/premium/ to instantly unlock all premium endpoints" }: console.print( - "This is a premium endpoint for AlphaVantage. Please use a premium key\n" + "This is a premium endpoint for AlphaVantage. Please use a premium key.\n" ) return True return False @@ -614,8 +614,11 @@ def get_fraud_ratios(symbol: str, detail: bool = False) -> pd.DataFrame: df_bs, _ = fd.get_balance_sheet_annual(symbol=symbol) df_is, _ = fd.get_income_statement_annual(symbol=symbol) - except Exception as e: - console.print(e) + except ValueError as e: + if "premium endpoint" in str(e): + console.print( + "This is a premium endpoint for AlphaVantage. Please use a premium key.\n" + ) return pd.DataFrame() # pylint: disable=no-member @@ -729,8 +732,11 @@ def get_dupont(symbol: str) -> pd.DataFrame: df_bs, _ = fd.get_balance_sheet_annual(symbol=symbol) df_is, _ = fd.get_income_statement_annual(symbol=symbol) - except Exception as e: - console.print(e) + except ValueError as e: + if "premium endpoint" in str(e): + console.print( + "This is a premium endpoint for AlphaVantage. Please use a premium key.\n" + ) return pd.DataFrame() # pylint: disable=no-member diff --git a/openbb_terminal/stocks/fundamental_analysis/av_view.py b/openbb_terminal/stocks/fundamental_analysis/av_view.py index 67e9f31737a..e57c6e734e5 100644 --- a/openbb_terminal/stocks/fundamental_analysis/av_view.py +++ b/openbb_terminal/stocks/fundamental_analysis/av_view.py @@ -436,7 +436,6 @@ def display_dupont( """ df = av_model.get_dupont(symbol) if df.empty: - console.print("[red]Invalid response from AlphaVantage[/red]\n") return if raw: print_rich_table( diff --git a/openbb_terminal/stocks/screener/screener_controller.py b/openbb_terminal/stocks/screener/screener_controller.py index 0194d242a9b..56a3da7e9ee 100644 --- a/openbb_terminal/stocks/screener/screener_controller.py +++ b/openbb_terminal/stocks/screener/screener_controller.py @@ -12,7 +12,6 @@ from openbb_terminal.custom_prompt_toolkit import NestedCompleter from openbb_terminal import feature_flags as obbff from openbb_terminal.core.config.paths import USER_PRESETS_DIRECTORY from openbb_terminal.decorators import log_start_end -from openbb_terminal.helper_classes import AllowArgsWithWhiteSpace from openbb_terminal.helper_funcs import ( EXPORT_BOTH_RAW_DATA_AND_FIGURES, EXPORT_ONLY_RAW_DATA_ALLOWED, @@ -29,20 +28,21 @@ from openbb_terminal.stocks.screener import ( finviz_view, yahoofinance_view, screener_view, + screener_helper, ) logger = logging.getLogger(__name__) # pylint: disable=E1121 -# TODO: HELP WANTED! This menu required some refactoring. Things that can be addressed: -# - better preset management (MVC style). PRESETS_PATH = USER_PRESETS_DIRECTORY / "stocks" / "screener" class ScreenerController(BaseController): """Screener Controller class""" + CHOICES_MENUS = ["ca"] + CHOICES_COMMANDS = [ "view", "set", @@ -53,7 +53,6 @@ class ScreenerController(BaseController): "ownership", "performance", "technical", - "ca", ] PRESETS_PATH_DEFAULT = Path(__file__).parent / "presets" @@ -73,6 +72,7 @@ class ScreenerController(BaseController): historical_candle_choices = ["o", "h", "l", "c", "a"] PATH = "/stocks/scr/" + CHOICES_GENERATION = True def __init__(self, queue: List[str] = None): """Constructor""" @@ -82,36 +82,7 @@ class ScreenerController(BaseController): self.screen_tickers: List = list() if session and obbff.USE_PROMPT_TOOLKIT: - choices: dict = {c: {} for c in self.controller_choices} - - choices["view"] = {c: {} for c in self.preset_choices} - choices["set"] = {c: {} for c in self.preset_choices} - choices["historical"] = { - "--start": None, - "-s": "--start", - "--type": {c: {} for c in self.historical_candle_choices}, - "--no-scale": {}, - "-n": "--no-scale", - "--limit": None, - "-l": "--limit", - } - screener_standard = { - "--preset": {c: {} for c in self.preset_choices}, - "-p": "--preset", - "--sort": {c: {} for c in finviz_view.d_cols_to_sort["overview"]}, - "-s": "--sort", - "--limit": None, - "-l": "--limit", - "--reverse": {}, - "-r": "--reverse", - } - choices["overview"] = screener_standard - choices["valuation"] = screener_standard - choices["financial"] = screener_standard - choices["ownership"] = screener_standard - choices["performance"] = screener_standard - choices["technical"] = screener_standard - + choices: dict = self.choices_default self.completer = NestedCompleter.from_nested_dict(choices) def parse_input(self, an_input: str) -> List: @@ -311,10 +282,11 @@ class ScreenerController(BaseController): parser.add_argument( "-s", "--sort", - action=AllowArgsWithWhiteSpace, + choices=screener_helper.finviz_choices("overview"), + type=str.lower, dest="sort", - default="", - nargs="+", + metavar="SORT", + default="Ticker", help="Sort elements of the table.", ) if other_args and "-" not in other_args[0][0]: @@ -329,29 +301,15 @@ class ScreenerController(BaseController): else: preset = self.preset - if ns_parser.sort: - if ns_parser.sort not in finviz_view.d_cols_to_sort["overview"]: - console.print(f"{ns_parser.sort} not a valid sort choice.\n") - else: - self.screen_tickers = finviz_view.screener( - loaded_preset=preset, - data_type="overview", - limit=ns_parser.limit, - ascend=ns_parser.reverse, - sortby=ns_parser.sort, - export=ns_parser.export, - ) - - else: - - self.screen_tickers = finviz_view.screener( - loaded_preset=preset, - data_type="overview", - limit=ns_parser.limit, - ascend=ns_parser.ascend, - sortby=ns_parser.sort, - export=ns_parser.export, - ) + sort_map = screener_helper.finviz_map("overview") + self.screen_tickers = finviz_view.screener( + loaded_preset=preset, + data_type="overview", + limit=ns_parser.limit, + ascend=ns_parser.reverse, + sortby=sort_map[ns_parser.sort], + export=ns_parser.export, + ) @log_start_end(log=logger) def call_valuation(self, other_args: List[str]): @@ -399,9 +357,10 @@ class ScreenerController(BaseController): "-s", "--sort", dest="sort", - default="", - nargs="+", - action=AllowArgsWithWhiteSpace, + default="Ticker", + choices=screener_helper.finviz_choices("valuation"), + type=str.lower, + metavar="SORT", help="Sort elements of the table.", ) if other_args and "-" not in other_args[0][0]: @@ -411,29 +370,15 @@ class ScreenerController(BaseController): ) if ns_parser: - if ns_parser.sort: - if ns_parser.sort not in finviz_view.d_cols_to_sort["valuation"]: - console.print(f"{ns_parser.sort} not a valid sort choice.\n") - else: - self.screen_tickers = finviz_view.screener( - loaded_preset=self.preset, - data_type="valuation", - limit=ns_parser.limit, - ascend=ns_parser.reverse, - sortby=ns_parser.sort, - export=ns_parser.export, - ) - - else: - - self.screen_tickers = finviz_view.screener( - loaded_preset=self.preset, - data_type="valuation", - limit=ns_parser.limit, - ascend=ns_parser.reverse, - sortby=ns_parser.sort, - export=ns_parser.export, - ) + sort_map = screener_helper.finviz_map("valuation") + self.screen_tickers = finviz_view.screener( + loaded_preset=self.preset, + data_type="valuation", + limit=ns_parser.limit, + ascend=ns_parser.reverse, + sortby=sort_map[ns_parser.sort], + export=ns_parser.export, + ) @log_start_end(log=logger) def call_financial(self, other_args: List[str]): @@ -480,10 +425,11 @@ class ScreenerController(BaseController): parser.add_argument( "-s", "--sort", - action=AllowArgsWithWhiteSpace, + choices=screener_helper.finviz_choices("financial"), + type=str.lower, dest="sort", - default="", - nargs="+", + metavar="SORT", + default="Ticker", help="Sort elements of the table.", ) if other_args and "-" not in other_args[0][0]: @@ -493,29 +439,15 @@ class ScreenerController(BaseController): ) if ns_parser: - if ns_parser.sort: - if ns_parser.sort not in finviz_view.d_cols_to_sort["financial"]: - console.print(f"{ns_parser.sort} not a valid sort choice.\n") - else: - self.screen_tickers = finviz_view.screener( - loaded_preset=self.preset, - data_type="financial", - limit=ns_parser.limit, - ascend=ns_parser.reverse, - sortby=ns_parser.sort, - export=ns_parser.export, - ) - - else: - - self.screen_tickers = finviz_view.screener( - loaded_preset=self.preset, - data_type="financial", - limit=ns_parser.limit, - ascend=ns_parser.reverse, - sortby=ns_parser.sort, - export=ns_parser.export, - ) + sort_map = screener_helper.finviz_map("financial") + self.screen_tickers = finviz_view.screener( + loaded_preset=self.preset, + data_type="financial", + limit=ns_parser.limit, + ascend=ns_parser.reverse, + sortby=sort_map[ns_parser.sort], + export=ns_parser.export, + ) @log_start_end(log=logger) def call_ownership(self, other_args: List[str]): @@ -563,9 +495,10 @@ class ScreenerController(BaseController): "-s", "--sort", dest="sort", - default="", - nargs="+", - action=AllowArgsWithWhiteSpace, + metavar="SORT", + default="Ticker", + choices=screener_helper.finviz_choices("ownership"), + type=str.lower, help="Sort elements of the table.", ) if other_args and "-" not in other_args[0][0]: @@ -576,29 +509,15 @@ class ScreenerController(BaseController): if ns_parser: - if ns_parser.sort: - if ns_parser.sort not in finviz_view.d_cols_to_sort["ownership"]: - console.print(f"{ns_parser.sort} not a valid sort choice.\n") - else: - self.screen_tickers = finviz_view.screener( - loaded_preset=self.preset, - data_type="ownership", - limit=ns_parser.limit, - ascend=ns_parser.reverse, - sortby=ns_parser.sort, - export=ns_parser.export, - ) - - else: - - self.screen_tickers = finviz_view.screener( - loaded_preset=self.preset, - data_type="ownership", - limit=ns_parser.limit, - ascend=ns_parser.reverse, - sortby=ns_parser.sort, - export=ns_parser.export, - ) + sort_map = screener_helper.finviz_map("ownership") + self.screen_tickers = finviz_view.screener( + loaded_preset=self.preset, + data_type="ownership", + limit=ns_parser.limit, + ascend=ns_parser.reverse, + sortby=sort_map[ns_parser.sort], + export=ns_parser.export, + ) @log_start_end(log=logger) def call_performance(self, other_args: List[str]): @@ -645,10 +564,11 @@ class ScreenerController(BaseController): parser.add_argument( "-s", "--sort", - action=AllowArgsWithWhiteSpace, + choices=screener_helper.finviz_choices("performance"), + type=str.lower, dest="sort", - default="", - nargs="+", + default="Ticker", + metavar="SORTBY", help="Sort elements of the table.", ) if other_args and "-" not in other_args[0][0]: @@ -657,31 +577,16 @@ class ScreenerController(BaseController): parser, other_args, EXPORT_ONLY_RAW_DATA_ALLOWED ) + sort_map = screener_helper.finviz_map("performance") if ns_parser: - - if ns_parser.sort: - if ns_parser.sort not in finviz_view.d_cols_to_sort["performance"]: - console.print(f"{ns_parser.sort} not a valid sort choice.\n") - else: - self.screen_tickers = finviz_view.screener( - loaded_preset=self.preset, - data_type="performance", - limit=ns_parser.limit, - ascend=ns_parser.reverse, - sortby=ns_parser.sort, - export=ns_parser.export, - ) - - else: - - self.screen_tickers = finviz_view.screener( - loaded_preset=self.preset, - data_type="performance", - limit=ns_parser.limit, - ascend=ns_parser.store_true, - sortby=ns_parser.sort, - export=ns_parser.export, - ) + self.screen_tickers = finviz_view.screener( + loaded_preset=self.preset, + data_type="performance", + limit=ns_parser.limit, + ascend=ns_parser.reverse, + sortby=sort_map[ns_parser.sort], + export=ns_parser.export, + ) @log_start_end(log=logger) def call_technical(self, other_args: List[str]): @@ -728,10 +633,10 @@ class ScreenerController(BaseController): parser.add_argument( "-s", "--sort", - action=AllowArgsWithWhiteSpace, + choices=screener_helper.finviz_choices("technical"), + type=str.lower, dest="sort", - default="", - nargs="+", + default="Ticker", help="Sort elements of the table.", ) if other_args and "-" not in other_args[0][0]: @@ -741,39 +646,27 @@ class ScreenerController(BaseController): ) if ns_parser: - - if ns_parser.sort: - if ns_parser.sort not in finviz_view.d_cols_to_sort["technical"]: - console.print(f"{ns_parser.sort} not a valid sort choice.\n") - else: - self.screen_tickers = finviz_view.screener( - loaded_preset=self.preset, - data_type="technical", - limit=ns_parser.limit, - ascend=ns_parser.reverse, - sortby=ns_parser.sort, - export=ns_parser.export, - ) - - else: - - self.screen_tickers = finviz_view.screener( - loaded_preset=self.preset, - data_type="technical", - limit=ns_parser.limit, - ascend=ns_parser.reverse, - sortby=ns_parser.sort, - export=ns_parser.export, - ) + sort_map = screener_helper.finviz_map("technical") + self.screen_tickers = finviz_view.screener( + loaded_preset=self.preset, + data_type="technical", + limit=ns_parser.limit, + ascend=ns_parser.reverse, + sortby=sort_map[ns_parser.sort], + export=ns_parser.export, + ) @log_start_end(log=logger) def call_ca(self, _): """Call the comparison analysis menu with selected tickers""" if self.screen_tickers: - self.queue = ca_controller.ComparisonAnalysisController( - self.screen_tickers, self.queue - ).menu(custom_path_menu_above="/stocks/") + self.queue = self.load_class( + ca_controller.ComparisonAnalysisController, + self.screen_tickers, + self.queue, + ) else: console.print( - "Some tickers must be screened first through one of the presets!\n" + "Please select a screener using 'set' and then run 'historical' " + "before going to the CA menu.\n" ) diff --git a/openbb_terminal/stocks/screener/screener_helper.py b/openbb_terminal/stocks/screener/screener_helper.py new file mode 100644 index 00000000000..8878a802003 --- /dev/null +++ b/openbb_terminal/stocks/screener/screener_helper.py @@ -0,0 +1,34 @@ +from typing import List, Dict +from openbb_terminal.stocks.screener import finviz_view + + +def finviz_choices(section: str) -> List[str]: + """Generates the available choices for the given section. Formatted in an argparse friendly way + + Parameters + ---------- + section: str + The section to generate the choices for + + Returns + ------- + List[str] + The argparse friendly strings + """ + return [x.replace(" ", "").lower() for x in finviz_view.d_cols_to_sort[section]] + + +def finviz_map(section: str) -> Dict[str, str]: + """Generates a mapping of our custom formatted choices to the actual choices + + Parameters + ---------- + section: str + The section to generate the choices for + + Returns + ------- + Dict[str] + Keys are the arpgarse friendly version, values are the original version + """ + return {x.replace(" ", "").lower(): x for x in finviz_view.d_cols_to_sort[section]} diff --git a/openbb_terminal/stocks/screener/yahoofinance_model.py b/openbb_terminal/stocks/screener/yahoofinance_model.py index f80abe87176..ba2551e0fbc 100644 --- a/openbb_terminal/stocks/screener/yahoofinance_model.py +++ b/openbb_terminal/stocks/screener/yahoofinance_model.py @@ -111,9 +111,12 @@ def historical( l_stocks = screen.screener_view(verbose=0) limit_random_stocks = False - df_screener = pd.DataFrame() + final_screener = pd.DataFrame() if l_stocks: + if len(l_stocks) < 2: + console.print("Please select two or more tickers.\n") + return pd.DataFrame(), [], False if len(l_stocks) > limit: random.shuffle(l_stocks) l_stocks = sorted(l_stocks[:limit]) @@ -123,25 +126,29 @@ def historical( ) limit_random_stocks = True + selector = d_candle_types[type_candle] df_screener = yf.download( l_stocks, start=start_date, progress=False, threads=False - )[d_candle_types[type_candle]][l_stocks] - df_screener = df_screener[l_stocks] + )[selector] + clean_screener = df_screener[l_stocks] + final_screener = clean_screener[l_stocks] - if np.any(df_screener.isna()): - nan_tickers = df_screener.columns[df_screener.isna().sum() >= 1].to_list() + if np.any(final_screener.isna()): + nan_tickers = final_screener.columns[ + final_screener.isna().sum() >= 1 + ].to_list() console.print( f"NaN values found in: {', '.join(nan_tickers)}. Replacing with zeros." ) - df_screener = df_screener.fillna(0) + final_screener = final_screener.fillna(0) # This puts everything on 0-1 scale for visualizing if normalize: mm_scale = MinMaxScaler() - df_screener = pd.DataFrame( - mm_scale.fit_transform(df_screener), - columns=df_screener.columns, - index=df_screener.index, + final_screener = pd.DataFrame( + mm_scale.fit_transform(final_screener), + columns=final_screener.columns, + index=final_screener.index, ) - return df_screener, l_stocks, limit_random_stocks + return final_screener, l_stocks, limit_random_stocks diff --git a/openbb_terminal/stocks/screener/yahoofinance_view.py b/openbb_terminal/stocks/screener/yahoofinance_view.py index 6345cc5d6ca..03728b31427 100644 --- a/openbb_terminal/stocks/screener/yahoofinance_view.py +++ b/openbb_terminal/stocks/screener/yahoofinance_view.py @@ -64,6 +64,9 @@ def historical( preset_loaded, limit, start_date, type_candle, normalize ) + if df_screener.empty: + return [] + if l_stocks: # This plot has 1 axis if not external_axes: diff --git a/tests/openbb_terminal/stocks/fundamental_analysis/txt/test_av_view/test_display_fraud[df0].txt b/tests/openbb_terminal/stocks/fundamental_analysis/txt/test_av_view/test_display_fraud[df0].txt index 8b137891791..e69de29bb2d 100644 --- a/tests/openbb_terminal/stocks/fundamental_analysis/txt/test_av_view/test_display_fraud[df0].txt +++ b/tests/openbb_terminal/stocks/fundamental_analysis/txt/test_av_view/test_display_fraud[df0].txt @@ -1 +0,0 @@ - diff --git a/tests/openbb_terminal/stocks/fundamental_analysis/txt/test_av_view/test_dupont[df0].txt b/tests/openbb_terminal/stocks/fundamental_analysis/txt/test_av_view/test_dupont[df0].txt index 6166bbb294b..e69de29bb2d 100644 --- a/tests/openbb_terminal/stocks/fundamental_analysis/txt/test_av_view/test_dupont[df0].txt +++ b/tests/openbb_terminal/stocks/fundamental_analysis/txt/test_av_view/test_dupont[df0].txt @@ -1,2 +0,0 @@ -[red]Invalid response from AlphaVantage[/red] -