mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-11-08 02:11:44 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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]):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
12
tests/test_helpers.py
Normal 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
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user