diff --git a/openbb_terminal/common/quantitative_analysis/qa_view.py b/openbb_terminal/common/quantitative_analysis/qa_view.py index 6ae9e70d70b..22fbf8ee506 100644 --- a/openbb_terminal/common/quantitative_analysis/qa_view.py +++ b/openbb_terminal/common/quantitative_analysis/qa_view.py @@ -845,7 +845,7 @@ def display_unitroot( def display_raw( data: pd.DataFrame, sortby: str = "", - descend: bool = False, + ascend: bool = False, limit: int = 20, export: str = "", ) -> None: @@ -857,7 +857,7 @@ def display_raw( DataFrame with historical information sortby : str The column to sort by - descend : bool + ascend : bool Whether to sort descending limit : int Number of rows to show @@ -871,13 +871,20 @@ def display_raw( df1 = data.copy() if sortby: - df1 = df1.sort_values( - by=sortby if sortby != "AdjClose" else "Adj Close", ascending=not descend - ) + try: + sort_col = [x.lower().replace(" ", "") for x in df1.columns].index( + sortby.lower().replace(" ", "") + ) + except ValueError: + console.print("[red]The provided column is not a valid option[/red]\n") + return + df1 = df1.sort_values(by=data.columns[sort_col], ascending=ascend) + else: + df1 = df1.sort_index(ascending=ascend) df1.index = [x.strftime("%Y-%m-%d") for x in df1.index] print_rich_table( - df1.head(limit) if sortby else df1.tail(limit), + df1.head(limit), headers=[x.title() if x != "" else "Date" for x in df1.columns], title="[bold]Raw Data[/bold]", show_index=True, diff --git a/openbb_terminal/cryptocurrency/quantitative_analysis/qa_controller.py b/openbb_terminal/cryptocurrency/quantitative_analysis/qa_controller.py index 81c311aee88..684e2aeafea 100644 --- a/openbb_terminal/cryptocurrency/quantitative_analysis/qa_controller.py +++ b/openbb_terminal/cryptocurrency/quantitative_analysis/qa_controller.py @@ -152,7 +152,13 @@ class QaController(CryptoBaseController): choices["raw"] = { "--limit": {str(c): {} for c in range(1, 100)}, "-l": "--limit", + "--sortby": { + c: {} + for c in [x.lower().replace(" ", "") for x in self.data.columns] + }, + "-s": "--sortby", "--descend": {}, + "--export": {c: {} for c in ["csv", "json", "xlsx"]}, } choices["decompose"] = { "--multiplicative": None, @@ -260,16 +266,24 @@ class QaController(CryptoBaseController): dest="descend", help="Sort in descending order", ) + parser.add_argument( + "-s", + "--sortby", + help="The column to sort by", + choices=[x.lower().replace(" ", "") for x in self.data.columns], + type=str.lower, + dest="sortby", + ) ns_parser = self.parse_known_args_and_warn( parser, other_args, export_allowed=EXPORT_ONLY_RAW_DATA_ALLOWED ) if ns_parser: qa_view.display_raw( - data=self.data[self.target], + data=self.data, limit=ns_parser.limit, - sortby="Close", - descend=ns_parser.descend, + sortby=ns_parser.sortby, + ascend=not ns_parser.descend, export=ns_parser.export, ) diff --git a/openbb_terminal/economy/economy_controller.py b/openbb_terminal/economy/economy_controller.py index c62dac87512..047f436bf13 100644 --- a/openbb_terminal/economy/economy_controller.py +++ b/openbb_terminal/economy/economy_controller.py @@ -1806,4 +1806,9 @@ class EconomyController(BaseController): for col in list(self.DATASETS[source].columns): data[col] = self.DATASETS[source][col].to_frame() - self.queue = self.load_class(QaController, data, self.queue) + if data: + self.queue = self.load_class(QaController, data, self.queue) + else: + console.print( + "[red]Please load a dataset before moving to the qa menu[/red]\n" + ) diff --git a/openbb_terminal/economy/quantitative_analysis/qa_controller.py b/openbb_terminal/economy/quantitative_analysis/qa_controller.py index 0bc2924730e..e1c996aec62 100644 --- a/openbb_terminal/economy/quantitative_analysis/qa_controller.py +++ b/openbb_terminal/economy/quantitative_analysis/qa_controller.py @@ -136,6 +136,8 @@ class QaController(StockBaseController): choices["raw"] = { "--limit": {str(c): {} for c in range(1, 100)}, "-l": "--limit", + "--sortby": {}, + "-s": "--sortby", "--descend": {}, } choices["decompose"] = { @@ -252,16 +254,27 @@ class QaController(StockBaseController): dest="descend", help="Sort in descending order", ) + parser.add_argument( + "-s", + "--sortby", + help="The column to sort by", + type=str.lower, + dest="sortby", + ) ns_parser = self.parse_known_args_and_warn( parser, other_args, export_allowed=EXPORT_ONLY_RAW_DATA_ALLOWED ) + if isinstance(self.data, pd.Series): + data = self.data.to_frame() + else: + data = self.data if ns_parser: qa_view.display_raw( - data=self.data, + data=data, limit=ns_parser.limit, - sortby="", - descend=ns_parser.descend, + sortby=ns_parser.sortby, + ascend=not ns_parser.descend, export=ns_parser.export, ) diff --git a/openbb_terminal/etf/etf_controller.py b/openbb_terminal/etf/etf_controller.py index f0b0b5b5a12..c86648e5be3 100644 --- a/openbb_terminal/etf/etf_controller.py +++ b/openbb_terminal/etf/etf_controller.py @@ -70,14 +70,14 @@ class ETFController(BaseController): "disc", ] CANDLE_COLUMNS = [ - "AdjClose", - "Open", - "Close", - "High", - "Low", - "Volume", - "Returns", - "LogRet", + "adjclose", + "open", + "close", + "high", + "low", + "volume", + "returns", + "logret", ] PATH = "/etf/" @@ -479,18 +479,19 @@ class ETFController(BaseController): ) parser.add_argument( "--sort", + "-s", choices=self.CANDLE_COLUMNS, default="", - type=str, + type=str.lower, dest="sort", help="Choose a column to sort by", ) parser.add_argument( "-d", "--descending", - action="store_false", + action="store_true", dest="descending", - default=True, + default=False, help="Sort selected column descending", ) parser.add_argument( @@ -531,54 +532,54 @@ class ETFController(BaseController): parser, other_args, EXPORT_BOTH_RAW_DATA_AND_FIGURES ) if ns_parser: - if self.etf_name: - if ns_parser.raw: - qa_view.display_raw( - data=self.etf_data, - sortby=ns_parser.sort, - descend=ns_parser.descending, - limit=ns_parser.num, - ) - - else: - - data = stocks_helper.process_candle(self.etf_data) - mov_avgs = [] - - if ns_parser.mov_avg: - mov_list = (num for num in ns_parser.mov_avg.split(",")) - - for num in mov_list: - try: - num = int(num) - - if num <= 1: - raise ValueError - - mov_avgs.append(num) - except ValueError: - console.print( - f"[red]{num} is not a valid moving average, must be an integer greater than 1." - ) - - stocks_helper.display_candle( - symbol=self.etf_name, - data=data, - use_matplotlib=ns_parser.plotly, - intraday=False, - add_trend=ns_parser.trendlines, - ma=mov_avgs, - asset_type="ETF", - ) - - export_data( - ns_parser.export, - os.path.dirname(os.path.abspath(__file__)), - f"{self.etf_name}", - self.etf_data, - ) - else: + if not self.etf_name: console.print("No ticker loaded. First use `load {ticker}`\n") + return + if ns_parser.raw: + qa_view.display_raw( + data=self.etf_data, + sortby=ns_parser.sort, + ascend=not ns_parser.descending, + limit=ns_parser.num, + ) + + else: + data = stocks_helper.process_candle(self.etf_data) + mov_avgs = [] + + if ns_parser.mov_avg: + mov_list = (num for num in ns_parser.mov_avg.split(",")) + + for num in mov_list: + try: + num = int(num) + + if num <= 1: + raise ValueError + + mov_avgs.append(num) + except ValueError: + console.print( + f"[red]{num} is not a valid moving average, must be an integer " + "greater than 1.[/red]\n" + ) + + stocks_helper.display_candle( + symbol=self.etf_name, + data=data, + use_matplotlib=ns_parser.plotly, + intraday=False, + add_trend=ns_parser.trendlines, + ma=mov_avgs, + asset_type="ETF", + ) + + export_data( + ns_parser.export, + os.path.dirname(os.path.abspath(__file__)), + f"{self.etf_name}", + self.etf_data, + ) @log_start_end(log=logger) def call_pir(self, other_args): diff --git a/openbb_terminal/forex/quantitative_analysis/qa_controller.py b/openbb_terminal/forex/quantitative_analysis/qa_controller.py index de1b7b3ed22..96120d8c544 100644 --- a/openbb_terminal/forex/quantitative_analysis/qa_controller.py +++ b/openbb_terminal/forex/quantitative_analysis/qa_controller.py @@ -153,6 +153,11 @@ class QaController(CryptoBaseController): choices["raw"] = { "--limit": {str(c): {} for c in range(1, 100)}, "-l": "--limit", + "--sortby": { + c: {} + for c in [x.lower().replace(" ", "") for x in self.data.columns] + }, + "-s": "--sortby", "--descend": {}, } choices["decompose"] = { @@ -266,16 +271,24 @@ class QaController(CryptoBaseController): dest="descend", help="Sort in descending order", ) + parser.add_argument( + "-s", + "--sortby", + help="The column to sort by", + type=str.lower, + choices=[x.lower().replace(" ", "") for x in self.data.columns], + dest="sortby", + ) ns_parser = self.parse_known_args_and_warn( parser, other_args, export_allowed=EXPORT_ONLY_RAW_DATA_ALLOWED ) if ns_parser: qa_view.display_raw( - data=self.data[self.target], + data=self.data, limit=ns_parser.limit, - sortby="", - descend=ns_parser.descend, + sortby=ns_parser.sortby, + ascend=not ns_parser.descend, export=ns_parser.export, ) diff --git a/openbb_terminal/stocks/quantitative_analysis/qa_controller.py b/openbb_terminal/stocks/quantitative_analysis/qa_controller.py index 01c542006df..bb332b286e3 100644 --- a/openbb_terminal/stocks/quantitative_analysis/qa_controller.py +++ b/openbb_terminal/stocks/quantitative_analysis/qa_controller.py @@ -27,6 +27,7 @@ from openbb_terminal.rich_config import console, MenuText, get_ordered_list_sour from openbb_terminal.stocks.quantitative_analysis.beta_view import beta_view from openbb_terminal.stocks.quantitative_analysis.factors_view import capm_view from openbb_terminal.stocks.quantitative_analysis.qa_model import full_stock_df +from openbb_terminal.stocks import stocks_helper # pylint: disable=C0302 @@ -174,6 +175,10 @@ class QaController(StockBaseController): "--limit": {str(c): {} for c in range(1, 100)}, "-l": "--limit", "--descend": {}, + "-d": "--descend", + "--export": {x: {} for x in ["csv", "json", "xlsx"]}, + "--sortby": {c.lower(): {} for c in stocks_helper.CANDLE_SORT}, + "-s": "--sortby", } choices["decompose"] = { "--multiplicative": None, @@ -334,16 +339,24 @@ class QaController(StockBaseController): dest="descend", help="Sort in descending order", ) + parser.add_argument( + "-s", + "--sortby", + help="The column to sort by", + choices=[x.lower().replace(" ", "") for x in self.stock.columns], + type=str.lower, + dest="sortby", + ) ns_parser = self.parse_known_args_and_warn( parser, other_args, export_allowed=EXPORT_ONLY_RAW_DATA_ALLOWED ) if ns_parser: qa_view.display_raw( - data=self.stock[self.target], + data=self.stock, limit=ns_parser.limit, - sortby="", - descend=ns_parser.descend, + sortby=ns_parser.sortby, + ascend=not ns_parser.descend, export=ns_parser.export, ) diff --git a/openbb_terminal/stocks/stock_statics.py b/openbb_terminal/stocks/stock_statics.py index 36b5af791c0..f6f6f936f4d 100644 --- a/openbb_terminal/stocks/stock_statics.py +++ b/openbb_terminal/stocks/stock_statics.py @@ -1,14 +1,14 @@ INTERVALS = [1, 5, 15, 30, 60] SOURCES = ["YahooFinance", "AlphaVantage", "IEXCloud", "EODHD"] CANDLE_SORT = [ - "AdjClose", - "Open", - "Close", - "High", - "Low", - "Volume", - "Returns", - "LogRet", + "adjclose", + "open", + "close", + "high", + "low", + "volume", + "returns", + "logret", ] market_coverage_suffix = { diff --git a/openbb_terminal/stocks/stocks_controller.py b/openbb_terminal/stocks/stocks_controller.py index ff6826a7681..6cef8807528 100644 --- a/openbb_terminal/stocks/stocks_controller.py +++ b/openbb_terminal/stocks/stocks_controller.py @@ -407,16 +407,16 @@ class StocksController(StockBaseController): "--sort", choices=stocks_helper.CANDLE_SORT, default="", - type=str, + type=str.lower, dest="sort", help="Choose a column to sort by. Only works when raw data is displayed.", ) parser.add_argument( "-d", "--descending", - action="store_false", + action="store_true", dest="descending", - default=True, + default=False, help="Sort selected column descending. Only works when raw data is displayed.", ) parser.add_argument( @@ -458,69 +458,53 @@ class StocksController(StockBaseController): limit=20, ) if ns_parser: - if self.ticker: - if ns_parser.sort and not self.stock.empty: - sort = ( - ns_parser.sort if ns_parser.sort != "AdjClose" else "Adj Close" - ) - if sort not in self.stock.columns: - col_names_no_spaces = [ - "'" + col.replace(" ", "") + "'" - for col in self.stock.columns - ] - console.print( - f"candle: error: argument --sort: invalid choice: '{sort}' for the source chosen " - f"(choose from {(', '.join(list(col_names_no_spaces)))})" - ) - return - - if ns_parser.raw: - qa_view.display_raw( - data=self.stock, - sortby=ns_parser.sort, - descend=ns_parser.descending, - limit=ns_parser.limit, - ) - - else: - data = stocks_helper.process_candle(self.stock) - mov_avgs = [] - - if ns_parser.mov_avg: - mov_list = (num for num in ns_parser.mov_avg.split(",")) - - for num in mov_list: - try: - num = int(num) - - if num <= 1: - raise ValueError - - mov_avgs.append(num) - except ValueError: - console.print( - f"[red]{num} is not a valid moving average, must be an integer greater than 1." - ) - - stocks_helper.display_candle( - symbol=self.ticker, - data=data, - use_matplotlib=ns_parser.plotly, - intraday=self.interval != "1440min", - add_trend=ns_parser.trendlines, - ma=mov_avgs, - yscale="log" if ns_parser.logy else "linear", - ) - - export_data( - ns_parser.export, - os.path.dirname(os.path.abspath(__file__)), - f"{self.ticker}", - self.stock, + if not self.ticker: + console.print("No ticker loaded. First use 'load '") + return + if ns_parser.raw: + qa_view.display_raw( + data=self.stock, + sortby=ns_parser.sort, + ascend=not ns_parser.descending, + limit=ns_parser.limit, ) else: - console.print("No ticker loaded. First use 'load '") + data = stocks_helper.process_candle(self.stock) + mov_avgs = [] + + if ns_parser.mov_avg: + mov_list = (num for num in ns_parser.mov_avg.split(",")) + + for num in mov_list: + try: + num = int(num) + + if num <= 1: + raise ValueError + + mov_avgs.append(num) + except ValueError: + console.print( + f"[red]{num} is not a valid moving average, must be an integer greater than 1." + ) + + stocks_helper.display_candle( + symbol=self.ticker, + data=data, + use_matplotlib=ns_parser.plotly, + intraday=self.interval != "1440min", + add_trend=ns_parser.trendlines, + ma=mov_avgs, + yscale="log" if ns_parser.logy else "linear", + ) + + export_data( + ns_parser.export, + os.path.dirname(os.path.abspath(__file__)), + f"{self.ticker}", + self.stock, + ) @log_start_end(log=logger) def call_news(self, other_args: List[str]): diff --git a/tests/openbb_terminal/forex/quantitative_analysis/test_qa_controller.py b/tests/openbb_terminal/forex/quantitative_analysis/test_qa_controller.py index d7c345fdd0b..4e96ea0ff3f 100644 --- a/tests/openbb_terminal/forex/quantitative_analysis/test_qa_controller.py +++ b/tests/openbb_terminal/forex/quantitative_analysis/test_qa_controller.py @@ -7,6 +7,7 @@ import pytest # IMPORTATION INTERNAL from openbb_terminal.forex.quantitative_analysis import qa_controller +from tests.test_helpers import no_dfs DF_PAIR = pd.DataFrame.from_dict( @@ -311,10 +312,10 @@ def test_call_func_expect_queue(expected_queue, queue, func): "qa_view.display_raw", [], dict( - data=QA_CONTROLLER.data[QA_CONTROLLER.target], + data=QA_CONTROLLER.data, limit=1, sortby="", - descend=True, + ascend=True, export="csv", ), ), @@ -516,7 +517,7 @@ def test_call_func( getattr(QA_CONTROLLER, tested_func)(other_args=other_args) - if called_args or called_kwargs: + if called_args or called_kwargs and no_dfs(called_args, called_kwargs): mock.assert_called_once_with(*called_args, **called_kwargs) else: mock.assert_called_once() diff --git a/tests/openbb_terminal/stocks/test_stocks_controller.py b/tests/openbb_terminal/stocks/test_stocks_controller.py index 6f59019b3a0..3c716bfdb00 100644 --- a/tests/openbb_terminal/stocks/test_stocks_controller.py +++ b/tests/openbb_terminal/stocks/test_stocks_controller.py @@ -7,6 +7,7 @@ import pytest # IMPORTATION INTERNAL from openbb_terminal.stocks import stocks_controller +from tests.test_helpers import no_dfs # pylint: disable=E1101 # pylint: disable=W0603 @@ -294,7 +295,7 @@ def test_call_func_expect_queue(expected_queue, func, queue): dict( data=EMPTY_DF, sortby="Open", - descend=False, + ascend=False, limit=1, ), ), @@ -463,7 +464,7 @@ def test_call_func( getattr(controller, tested_func)(other_args) - if called_args or called_kwargs: + if called_args or called_kwargs and no_dfs(called_args, called_kwargs): mock.assert_called_once_with(*called_args, **called_kwargs) else: mock.assert_called_once() diff --git a/tests/test_helpers.py b/tests/test_helpers.py new file mode 100644 index 00000000000..7820199cf70 --- /dev/null +++ b/tests/test_helpers.py @@ -0,0 +1,12 @@ +import pandas as pd + + +def no_dfs(args: list, kwargs: dict) -> bool: + """Returns false if there are in dataframe objects given""" + for item in args: + if isinstance(item, pd.DataFrame): + return False + for item in kwargs.values(): + if isinstance(item, pd.DataFrame): + return False + return True diff --git a/website/content/terminal/common/qa/raw/_index.md b/website/content/terminal/common/qa/raw/_index.md index 1b5013faa61..228cd27e1ea 100755 --- a/website/content/terminal/common/qa/raw/_index.md +++ b/website/content/terminal/common/qa/raw/_index.md @@ -7,6 +7,8 @@ Shows historical daily value for the target value. ``` optional arguments: -n NUM, --num NUM Number to show (default: 20) + -s SORTBY, --sortby SORTBY + The column to sort by (default: None) -d, --descend Sort in descending order (default: False) -h, --help show this help message (default: False) --export {csv,json,xlsx}