Screener controller big improvements (#1007)

* massively improve screener controller

* Add limit of stocks to display
This commit is contained in:
DidierRLopes
2021-12-01 23:07:33 +00:00
committed by GitHub
parent a616764601
commit 233d9411f3
4 changed files with 359 additions and 285 deletions

View File

@@ -14,99 +14,6 @@ presets_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "presets
# pylint: disable=C0302
def get_screener_data(
preset_loaded: str, data_type: str, signal: str, limit: int, ascend: bool
):
"""Screener Overview
Parameters
----------
preset_loaded : str
Loaded preset filter
data_type : str
Data type between: overview, valuation, financial, ownership, performance, technical
signal : str
Signal to use to filter data
limit : int
Limit of stocks filtered with presets to print
ascend : bool
Ascended order of stocks filtered to print
Returns
----------
pd.DataFrame
Dataframe with loaded filtered stocks
"""
preset_filter = configparser.RawConfigParser()
preset_filter.optionxform = str # type: ignore
preset_filter.read(presets_path + preset_loaded + ".ini")
d_general = preset_filter["General"]
d_filters = {
**preset_filter["Descriptive"],
**preset_filter["Fundamental"],
**preset_filter["Technical"],
}
for section in ["General", "Descriptive", "Fundamental", "Technical"]:
for key, val in {**preset_filter[section]}.items():
if key not in d_check_screener:
print(f"The screener variable {section}.{key} shouldn't exist!\n")
return pd.DataFrame()
if val not in d_check_screener[key]:
print(
f"Invalid [{section}] {key}={val}. "
f"Choose one of the following options:\n{', '.join(d_check_screener[key])}.\n"
)
return pd.DataFrame()
d_filters = {k: v for k, v in d_filters.items() if v}
if data_type == "overview":
screen = overview.Overview()
elif data_type == "valuation":
screen = valuation.Valuation()
elif data_type == "financial":
screen = financial.Financial()
elif data_type == "ownership":
screen = ownership.Ownership()
elif data_type == "performance":
screen = performance.Performance()
elif data_type == "technical":
screen = technical.Technical()
else:
print("Invalid selected screener type")
return pd.DataFrame()
if signal:
screen.set_filter(signal=d_signals[signal])
else:
if "Signal" in d_general:
screen.set_filter(filters_dict=d_filters, signal=d_general["Signal"])
else:
screen.set_filter(filters_dict=d_filters)
if "Order" in d_general:
if limit > 0:
df_screen = screen.ScreenerView(
order=d_general["Order"],
limit=limit,
ascend=ascend,
)
else:
df_screen = screen.ScreenerView(order=d_general["Order"], ascend=ascend)
else:
if limit > 0:
df_screen = screen.ScreenerView(limit=limit, ascend=ascend)
else:
df_screen = screen.ScreenerView(ascend=ascend)
return df_screen
d_signals = {
"top_gainers": "Top Gainers",
"top_losers": "Top Losers",
@@ -143,6 +50,135 @@ d_signals = {
"head_shoulders_inverse": "Head & Shoulders Inverse",
}
def get_screener_data(preset_loaded: str, data_type: str, limit: int, ascend: bool):
"""Screener Overview
Parameters
----------
preset_loaded : str
Loaded preset filter
data_type : str
Data type between: overview, valuation, financial, ownership, performance, technical
limit : int
Limit of stocks filtered with presets to print
ascend : bool
Ascended order of stocks filtered to print
Returns
----------
pd.DataFrame
Dataframe with loaded filtered stocks
"""
if data_type == "overview":
screen = overview.Overview()
elif data_type == "valuation":
screen = valuation.Valuation()
elif data_type == "financial":
screen = financial.Financial()
elif data_type == "ownership":
screen = ownership.Ownership()
elif data_type == "performance":
screen = performance.Performance()
elif data_type == "technical":
screen = technical.Technical()
else:
print("Invalid selected screener type")
return pd.DataFrame()
if preset_loaded in list(d_signals.keys()):
screen.set_filter(signal=d_signals[preset_loaded])
if limit > 0:
df_screen = screen.ScreenerView(limit=limit, ascend=ascend)
else:
df_screen = screen.ScreenerView(ascend=ascend)
else:
preset_filter = configparser.RawConfigParser()
preset_filter.optionxform = str # type: ignore
preset_filter.read(presets_path + preset_loaded + ".ini")
d_general = preset_filter["General"]
d_filters = {
**preset_filter["Descriptive"],
**preset_filter["Fundamental"],
**preset_filter["Technical"],
}
for section in ["General", "Descriptive", "Fundamental", "Technical"]:
for key, val in {**preset_filter[section]}.items():
if key not in d_check_screener:
print(f"The screener variable {section}.{key} shouldn't exist!\n")
return pd.DataFrame()
if val not in d_check_screener[key]:
print(
f"Invalid [{section}] {key}={val}. "
f"Choose one of the following options:\n{', '.join(d_check_screener[key])}.\n"
)
return pd.DataFrame()
d_filters = {k: v for k, v in d_filters.items() if v}
screen.set_filter(filters_dict=d_filters)
if "Order" in d_general:
if limit > 0:
df_screen = screen.ScreenerView(
order=d_general["Order"],
limit=limit,
ascend=ascend,
)
else:
df_screen = screen.ScreenerView(order=d_general["Order"], ascend=ascend)
else:
if limit > 0:
df_screen = screen.ScreenerView(limit=limit, ascend=ascend)
else:
df_screen = screen.ScreenerView(ascend=ascend)
return df_screen
d_signals_desc = {
"top_gainers": "stocks with the highest %% price gain today",
"top_losers": "stocks with the highest %% price loss today",
"new_high": "stocks making 52-week high today",
"new_low": "stocks making 52-week low today",
"most_volatile": "stocks with the highest widest high/low trading range today",
"most_active": "stocks with the highest trading volume today",
"unusual_volume": "stocks with unusually high volume today - the highest relative volume ratio",
"overbought": "stock is becoming overvalued and may experience a pullback.",
"oversold": "oversold stocks may represent a buying opportunity for investors",
"downgrades": "stocks downgraded by analysts today",
"upgrades": "stocks upgraded by analysts today",
"earnings_before": "companies reporting earnings today, before market open",
"earnings_after": "companies reporting earnings today, after market close",
"recent_insider_buying": "stocks with recent insider buying activity",
"recent_insider_selling": "stocks with recent insider selling activity",
"major_news": "stocks with the highest news coverage today",
"horizontal_sr": "horizontal channel of price range between support and resistance trendlines",
"tl_resistance": "once a rising trendline is broken",
"tl_support": "once a falling trendline is broken",
"wedge_up": "upward trendline support and upward trendline resistance (reversal)",
"wedge_down": "downward trendline support and downward trendline resistance (reversal)",
"wedge": "upward trendline support, downward trendline resistance (contiunation)",
"triangle_ascending": "upward trendline support and horizontal trendline resistance",
"triangle_descending": "horizontal trendline support and downward trendline resistance",
"channel_up": "both support and resistance trendlines slope upward",
"channel_down": "both support and resistance trendlines slope downward",
"channel": "both support and resistance trendlines are horizontal",
"double_top": "stock with 'M' shape that indicates a bearish reversal in trend",
"double_bottom": "stock with 'W' shape that indicates a bullish reversal in trend",
"multiple_top": "same as double_top hitting more highs",
"multiple_bottom": "same as double_bottom hitting more lows",
"head_shoulders": "chart formation that predicts a bullish-to-bearish trend reversal",
"head_shoulders_inverse": "chart formation that predicts a bearish-to-bullish trend reversal",
}
d_check_screener = {
"Order": [
"Any",

View File

@@ -4,19 +4,126 @@ __docformat__ = "numpy"
import argparse
from typing import List
import os
from datetime import datetime
import difflib
from tabulate import tabulate
import pandas as pd
from gamestonk_terminal.helper_funcs import (
parse_known_args_and_warn,
check_positive,
try_except,
export_data,
)
from gamestonk_terminal.stocks.screener.finviz_model import (
d_signals,
presets_path,
get_screener_data,
)
from gamestonk_terminal import feature_flags as gtff
d_cols_to_sort = {
"overview": [
"Ticker",
"Company",
"Sector",
"Industry",
"Country",
"Market Cap",
"P/E",
"Price",
"Change",
"Volume",
],
"valuation": [
"Ticker",
"Market Cap",
"P/E",
"Fwd P/E",
"PEG",
"P/S",
"P/B",
"P/C",
"P/FCF",
"EPS this Y",
"EPS next Y",
"EPS past 5Y",
"EPS next 5Y",
"Sales past 5Y",
"Price",
"Change",
"Volume",
],
"financial": [
"Ticker",
"Market Cap",
"Dividend",
"ROA",
"ROE",
"ROI",
"Curr R",
"Quick R",
"LTDebt/Eq",
"Debt/Eq",
"Gross M",
"Oper M",
"Profit M",
"Earnings",
"Price",
"Change",
"Volume",
],
"ownership": [
"Ticker",
"Market Cap",
"Outstanding",
"Float",
"Insider Own",
"Insider Trans",
"Inst Own",
"Inst Trans",
"Float Short",
"Short Ratio",
"Avg Volume",
"Price",
"Change",
"Volume",
],
"performance": [
"Ticker",
"Perf Week",
"Perf Month",
"Perf Quart",
"Perf Half",
"Perf Year",
"Perf YTD",
"Volatility W",
"Volatility M",
"Recom",
"Avg Volume",
"Rel Volume",
"Price",
"Change",
"Volume",
],
"technical": [
"Ticker",
"Beta",
"ATR",
"SMA20",
"SMA50",
"SMA200",
"52W High",
"52W Low",
"RSI",
"Price",
"Change",
"from Open",
"Gap",
"Volume",
],
}
@try_except
def screener(other_args: List[str], loaded_preset: str, data_type: str) -> List[str]:
"""Screener
@@ -56,17 +163,8 @@ def screener(other_args: List[str], loaded_preset: str, data_type: str) -> List[
preset.split(".")[0]
for preset in os.listdir(presets_path)
if preset[-4:] == ".ini"
],
)
parser.add_argument(
"-s",
"--signal",
action="store",
dest="signal",
type=str,
default=None,
help="Signal",
choices=list(d_signals.keys()),
]
+ list(d_signals.keys()),
)
parser.add_argument(
"-l",
@@ -74,7 +172,7 @@ def screener(other_args: List[str], loaded_preset: str, data_type: str) -> List[
action="store",
dest="limit",
type=check_positive,
default=0,
default=10,
help="Limit of stocks to print",
)
parser.add_argument(
@@ -86,147 +184,91 @@ def screener(other_args: List[str], loaded_preset: str, data_type: str) -> List[
help="Set order to Ascend, the default is Descend",
)
parser.add_argument(
"-e",
"-s",
"--sort",
action="store",
dest="sort",
default="",
nargs="+",
help=f"Sort elements of the table. Use {', '.join(d_cols_to_sort[data_type])}",
)
parser.add_argument(
"--export",
action="store_true",
dest="exportFile",
help="Save list as a text file",
choices=["csv", "json", "xlsx"],
default="",
dest="export",
help="Export dataframe data to csv,json,xlsx file",
)
try:
ns_parser = parse_known_args_and_warn(parser, other_args)
if not ns_parser:
ns_parser = parse_known_args_and_warn(parser, other_args)
if not ns_parser:
return []
df_screen = get_screener_data(
ns_parser.preset,
data_type,
0,
ns_parser.ascend,
)
if isinstance(df_screen, pd.DataFrame):
if df_screen.empty:
return []
df_screen = get_screener_data(
ns_parser.preset,
df_screen = df_screen.dropna(axis="columns", how="all")
if ns_parser.sort:
if " ".join(ns_parser.sort) in d_cols_to_sort[data_type]:
df_screen = df_screen.sort_values(
by=[" ".join(ns_parser.sort)],
ascending=ns_parser.ascend,
na_position="last",
)
else:
similar_cmd = difflib.get_close_matches(
" ".join(ns_parser.sort),
d_cols_to_sort[data_type],
n=1,
cutoff=0.7,
)
if similar_cmd:
print(
f"Replacing '{' '.join(ns_parser.sort)}' by '{similar_cmd[0]}' so table can be sorted."
)
df_screen = df_screen.sort_values(
by=[similar_cmd[0]],
ascending=ns_parser.ascend,
na_position="last",
)
else:
print(
f"Wrong sort column provided! Provide one of these: {', '.join(d_cols_to_sort[data_type])}"
)
df_screen = df_screen.fillna("")
if gtff.USE_TABULATE_DF:
print(
tabulate(
df_screen.head(n=ns_parser.limit),
headers=df_screen.columns,
floatfmt=".2f",
showindex=False,
tablefmt="fancy_grid",
),
)
else:
print(df_screen.head(n=ns_parser.limit).to_string())
print("")
export_data(
ns_parser.export,
os.path.dirname(os.path.abspath(__file__)),
data_type,
ns_parser.signal,
ns_parser.limit,
ns_parser.ascend,
df_screen,
)
if isinstance(df_screen, pd.DataFrame):
if df_screen.empty:
return []
return list(df_screen.head(n=ns_parser.limit)["Ticker"].values)
print(df_screen.to_string())
print("")
if ns_parser.exportFile:
now = datetime.now()
if not os.path.exists("reports/screener"):
os.makedirs("reports/screener")
with open(
f"reports/screener/{ns_parser.signal}-{now.strftime('%Y-%m-%d_%H:%M:%S')}",
"w",
) as file:
file.write(df_screen.to_string(index=False) + "\n")
return list(df_screen["Ticker"].values)
print("")
return []
except Exception as e:
print(e, "\n")
return []
def view_signals(other_args: List[str]):
"""View list of available signals
Parameters
----------
other_args : List[str]
Command line arguments to be processed with argparse
"""
parser = argparse.ArgumentParser(
add_help=False,
prog="signals",
description="""
Prints list of available signals. [Source: Finviz]
""",
)
try:
ns_parser = parse_known_args_and_warn(parser, other_args)
if not ns_parser:
return
print("top_gainers stocks with the highest %% price gain today")
print("top_losers stocks with the highest %% price loss today.")
print("new_high stocks making 52-week high today")
print("new_low stocks making 52-week low today")
print(
"most_volatile stocks with the highest widest high/low trading range today"
)
print("most_active stocks with the highest trading volume today")
print(
"unusual_volume stocks with unusually high volume today - the highest relative volume ratio"
)
print(
"overbought stock is becoming overvalued and may experience a pullback."
)
print(
"oversold oversold stocks may represent a buying opportunity for investors."
)
print("downgrades stocks downgraded by analysts today")
print("upgrades stocks upgraded by analysts today.")
print(
"earnings_before companies reporting earnings today, before market open"
)
print(
"earnings_after companies reporting earnings today, after market close"
)
print("recent_insider_buying stocks with recent insider buying activity")
print("recent_insider_selling stocks with recent insider selling activity")
print("major_news stocks with the highest news coverage today")
print(
"horizontal_sr horizontal channel of price range between support and resistance trendlines"
)
print("tl_resistance once a rising trendline is broken")
print("tl_support once a falling trendline is broken")
print(
"wedge_up upward trendline support and upward trendline resistance (reversal)"
)
print(
"wedge_down downward trendline support and downward trendline resistance (reversal)"
)
print(
"wedge upward trendline support, downward trendline resistance (contiunation)"
)
print(
"triangle_ascending upward trendline support and horizontal trendline resistance"
)
print(
"triangle_descending horizontal trendline support and downward trendline resistance"
)
print(
"channel_up both support and resistance trendlines slope upward"
)
print(
"channel_down both support and resistance trendlines slope downward"
)
print(
"channel both support and resistance trendlines are horizontal"
)
print(
"double_top stock with 'M' shape that indicates a bearish reversal in trend"
)
print(
"double_bottom stock with 'W' shape that indicates a bullish reversal in trend"
)
print("multiple_top same as double_top hitting more highs")
print("multiple_bottom same as double_bottom hitting more lows")
print(
"head_shoulders chart formation that predicts a bullish-to-bearish trend reversal"
)
print(
"head_shoulders_inverse chart formation that predicts a bearish-to-bullish trend reversal"
)
print("")
except Exception as e:
print(e)
print("")
return
print("")
return []

View File

@@ -17,7 +17,11 @@ from gamestonk_terminal.helper_funcs import (
)
from gamestonk_terminal.menu import session
from gamestonk_terminal.portfolio.portfolio_optimization import po_controller
from gamestonk_terminal.stocks.screener import finviz_view, yahoofinance_view
from gamestonk_terminal.stocks.screener import (
finviz_view,
yahoofinance_view,
finviz_model,
)
presets_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "presets/")
@@ -43,13 +47,12 @@ class ScreenerController:
"ownership",
"performance",
"technical",
"signals",
"po",
]
def __init__(self):
"""Constructor"""
self.preset = "template"
self.preset = "top_gainers"
self.screen_tickers = []
self.scr_parser = argparse.ArgumentParser(add_help=False, prog="scr")
self.scr_parser.add_argument(
@@ -65,7 +68,7 @@ class ScreenerController:
print(" q quit this menu, and shows back to main menu")
print(" quit quit to abandon program")
print("")
print(" view view available presets")
print(" view view available presets (defaults and customs)")
print(" set set one of the available presets")
print("")
print(f"PRESET: {self.preset}")
@@ -78,8 +81,6 @@ class ScreenerController:
print(" performance performance (e.g. Perf Week, Perf YTD, Volatility M)")
print(" technical technical (e.g. Beta, SMA50, 52W Low, RSI, Change)")
print("")
print(" signals view filter signals (e.g. -s top_gainers)")
print("")
if self.screen_tickers:
print(f"Last screened tickers: {', '.join(self.screen_tickers)}")
print("")
@@ -142,6 +143,7 @@ class ScreenerController:
if preset[-4:] == ".ini"
]
print("\nCustom Presets:")
for preset in presets:
with open(
presets_path + preset + ".ini",
@@ -152,8 +154,13 @@ class ScreenerController:
if line.strip() == "[General]":
break
description += line.strip()
print(f"\nPRESET: {preset}")
print(description.split("Description: ")[1].replace("#", ""))
print(
f" {preset}{(50-len(preset)) * ' '}{description.split('Description: ')[1].replace('#', '')}"
)
print("\nDefault Presets:")
for signame, sigdesc in finviz_model.d_signals_desc.items():
print(f" {signame}{(50-len(signame)) * ' '}{sigdesc}")
print("")
except Exception as e:
@@ -178,7 +185,8 @@ class ScreenerController:
preset.split(".")[0]
for preset in os.listdir(presets_path)
if preset[-4:] == ".ini"
],
]
+ list(finviz_model.d_signals.keys()),
)
try:
@@ -280,10 +288,6 @@ class ScreenerController:
"""Process technical command"""
self.screen_tickers = finviz_view.screener(other_args, self.preset, "technical")
def call_signals(self, other_args: List[str]):
"""Process signals command"""
finviz_view.view_signals(other_args)
def call_po(self, _):
"""Call the portfolio optimization menu with selected tickers"""
return po_controller.menu(self.screen_tickers)

View File

@@ -18,7 +18,7 @@ from gamestonk_terminal.helper_funcs import (
plot_autoscale,
valid_date,
)
from gamestonk_terminal.stocks.screener import finviz_view
from gamestonk_terminal.stocks.screener import finviz_model
register_matplotlib_converters()
@@ -76,40 +76,30 @@ def historical(other_args: List[str], preset_loaded: str) -> List[str]:
default="a", # in case it's adjusted close
help=("type of candles: o-open, h-high, l-low, c-close, a-adjusted close."),
)
parser.add_argument(
"-s",
"--signal",
action="store",
dest="signal",
type=str,
default=None,
help="Signal",
choices=list(finviz_view.d_signals.keys()),
)
try:
ns_parser = parse_known_args_and_warn(parser, other_args)
if not ns_parser:
return list()
preset_filter = configparser.RawConfigParser()
preset_filter.optionxform = str # type: ignore
preset_filter.read(presets_path + preset_loaded + ".ini")
d_general = preset_filter["General"]
d_filters = {
**preset_filter["Descriptive"],
**preset_filter["Fundamental"],
**preset_filter["Technical"],
}
d_filters = {k: v for k, v in d_filters.items() if v}
screen = ticker.Ticker()
if preset_loaded in list(finviz_model.d_signals.keys()):
screen.set_filter(signal=finviz_model.d_signals[preset_loaded])
if ns_parser.signal:
screen.set_filter(signal=finviz_view.d_signals[ns_parser.signal])
else:
preset_filter = configparser.RawConfigParser()
preset_filter.optionxform = str # type: ignore
preset_filter.read(presets_path + preset_loaded + ".ini")
d_general = preset_filter["General"]
d_filters = {
**preset_filter["Descriptive"],
**preset_filter["Fundamental"],
**preset_filter["Technical"],
}
d_filters = {k: v for k, v in d_filters.items() if v}
if d_general["Signal"]:
screen.set_filter(filters_dict=d_filters, signal=d_general["Signal"])
else:
@@ -117,9 +107,8 @@ def historical(other_args: List[str], preset_loaded: str) -> List[str]:
l_min = []
l_leg = []
plt.figure(figsize=plot_autoscale(), dpi=PLOT_DPI)
l_stocks = screen.ScreenerView(verbose=0)
limit_random_stocks = False
if len(l_stocks) > 10:
print(
@@ -129,6 +118,9 @@ def historical(other_args: List[str], preset_loaded: str) -> List[str]:
random.shuffle(l_stocks)
l_stocks = sorted(l_stocks[:10])
print(", ".join(l_stocks))
limit_random_stocks = True
plt.figure(figsize=plot_autoscale(), dpi=PLOT_DPI)
while l_stocks:
l_parsed_stocks = []
@@ -164,12 +156,12 @@ def historical(other_args: List[str], preset_loaded: str) -> List[str]:
for parsed_stock in l_parsed_stocks:
l_stocks.remove(parsed_stock)
if ns_parser.signal:
if limit_random_stocks:
plt.title(
f"Screener Historical Price using {finviz_view.d_signals[ns_parser.signal]} signal"
f"Screener Historical Price using {preset_loaded} on 10 of those stocks"
)
else:
plt.title(f"Screener Historical Price using {preset_loaded} preset")
plt.title(f"Screener Historical Price using {preset_loaded}")
plt.xlabel("Time")
plt.ylabel("Share Price ($)")