Refactor the QA Raw Command (#3234)

* Improved usability of raw

* Began refactoring qa raw

* Continued adding fixes

* Added more fixes

* Updated docs

* James fixes

* Added formtting changes

* In process of converting to all lower

* Added fixes

* Fixed tests
This commit is contained in:
Colin Delahunty
2022-11-02 18:35:55 +01:00
committed by GitHub
parent c7a51e0571
commit 500fcd6aa2
13 changed files with 219 additions and 153 deletions

View File

@@ -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,

View File

@@ -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,
)

View File

@@ -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"
)

View File

@@ -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,
)

View File

@@ -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):

View File

@@ -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,
)

View File

@@ -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,
)

View File

@@ -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 = {

View File

@@ -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 <ticker>'")
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 <ticker>'")
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]):

View File

@@ -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()

View File

@@ -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()

12
tests/test_helpers.py Normal file
View File

@@ -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

View File

@@ -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}