ETF context new and improved (#1103)

* Add lookup commands

* main ETF commands

* Add technical analysis to ETF

* add prediction techniques to ETF

* add comparison analysis to etf

* allow to select nr of holdings on load

* add shold and summary commands to ETF

* minor improvs

* screener on ETF menu

* add etf/scr wsj commands

* readd compare command

* add sbc to etf/screener

* rename chold and shold

* fix rogue etf and reset

* minor corrections

* minor menu print improv

* legend of weights and title of plot

* refactor screener to screener and disc on ETF

* add ETF scripts
This commit is contained in:
Didier Rodrigues Lopes
2021-12-30 03:21:21 +00:00
committed by GitHub
parent f83ee10c22
commit 78f110b389
28 changed files with 4209 additions and 460 deletions

View File

@@ -30,7 +30,7 @@ jobs:
- run: pip install types-pytz types-requests types-termcolor types-tabulate types-PyYAML types-python-dateutil
- run: bandit -x ./tests -r . || true
- run: black --check .
- run: codespell --ignore-words-list=ba,buil,coo,ether,hist,hsi,mape,navagation,operatio,pres,ser,yeld --quiet-level=2 --skip=./tests,.git,*.css,*.csv,*.html,*.ini,*.ipynb,*.js,*.json,*.lock,*.scss,*.txt,*.yaml
- run: codespell --ignore-words-list=ba,buil,coo,ether,hist,hsi,mape,navagation,operatio,pres,ser,yeld,shold --quiet-level=2 --skip=./tests,.git,*.css,*.csv,*.html,*.ini,*.ipynb,*.js,*.json,*.lock,*.scss,*.txt,*.yaml
- run: flake8 . --count --ignore=E203,W503 --max-line-length=122 --show-source --statistics
- run: mypy --ignore-missing-imports --exclude="/setup\.py$" .
- run: shopt -s globstar && pyupgrade --py36-plus **/*.py

View File

@@ -23,7 +23,7 @@ repos:
hooks:
- id: codespell
entry: codespell
args: [ '--ignore-words-list=ba,buil,coo,ether,hist,hsi,mape,navagation,operatio,pres,ser,yeld', '--quiet-level=2', '--skip=./tests,.git,*.css,*.csv,*.html,*.ini,*.ipynb,*.js,*.json,*.lock,*.scss,*.txt,*.yaml' ]
args: [ '--ignore-words-list=ba,buil,coo,ether,hist,hsi,mape,navagation,operatio,pres,ser,yeld,shold', '--quiet-level=2', '--skip=./tests,.git,*.css,*.csv,*.html,*.ini,*.ipynb,*.js,*.json,*.lock,*.scss,*.txt,*.yaml' ]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: 'v0.910-1'
hooks:

View File

@@ -0,0 +1,307 @@
"""Discovery Controller Module"""
__docformat__ = "numpy"
import argparse
import difflib
from typing import List, Union
from prompt_toolkit.completion import NestedCompleter
from gamestonk_terminal import feature_flags as gtff
from gamestonk_terminal.helper_funcs import (
EXPORT_ONLY_RAW_DATA_ALLOWED,
get_flair,
parse_known_args_and_warn,
try_except,
system_clear,
check_positive,
)
from gamestonk_terminal.menu import session
from gamestonk_terminal.etf.discovery import wsj_view
class DiscoveryController:
"""Discovery Controller class"""
CHOICES = [
"cls",
"home",
"h",
"?",
"help",
"q",
"quit",
"..",
"exit",
"r",
"reset",
]
CHOICES_COMMANDS = [
"gainers",
"decliners",
"active",
]
CHOICES += CHOICES_COMMANDS
def __init__(
self,
queue: List[str] = None,
):
"""Constructor"""
self.disc_parser = argparse.ArgumentParser(add_help=False, prog="disc")
self.disc_parser.add_argument(
"cmd",
choices=self.CHOICES,
)
self.completer: Union[None, NestedCompleter] = None
if session and gtff.USE_PROMPT_TOOLKIT:
choices: dict = {c: {} for c in self.CHOICES}
self.completer = NestedCompleter.from_nested_dict(choices)
if queue:
self.queue = queue
else:
self.queue = list()
def print_help(self):
"""Print help"""
help_str = """
gainers show top gainers [WSJ]
decliners show top decliners [WSJ]
active show most active [WSJ]
"""
print(help_str)
def switch(self, an_input: str):
"""Process and dispatch input
Parameters
-------
an_input : str
string with input arguments
Returns
-------
List[str]
List of commands in the queue to execute
"""
# Empty command
if not an_input:
print("")
return self.queue
# Navigation slash is being used
if "/" in an_input:
actions = an_input.split("/")
# Absolute path is specified
if not actions[0]:
an_input = "home"
# Relative path so execute first instruction
else:
an_input = actions[0]
# Add all instructions to the queue
for cmd in actions[1:][::-1]:
if cmd:
self.queue.insert(0, cmd)
(known_args, other_args) = self.disc_parser.parse_known_args(an_input.split())
# Redirect commands to their correct functions
if known_args.cmd:
if known_args.cmd in ("..", "q"):
known_args.cmd = "quit"
elif known_args.cmd in ("?", "h"):
known_args.cmd = "help"
elif known_args.cmd == "r":
known_args.cmd = "reset"
getattr(
self,
"call_" + known_args.cmd,
lambda _: "Command not recognized!",
)(other_args)
return self.queue
def call_cls(self, _):
"""Process cls command"""
system_clear()
def call_home(self, _):
"""Process home command"""
self.queue.insert(0, "quit")
self.queue.insert(0, "quit")
def call_help(self, _):
"""Process help command"""
self.print_help()
def call_quit(self, _):
"""Process quit menu command"""
print("")
self.queue.insert(0, "quit")
def call_exit(self, _):
"""Process exit terminal command"""
self.queue.insert(0, "quit")
self.queue.insert(0, "quit")
self.queue.insert(0, "quit")
def call_reset(self, _):
"""Process reset command"""
self.queue.insert(0, "disc")
self.queue.insert(0, "etf")
self.queue.insert(0, "reset")
self.queue.insert(0, "quit")
self.queue.insert(0, "quit")
@try_except
def call_gainers(self, other_args):
"""Process gainers command"""
parser = argparse.ArgumentParser(
prog="gainers",
description="Displays top ETF/Mutual fund gainers from wsj.com/market-data",
add_help=False,
)
parser.add_argument(
"-l",
"--limit",
help="Limit of ETFs to display",
type=check_positive,
default=10,
dest="limit",
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-l")
ns_parser = parse_known_args_and_warn(
parser, other_args, export_allowed=EXPORT_ONLY_RAW_DATA_ALLOWED
)
if ns_parser:
wsj_view.show_top_mover("gainers", ns_parser.limit, ns_parser.export)
def call_decliners(self, other_args):
"""Process decliners command"""
parser = argparse.ArgumentParser(
prog="decliners",
description="Displays top ETF/Mutual fund decliners from wsj.com/market-data",
add_help=False,
)
parser.add_argument(
"-l",
"--limit",
help="Limit of ETFs to display",
type=check_positive,
default=10,
dest="limit",
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-l")
ns_parser = parse_known_args_and_warn(
parser, other_args, export_allowed=EXPORT_ONLY_RAW_DATA_ALLOWED
)
if ns_parser:
wsj_view.show_top_mover("decliners", ns_parser.limit, ns_parser.export)
def call_active(self, other_args):
"""Process gainers command"""
parser = argparse.ArgumentParser(
prog="active",
description="Displays most active ETF/Mutual funds from wsj.com/market-data",
add_help=False,
)
parser.add_argument(
"-l",
"--limit",
help="Limit of ETFs to display",
type=check_positive,
default=10,
dest="limit",
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-l")
ns_parser = parse_known_args_and_warn(
parser, other_args, export_allowed=EXPORT_ONLY_RAW_DATA_ALLOWED
)
if ns_parser:
wsj_view.show_top_mover("active", ns_parser.limit, ns_parser.export)
def menu(
queue: List[str] = None,
):
"""Discovery Menu"""
disc_controller = DiscoveryController(queue)
an_input = "HELP_ME"
while True:
# There is a command in the queue
if disc_controller.queue and len(disc_controller.queue) > 0:
# If the command is quitting the menu we want to return in here
if disc_controller.queue[0] in ("q", "..", "quit"):
if len(disc_controller.queue) > 1:
return disc_controller.queue[1:]
return []
# Consume 1 element from the queue
an_input = disc_controller.queue[0]
disc_controller.queue = disc_controller.queue[1:]
# Print the current location because this was an instruction and we want user to know what was the action
if an_input and an_input.split(" ")[0] in disc_controller.CHOICES_COMMANDS:
print(f"{get_flair()} /etf/disc/ $ {an_input}")
# Get input command from user
else:
# Display help menu when entering on this menu from a level above
if an_input == "HELP_ME":
disc_controller.print_help()
# Get input from user using auto-completion
if session and gtff.USE_PROMPT_TOOLKIT and disc_controller.completer:
an_input = session.prompt(
f"{get_flair()} /etf/disc/ $ ",
completer=disc_controller.completer,
search_ignore_case=True,
)
# Get input from user without auto-completion
else:
an_input = input(f"{get_flair()} /etf/disc/ $ ")
try:
# Process the input command
disc_controller.queue = disc_controller.switch(an_input)
except SystemExit:
print(
f"\nThe command '{an_input}' doesn't exist on the /etf/disc menu.",
end="",
)
similar_cmd = difflib.get_close_matches(
an_input.split(" ")[0] if " " in an_input else an_input,
disc_controller.CHOICES,
n=1,
cutoff=0.7,
)
if similar_cmd:
if " " in an_input:
candidate_input = (
f"{similar_cmd[0]} {' '.join(an_input.split(' ')[1:])}"
)
else:
candidate_input = similar_cmd[0]
if candidate_input == an_input:
an_input = ""
disc_controller.queue = []
print("\n")
continue
print(f" Replacing by '{candidate_input}'.")
disc_controller.queue.insert(0, candidate_input)
else:
print("\n")
an_input = ""
disc_controller.queue = []

View File

@@ -0,0 +1,50 @@
"""WSJ view"""
__docformat__ = "numpy"
import os
from tabulate import tabulate
from gamestonk_terminal.etf.discovery import wsj_model
from gamestonk_terminal.helper_funcs import (
export_data,
)
from gamestonk_terminal import feature_flags as gtff
def show_top_mover(sort_type: str, limit: int = 10, export=""):
"""
Show top ETF movers from wsj.com
Parameters
----------
sort_type: str
What to show. Either Gainers, Decliners or Activity
limit: int
Number of etfs to show
export: str
Format to export data
"""
data = wsj_model.etf_movers(sort_type)
if data.empty:
print("No data available\n")
return
if gtff.USE_TABULATE_DF:
print(
tabulate(
data.iloc[:limit],
showindex=False,
headers=data.columns,
floatfmt=".2f",
tablefmt="fancy_grid",
)
)
else:
print(data.head(limit).to_string())
export_data(
export,
os.path.dirname(os.path.abspath(__file__)),
sort_type,
data,
)
print("")

View File

@@ -4,27 +4,45 @@ __docformat__ = "numpy"
import argparse
import difflib
import os
from datetime import datetime
from datetime import datetime, timedelta
from typing import List, Union
import yfinance as yf
import matplotlib.pyplot as plt
import mplfinance as mpf
from colorama import Style
from prompt_toolkit.completion import NestedCompleter
from thepassiveinvestor import create_ETF_report
from thepassiveinvestor import create_ETF_report
from gamestonk_terminal import feature_flags as gtff
from gamestonk_terminal.etf import (
screener_view,
stockanalysis_view,
wsj_view,
financedatabase_view,
stockanalysis_model,
yfinance_view,
)
from gamestonk_terminal.common import newsapi_view
from gamestonk_terminal.helper_funcs import (
EXPORT_BOTH_RAW_DATA_AND_FIGURES,
EXPORT_ONLY_RAW_DATA_ALLOWED,
check_non_negative_float,
check_positive,
valid_date,
get_flair,
parse_known_args_and_warn,
try_except,
system_clear,
plot_autoscale,
export_data,
)
from gamestonk_terminal.menu import session
from gamestonk_terminal.stocks import stocks_helper
from gamestonk_terminal.etf.technical_analysis import ta_controller
from gamestonk_terminal.stocks.comparison_analysis import ca_controller
from gamestonk_terminal.etf.screener import screener_controller
from gamestonk_terminal.etf.discovery import disc_controller
# pylint: disable=C0415,C0302
class ETFController:
@@ -45,26 +63,29 @@ class ETFController:
]
CHOICES_COMMANDS = [
"search",
"ln",
"ld",
"load",
"overview",
"compare",
"holdings",
"screener",
"gainers",
"decliners",
"active",
"news",
"candle",
"pir",
"fds",
"weights",
"summary",
"compare",
]
CHOICES += CHOICES_COMMANDS
preset_options = [
file.strip(".ini")
for file in os.listdir(
os.path.join(os.path.abspath(os.path.dirname(__file__)), "presets/")
)
CHOICES_MENUS = [
"ta",
"pred",
"ca",
"scr",
"disc",
]
CHOICES += CHOICES_COMMANDS + CHOICES_MENUS
def __init__(self, queue: List[str] = None):
"""Constructor"""
self.etf_parser = argparse.ArgumentParser(add_help=False, prog="etf")
@@ -74,7 +95,6 @@ class ETFController:
if session and gtff.USE_PROMPT_TOOLKIT:
choices: dict = {c: {} for c in self.CHOICES}
choices["screener"]["--preset"] = {c: None for c in self.preset_options}
self.completer = NestedCompleter.from_nested_dict(choices)
if queue:
@@ -82,25 +102,39 @@ class ETFController:
else:
self.queue = list()
self.etf_name = ""
self.etf_data = ""
self.etf_holdings: List = list()
def print_help(self):
"""Print help"""
help_str = """
StockAnalysis.com:
search search ETFs matching name (i.e. BlackRock or Invesco)
overview get overview of ETF symbol
holdings get top holdings for ETF
compare compare overview of multiple ETF
screener screen etfs based on overview data
Wall St. Journal:
gainers show top gainers
decliners show top decliners
active show most active
The Passive Investor:
pir create ETF report of multiple tickers
Finance Database:
fds advanced ETF search based on category, name and/or description
"""
print(help_str)
help_txt = f"""
ln lookup by name [FinanceDatabase/StockAnalysis.com]
ld lookup by description [FinanceDatabase]
load load ETF data [Yfinance]
{Style.DIM if not self.etf_name else ""}
Symbol: {self.etf_name}{Style.DIM if len(self.etf_holdings)==0 else ""}
Major holdings: {', '.join(self.etf_holdings)}
> ca comparison analysis, e.g.: get similar, historical, correlation, financials
{Style.RESET_ALL}
> disc discover ETFs, e.g.: gainers/decliners/active
> scr screener ETFs, e.g.: overview/performance, using preset filters
{Style.DIM if not self.etf_name else ""}
overview get overview [StockAnalysis]
holdings top company holdings [StockAnalysis]
weights sector weights allocation [Yfinance]
summary summary description of the ETF [Yfinance]
candle view a candle chart for ETF
news latest news of the company [News API]
pir create (multiple) passive investor excel report(s) [PassiveInvestor]
compare compare multiple different ETFs [StockAnalysis]
> ta technical analysis, e.g.: ema, macd, rsi, adx, bbands, obv
> pred prediction techniques, e.g.: regression, arima, rnn, lstm
{Style.RESET_ALL}"""
print(help_txt)
def switch(self, an_input: str):
"""Process and dispatch input
@@ -176,41 +210,182 @@ Finance Database:
def call_reset(self, _):
"""Process reset command"""
if self.etf_name:
self.queue.insert(0, f"load {self.etf_name}")
self.queue.insert(0, "etf")
self.queue.insert(0, "reset")
self.queue.insert(0, "quit")
@try_except
def call_search(self, other_args: List[str]):
"""Process search command"""
def call_ln(self, other_args: List[str]):
"""Process ln command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="search",
description="Search all available etfs for matching input",
prog="ln",
description="Lookup by name [Source: FinanceDatabase/StockAnalysis.com]",
)
parser.add_argument(
"-e",
"--etf",
"-n",
"--name",
type=str,
dest="search_str",
dest="name",
nargs="+",
help="String to search for",
help="Name to look for ETFs",
required="-h" not in other_args,
)
parser.add_argument(
"-s",
"--source",
type=str,
default="fd",
dest="source",
help="Name to search for, using either FinanceDatabase (fd) or StockAnalysis (sa) as source.",
choices=["sa", "fd"],
)
parser.add_argument(
"-l",
"--limit",
type=check_positive,
dest="limit",
help="Limit of ETFs to display",
default=5,
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-e")
other_args.insert(0, "-n")
ns_parser = parse_known_args_and_warn(
parser, other_args, export_allowed=EXPORT_ONLY_RAW_DATA_ALLOWED
)
if ns_parser:
search_string = " ".join(ns_parser.search_str)
stockanalysis_view.view_search(
to_match=search_string, export=ns_parser.export
name_to_search = " ".join(ns_parser.name)
if ns_parser.source == "fd":
financedatabase_view.display_etf_by_name(
name=name_to_search,
limit=ns_parser.limit,
export=ns_parser.export,
)
elif ns_parser.source == "sa":
stockanalysis_view.display_etf_by_name(
name=name_to_search,
limit=ns_parser.limit,
export=ns_parser.export,
)
else:
print("Wrong source choice!\n")
@try_except
def call_ld(self, other_args: List[str]):
"""Process ld command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="ld",
description="Lookup by description [Source: FinanceDatabase/StockAnalysis.com]",
)
parser.add_argument(
"-d",
"--description",
type=str,
dest="description",
nargs="+",
help="Name to look for ETFs",
required="-h" not in other_args,
)
parser.add_argument(
"-l",
"--limit",
type=check_positive,
dest="limit",
help="Limit of ETFs to display",
default=5,
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-d")
ns_parser = parse_known_args_and_warn(
parser, other_args, export_allowed=EXPORT_ONLY_RAW_DATA_ALLOWED
)
if ns_parser:
description_to_search = " ".join(ns_parser.description)
financedatabase_view.display_etf_by_description(
description=description_to_search,
limit=ns_parser.limit,
export=ns_parser.export,
)
@try_except
def call_load(self, other_args: List[str]):
"""Process load command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="load",
description="Load ETF ticker to perform analysis on.",
)
parser.add_argument(
"-t",
"--ticker",
action="store",
dest="ticker",
required="-h" not in other_args,
help="ETF ticker",
)
parser.add_argument(
"-s",
"--start",
type=valid_date,
default=(datetime.now() - timedelta(days=366)).strftime("%Y-%m-%d"),
dest="start",
help="The starting date (format YYYY-MM-DD) of the ETF",
)
parser.add_argument(
"-e",
"--end",
type=valid_date,
default=datetime.now().strftime("%Y-%m-%d"),
dest="end",
help="The ending date (format YYYY-MM-DD) of the ETF",
)
parser.add_argument(
"-l",
"--limit",
type=check_positive,
default=5,
dest="limit",
help="Limit of holdings to display",
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-t")
ns_parser = parse_known_args_and_warn(parser, other_args)
if ns_parser:
df_etf_candidate = yf.download(
ns_parser.ticker,
start=ns_parser.start,
end=ns_parser.end,
progress=False,
)
if df_etf_candidate.empty:
print("ETF ticker provided does not exist!\n")
return
df_etf_candidate.index.name = "date"
self.etf_name = ns_parser.ticker.upper()
self.etf_data = df_etf_candidate
holdings = stockanalysis_model.get_etf_holdings(self.etf_name)
if holdings.empty:
print("No company holdings found!\n")
else:
self.etf_holdings = holdings.index[: ns_parser.limit].tolist()
print(f"Top company holdings found: {', '.join(self.etf_holdings)}\n")
print("")
@try_except
def call_overview(self, other_args: List[str]):
"""Process overview command"""
@@ -220,25 +395,13 @@ Finance Database:
prog="overview",
description="Get overview data for selected etf",
)
parser.add_argument(
"-e",
"--etf",
type=str,
dest="name",
help="Symbol to look for",
required="-h" not in other_args,
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-e")
ns_parser = parse_known_args_and_warn(
parser, other_args, export_allowed=EXPORT_ONLY_RAW_DATA_ALLOWED
)
if ns_parser:
stockanalysis_view.view_overview(
symbol=ns_parser.name, export=ns_parser.export
symbol=self.etf_name, export=ns_parser.export
)
@try_except
@@ -248,15 +411,7 @@ Finance Database:
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="holdings",
description="Look at ETF holdings",
)
parser.add_argument(
"-e",
"--etf",
type=str,
dest="name",
help="ETF to get holdings for",
required="-h" not in other_args,
description="Look at ETF company holdings",
)
parser.add_argument(
"-l",
@@ -264,21 +419,319 @@ Finance Database:
type=int,
dest="limit",
help="Number of holdings to get",
default=20,
default=10,
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-e")
other_args.insert(0, "-l")
ns_parser = parse_known_args_and_warn(
parser, other_args, export_allowed=EXPORT_ONLY_RAW_DATA_ALLOWED
)
if ns_parser:
stockanalysis_view.view_holdings(
symbol=ns_parser.name,
symbol=self.etf_name,
num_to_show=ns_parser.limit,
export=ns_parser.export,
)
@try_except
def call_news(self, other_args: List[str]):
"""Process news command"""
parser = argparse.ArgumentParser(
add_help=False,
prog="news",
description="""
Prints latest news about ETF, including date, title and web link. [Source: News API]
""",
)
parser.add_argument(
"-l",
"--limit",
action="store",
dest="limit",
type=check_positive,
default=5,
help="Limit of latest news being printed.",
)
parser.add_argument(
"-d",
"--date",
action="store",
dest="n_start_date",
type=valid_date,
default=datetime.now() - timedelta(days=7),
help="The starting date (format YYYY-MM-DD) to search articles from",
)
parser.add_argument(
"-o",
"--oldest",
action="store_false",
dest="n_oldest",
default=True,
help="Show oldest articles first",
)
parser.add_argument(
"-s",
"--sources",
default=[],
nargs="+",
help="Show news only from the sources specified (e.g bbc yahoo.com)",
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-l")
ns_parser = parse_known_args_and_warn(parser, other_args)
if ns_parser:
if self.etf_name:
sources = ns_parser.sources
for idx, source in enumerate(sources):
if source.find(".") == -1:
sources[idx] += ".com"
d_stock = yf.Ticker(self.etf_name).info
newsapi_view.news(
term=d_stock["shortName"].replace(" ", "+")
if "shortName" in d_stock
else self.etf_name,
num=ns_parser.limit,
s_from=ns_parser.n_start_date.strftime("%Y-%m-%d"),
show_newest=ns_parser.n_oldest,
sources=",".join(sources),
)
else:
print("Use 'load <ticker>' prior to this command!", "\n")
@try_except
def call_candle(self, other_args: List[str]):
"""Process candle command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="candle",
description="Shows historic data for an ETF",
)
ns_parser = parse_known_args_and_warn(
parser, other_args, EXPORT_BOTH_RAW_DATA_AND_FIGURES
)
if ns_parser:
if self.etf_name:
data = stocks_helper.process_candle(self.etf_data)
df_etf = stocks_helper.find_trendline(data, "OC_High", "high")
df_etf = stocks_helper.find_trendline(data, "OC_Low", "low")
mc = mpf.make_marketcolors(
up="green",
down="red",
edge="black",
wick="black",
volume="in",
ohlc="i",
)
s = mpf.make_mpf_style(marketcolors=mc, gridstyle=":", y_on_right=True)
ap0 = []
if "OC_High_trend" in df_etf.columns:
ap0.append(
mpf.make_addplot(df_etf["OC_High_trend"], color="g"),
)
if "OC_Low_trend" in df_etf.columns:
ap0.append(
mpf.make_addplot(df_etf["OC_Low_trend"], color="b"),
)
if gtff.USE_ION:
plt.ion()
mpf.plot(
df_etf,
type="candle",
mav=(20, 50),
volume=True,
title=f"\nETF: {self.etf_name}",
addplot=ap0,
xrotation=10,
style=s,
figratio=(10, 7),
figscale=1.10,
figsize=(plot_autoscale()),
update_width_config=dict(
candle_linewidth=1.0, candle_width=0.8, volume_linewidth=1.0
),
)
print("")
export_data(
ns_parser.export,
os.path.join(os.path.dirname(os.path.abspath(__file__)), "candle"),
f"{self.etf_name}",
self.etf_data,
)
else:
print("No ticker loaded. First use `load {ticker}`\n")
@try_except
def call_pir(self, other_args):
"""Process pir command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="pir",
description="Create passive investor ETF excel report",
)
parser.add_argument(
"-e",
"--etfs",
type=str,
dest="names",
help="Symbols to create a report for (e.g. ARKW,ARKQ)",
default=self.etf_name,
)
parser.add_argument(
"--filename",
default=f"ETF_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}",
dest="filename",
help="Filename of the excel ETF report",
)
parser.add_argument(
"--folder",
default=os.path.dirname(os.path.abspath(__file__)).replace(
"gamestonk_terminal", "exports"
),
dest="folder",
help="Folder where the excel ETF report will be saved",
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-e")
ns_parser = parse_known_args_and_warn(parser, other_args)
if ns_parser:
if self.etf_name:
create_ETF_report(
ns_parser.names if ns_parser.names else [self.etf_name],
filename=ns_parser.filename,
folder=ns_parser.folder,
)
print(
f"Created ETF report as {ns_parser.filename} in folder {ns_parser.folder} \n"
)
@try_except
def call_weights(self, other_args: List[str]):
"""Process weights command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="weights",
description="Look at ETF sector holdings",
)
parser.add_argument(
"-m",
"--min",
type=check_non_negative_float,
dest="min",
help="Minimum positive float to display sector",
default=5,
)
parser.add_argument(
"--raw",
action="store_true",
dest="raw",
help="Only output raw data",
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-l")
ns_parser = parse_known_args_and_warn(
parser, other_args, export_allowed=EXPORT_BOTH_RAW_DATA_AND_FIGURES
)
if ns_parser:
yfinance_view.display_etf_weightings(
name=self.etf_name,
raw=ns_parser.raw,
min_pct_to_display=ns_parser.min,
export=ns_parser.export,
)
@try_except
def call_summary(self, other_args: List[str]):
"""Process summary command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="summary",
description="Print ETF description summary",
)
ns_parser = parse_known_args_and_warn(
parser,
other_args,
)
if ns_parser:
yfinance_view.display_etf_description(
name=self.etf_name,
)
@try_except
def call_ta(self, _):
"""Process ta command"""
if self.etf_name:
self.queue = ta_controller.menu(
self.etf_name, self.etf_data.index[0], self.etf_data, self.queue
)
else:
print("Use 'load <ticker>' prior to this command!", "\n")
@try_except
def call_pred(self, _):
"""Process pred command"""
if gtff.ENABLE_PREDICT:
if self.etf_name:
try:
from gamestonk_terminal.stocks.prediction_techniques import (
pred_controller,
)
self.queue = pred_controller.menu(
self.etf_name,
self.etf_data.index[0],
"1440min",
self.etf_data,
self.queue,
)
except ModuleNotFoundError as e:
print(
"One of the optional packages seems to be missing: ",
e,
"\n",
)
else:
print("Use 'load <ticker>' prior to this command!", "\n")
else:
print(
"Predict is disabled. Check ENABLE_PREDICT flag on feature_flags.py",
"\n",
)
@try_except
def call_ca(self, _):
"""Process ca command"""
if len(self.etf_holdings) > 0:
self.queue = ca_controller.menu(
self.etf_holdings, self.queue, from_submenu=True
)
@try_except
def call_scr(self, _):
"""Process scr command"""
self.queue = screener_controller.menu(self.queue)
@try_except
def call_disc(self, _):
"""Process disc command"""
self.queue = disc_controller.menu(self.queue)
@try_except
def call_compare(self, other_args):
"""Process compare command"""
@@ -286,7 +739,7 @@ Finance Database:
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="compare",
description="Compare selected ETFs",
description="Compare selected ETFs [Source: StockAnalysis]",
)
parser.add_argument(
"-e",
@@ -307,204 +760,6 @@ Finance Database:
etf_list = ns_parser.names.upper().split(",")
stockanalysis_view.view_comparisons(etf_list, export=ns_parser.export)
@try_except
def call_screener(self, other_args):
"""Process screener command"""
# TODO: Change presets to use view/set like in stocks/options
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="screener",
add_help=False,
description="Screens ETFS from a personal scraping github repository. Data scraped from stockanalysis.com",
)
parser.add_argument(
"-l",
"--limit",
type=int,
help="Number of etfs to show",
dest="limit",
default=20,
)
parser.add_argument(
"--preset",
choices=self.preset_options,
default="etf_config",
help="Preset to use",
dest="preset",
)
ns_parser = parse_known_args_and_warn(
parser, other_args, export_allowed=EXPORT_ONLY_RAW_DATA_ALLOWED
)
if ns_parser:
screener_view.view_screener(
num_to_show=ns_parser.limit,
preset=ns_parser.preset,
export=ns_parser.export,
)
@try_except
def call_gainers(self, other_args):
"""Process gainers command"""
parser = argparse.ArgumentParser(
prog="gainers",
description="Displays top ETF/Mutual fund gainers from wsj.com/market-data",
add_help=False,
)
parser.add_argument(
"-l", "--limit", help="Number to show", type=int, default=25, dest="limit"
)
ns_parser = parse_known_args_and_warn(
parser, other_args, export_allowed=EXPORT_ONLY_RAW_DATA_ALLOWED
)
if ns_parser:
wsj_view.show_top_mover("gainers", ns_parser.limit, ns_parser.export)
def call_decliners(self, other_args):
"""Process decliners command"""
parser = argparse.ArgumentParser(
prog="decliners",
description="Displays top ETF/Mutual fund decliners from wsj.com/market-data",
add_help=False,
)
parser.add_argument(
"-l", "--limit", help="Number to show", type=int, default=25, dest="limit"
)
ns_parser = parse_known_args_and_warn(
parser, other_args, export_allowed=EXPORT_ONLY_RAW_DATA_ALLOWED
)
if ns_parser:
wsj_view.show_top_mover("decliners", ns_parser.limit, ns_parser.export)
def call_active(self, other_args):
"""Process gainers command"""
parser = argparse.ArgumentParser(
prog="active",
description="Displays most active ETF/Mutual funds from wsj.com/market-data",
add_help=False,
)
parser.add_argument(
"-l", "--limit", help="Number to show", type=int, default=25, dest="limit"
)
ns_parser = parse_known_args_and_warn(
parser, other_args, export_allowed=EXPORT_ONLY_RAW_DATA_ALLOWED
)
if ns_parser:
wsj_view.show_top_mover("active", ns_parser.limit, ns_parser.export)
@try_except
def call_pir(self, other_args):
"""Process pir command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="pir",
description="Create a ETF Report of the selected ETFs",
)
parser.add_argument(
"-e",
"--etfs",
type=str,
dest="names",
help="Symbols to create a report for",
required="-h" not in other_args,
)
parser.add_argument(
"--filename",
default=f"ETF_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}",
dest="filename",
help="Filename of the ETF report",
)
parser.add_argument(
"--folder",
default=os.path.dirname(os.path.abspath(__file__)).replace(
"gamestonk_terminal", "exports"
),
dest="folder",
help="Folder where the ETF report will be saved",
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-e")
ns_parser = parse_known_args_and_warn(parser, other_args)
if ns_parser:
etf_list = ns_parser.names.upper().split(",")
create_ETF_report(
etf_list, filename=ns_parser.filename, folder=ns_parser.folder
)
print(
f"Created ETF report as {ns_parser.filename} in folder {ns_parser.folder} \n"
)
@try_except
def call_fds(self, other_args):
"""Process fds command"""
parser = argparse.ArgumentParser(
description="Display a selection of ETFs based on category, name and/or description filtered by total "
"assets. Returns the top ETFs when no argument is given. [Source: Finance Database]",
add_help=False,
)
parser.add_argument(
"-c",
"--category",
default=None,
nargs="+",
dest="category",
help="Specify the ETF selection based on a category",
)
parser.add_argument(
"-n",
"--name",
default=None,
nargs="+",
dest="name",
help="Specify the ETF selection based on the name",
)
parser.add_argument(
"-d",
"--description",
default=None,
nargs="+",
dest="description",
help="Specify the ETF selection based on the description (not shown in table)",
)
parser.add_argument(
"-ie",
"--include_exchanges",
action="store_false",
help="When used, data from different exchanges is also included. This leads to a much larger "
"pool of data due to the same ETF being listed on multiple exchanges",
)
parser.add_argument(
"-l",
"--limit",
default=10,
type=int,
dest="limit",
help="Enter the number of ETFs you wish to see in the Tabulate window",
)
parser.add_argument(
"-o",
"--options",
action="store_true",
help="Obtain the available categories",
)
ns_parser = parse_known_args_and_warn(parser, other_args)
if ns_parser:
financedatabase_view.show_etfs(
category=ns_parser.category,
name=ns_parser.name,
description=ns_parser.description,
include_exchanges=ns_parser.include_exchanges,
amount=ns_parser.limit,
options=ns_parser.options,
)
def menu(queue: List[str] = None):
etf_controller = ETFController(queue)

View File

@@ -0,0 +1,73 @@
"""Finance Database model"""
__docformat__ = "numpy"
from typing import Dict, List
import financedatabase as fd
def get_etfs_by_name(name: str) -> Dict:
"""Return a selection of ETFs based on name filtered by total assets. [Source: Finance Database]
Parameters
----------
name: str
Search by name to find ETFs matching the criteria.
Returns
----------
data : pd.DataFrame
Dataframe with ETFs that match a certain name
"""
data = fd.select_etfs()
data = fd.search_products(data, query=name, search="long_name")
return data
def get_etfs_by_description(description: str) -> Dict:
"""Return a selection of ETFs based on description filtered by total assets. [Source: Finance Database]
Parameters
----------
description: str
Search by description to find ETFs matching the criteria.
Returns
----------
data : pd.DataFrame
Dataframe with ETFs that match a certain description
"""
data = fd.select_etfs()
data = fd.search_products(data, query=description, search="summary")
return data
def get_etfs_by_category(category: str) -> Dict:
"""Return a selection of ETFs based on category filtered by total assets. [Source: Finance Database]
Parameters
----------
category: str
Search by category to find ETFs matching the criteria.
Returns
----------
data : pd.DataFrame
Dataframe with ETFs that match a certain description
"""
data = fd.select_etfs(category=category)
return data
def get_etfs_categories() -> List[str]:
"""Return a selection of ETF categories. [Source: Finance Database]
Returns
----------
List[str]
ETF categories
"""
return fd.show_options("etfs")

View File

@@ -1,57 +1,35 @@
"""Finance Database view"""
__docformat__ = "numpy"
import financedatabase as fd
import os
import pandas as pd
from tabulate import tabulate
from gamestonk_terminal import feature_flags as gtff
from gamestonk_terminal.etf import financedatabase_model
from gamestonk_terminal.helper_funcs import export_data
def show_etfs(
category: str,
def display_etf_by_name(
name: str,
description: str,
include_exchanges: bool,
amount: int,
options: str,
limit: int,
export: str = "",
):
"""
Display a selection of ETFs based on category, name and/or description filtered by total assets.
Returns the top ETFs when no argument is given. [Source: Finance Database]
"""Display a selection of ETFs based on name filtered by total assets. [Source: Finance Database]
Parameters
----------
category: str
Search by category to find ETFs matching the criteria.
name: str
Search by name to find ETFs matching the criteria.
description: str
Search by description to find ETFs matching the criteria.
include_exchanges: bool
When you wish to include different exchanges use this boolean.
amount : int
Number of ETFs to display, default is 10.
options : str
Show the category options.
limit: int
Limit of ETFs to display
export: str
Type of format to export data
"""
if options:
for option in fd.show_options("etfs"):
print(option)
data = financedatabase_model.get_etfs_by_name(name)
if not data:
print("No data was found with that name\n")
return
if category is not None:
data = fd.select_etfs(
category=" ".join(category).title(), exclude_exchanges=include_exchanges
)
else:
data = fd.select_etfs(category=category, exclude_exchanges=include_exchanges)
if name is not None:
print(name)
data = fd.search_products(data, query=" ".join(name), search="long_name")
if description is not None:
data = fd.search_products(data, query=" ".join(description), search="summary")
tabulate_data = pd.DataFrame(data).T[
["long_name", "family", "category", "total_assets"]
]
@@ -61,7 +39,7 @@ def show_etfs(
if gtff.USE_TABULATE_DF:
print(
tabulate(
tabulate_data_sorted.iloc[:amount],
tabulate_data_sorted.iloc[:limit],
showindex=True,
headers=["Name", "Family", "Category", "Total Assets [M]"],
floatfmt=".2f",
@@ -70,4 +48,99 @@ def show_etfs(
"\n",
)
else:
print(tabulate_data_sorted.iloc[:amount].to_string(), "\n")
print(tabulate_data_sorted.iloc[:limit].to_string(), "\n")
export_data(export, os.path.dirname(os.path.abspath(__file__)), "ln_fd", data)
def display_etf_by_description(
description: str,
limit: int,
export: str = "",
):
"""Display a selection of ETFs based on description filtered by total assets. [Source: Finance Database]
Parameters
----------
description: str
Search by description to find ETFs matching the criteria.
limit: int
Limit of ETFs to display
export: str
Type of format to export data
"""
data = financedatabase_model.get_etfs_by_description(description)
if not data:
print("No data was found with that description\n")
return
tabulate_data = pd.DataFrame(data).T[
["long_name", "family", "category", "total_assets"]
]
tabulate_data_sorted = tabulate_data.sort_values(by="total_assets", ascending=False)
tabulate_data_sorted["total_assets"] = tabulate_data_sorted["total_assets"] / 1e6
if gtff.USE_TABULATE_DF:
print(
tabulate(
tabulate_data_sorted.iloc[:limit],
showindex=True,
headers=["Name", "Family", "Category", "Total Assets [M]"],
floatfmt=".2f",
tablefmt="fancy_grid",
),
"\n",
)
else:
print(tabulate_data_sorted.iloc[:limit].to_string(), "\n")
export_data(export, os.path.dirname(os.path.abspath(__file__)), "ld", data)
def display_etf_by_category(
category: str,
limit: int,
export: str = "",
):
"""Display a selection of ETFs based on a category filtered by total assets. [Source: Finance Database]
Parameters
----------
description: str
Search by description to find ETFs matching the criteria.
limit: int
Limit of ETFs to display
export: str
Type of format to export data
"""
data = financedatabase_model.get_etfs_by_category(category)
if not data:
print("No data was found on that category\n")
return
tabulate_data = pd.DataFrame(data).T[
["long_name", "family", "category", "total_assets"]
]
tabulate_data_sorted = tabulate_data.sort_values(by="total_assets", ascending=False)
tabulate_data_sorted["total_assets"] = tabulate_data_sorted["total_assets"] / 1e6
if gtff.USE_TABULATE_DF:
print(
tabulate(
tabulate_data_sorted.iloc[:limit],
showindex=True,
headers=["Name", "Family", "Category", "Total Assets [M]"],
floatfmt=".2f",
tablefmt="fancy_grid",
),
"\n",
)
else:
print(tabulate_data_sorted.iloc[:limit].to_string(), "\n")
export_data(
export,
os.path.join(os.path.dirname(os.path.abspath(__file__)), "screener"),
"sbc",
data,
)

View File

@@ -0,0 +1,983 @@
""" Prediction Controller """
__docformat__ = "numpy"
# pylint:disable=too-many-lines
import argparse
import difflib
from typing import List, Union
from datetime import datetime, timedelta
import pandas as pd
import numpy as np
from prompt_toolkit.completion import NestedCompleter
from gamestonk_terminal import feature_flags as gtff
from gamestonk_terminal.helper_funcs import (
get_flair,
parse_known_args_and_warn,
check_positive,
valid_date,
get_next_stock_market_days,
EXPORT_ONLY_FIGURES_ALLOWED,
try_except,
system_clear,
)
from gamestonk_terminal.menu import session
from gamestonk_terminal.common.prediction_techniques import (
arima_view,
arima_model,
ets_model,
mc_model,
ets_view,
knn_view,
neural_networks_view,
regression_view,
pred_helper,
mc_view,
)
from gamestonk_terminal.stocks import stocks_helper
class PredictionTechniquesController:
"""Prediction Techniques Controller class"""
# Command choices
CHOICES = [
"cls",
"home",
"h",
"?",
"help",
"q",
"quit",
"..",
"exit",
"r",
"reset",
]
CHOICES_COMMANDS = [
"load",
"pick",
"ets",
"knn",
"regression",
"arima",
"mlp",
"rnn",
"lstm",
"conv1d",
"mc",
]
CHOICES += CHOICES_COMMANDS
def __init__(
self,
ticker: str,
start: datetime,
interval: str,
stock: pd.DataFrame,
queue: List[str] = None,
):
"""Constructor"""
stock["Returns"] = stock["Adj Close"].pct_change()
stock["LogRet"] = np.log(stock["Adj Close"]) - np.log(
stock["Adj Close"].shift(1)
)
stock = stock.rename(columns={"Adj Close": "AdjClose"})
stock = stock.dropna()
self.stock = stock
self.ticker = ticker
self.start = start
self.interval = interval
self.target = "AdjClose"
self.pred_parser = argparse.ArgumentParser(add_help=False, prog="pred")
self.pred_parser.add_argument(
"cmd",
choices=self.CHOICES,
)
self.completer: Union[None, NestedCompleter] = None
if session and gtff.USE_PROMPT_TOOLKIT:
choices: dict = {c: {} for c in self.CHOICES}
choices["load"]["-r"] = {c: {} for c in stocks_helper.INTERVALS}
choices["pick"] = {c: {} for c in self.stock.columns}
choices["ets"]["-t"] = {c: {} for c in ets_model.TRENDS}
choices["ets"]["-s"] = {c: {} for c in ets_model.SEASONS}
choices["arima"]["-i"] = {c: {} for c in arima_model.ICS}
choices["mc"]["--dist"] = {c: {} for c in mc_model.DISTRIBUTIONS}
self.completer = NestedCompleter.from_nested_dict(choices)
if queue:
self.queue = queue
else:
self.queue = list()
def print_help(self):
"""Print help"""
s_intraday = (f"Intraday {self.interval}", "Daily")[self.interval == "1440min"]
if self.start:
stock_info = f"{s_intraday} Stock: {self.ticker} (from {self.start.strftime('%Y-%m-%d')})"
else:
stock_info = "{s_intraday} Stock: {self.ticker}"
help_string = f"""
Prediction Techniques Menu:
load load new ticker
pick pick new target variable
Ticker Loaded: {stock_info}
Target Column: {self.target}
Models:
ets exponential smoothing (e.g. Holt-Winters)
knn k-Nearest Neighbors
regression polynomial regression
arima autoregressive integrated moving average
mlp MultiLayer Perceptron
rnn Recurrent Neural Network
lstm Long-Short Term Memory
conv1d 1D Convolutional Neural Network
mc Monte-Carlo simulations
"""
print(help_string)
def switch(self, an_input: str):
"""Process and dispatch input
Parameters
-------
an_input : str
string with input arguments
Returns
-------
List[str]
List of commands in the queue to execute
"""
# Empty command
if not an_input:
print("")
return self.queue
# Navigation slash is being used
if "/" in an_input:
actions = an_input.split("/")
# Absolute path is specified
if not actions[0]:
an_input = "home"
# Relative path so execute first instruction
else:
an_input = actions[0]
# Add all instructions to the queue
for cmd in actions[1:][::-1]:
if cmd:
self.queue.insert(0, cmd)
(known_args, other_args) = self.pred_parser.parse_known_args(an_input.split())
# Redirect commands to their correct functions
if known_args.cmd:
if known_args.cmd in ("..", "q"):
known_args.cmd = "quit"
elif known_args.cmd in ("?", "h"):
known_args.cmd = "help"
elif known_args.cmd == "r":
known_args.cmd = "reset"
getattr(
self,
"call_" + known_args.cmd,
lambda _: "Command not recognized!",
)(other_args)
return self.queue
def call_cls(self, _):
"""Process cls command"""
system_clear()
def call_home(self, _):
"""Process home command"""
self.queue.insert(0, "quit")
self.queue.insert(0, "quit")
def call_help(self, _):
"""Process help command"""
self.print_help()
def call_quit(self, _):
"""Process quit menu command"""
print("")
self.queue.insert(0, "quit")
def call_exit(self, _):
"""Process exit terminal command"""
self.queue.insert(0, "quit")
self.queue.insert(0, "quit")
self.queue.insert(0, "quit")
def call_reset(self, _):
"""Process reset command"""
self.queue.insert(0, "pred")
if self.ticker:
self.queue.insert(0, f"load {self.ticker}")
self.queue.insert(0, "stocks")
self.queue.insert(0, "reset")
self.queue.insert(0, "quit")
self.queue.insert(0, "quit")
def call_load(self, other_args: List[str]):
"""Process load command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="load",
description="Load stock ticker to perform analysis on. When the data source is 'yf', an Indian ticker can be"
" loaded by using '.NS' at the end, e.g. 'SBIN.NS'. See available market in"
" https://help.yahoo.com/kb/exchanges-data-providers-yahoo-finance-sln2310.html.",
)
parser.add_argument(
"-t",
"--ticker",
action="store",
dest="ticker",
required="-h" not in other_args,
help="Stock ticker",
)
parser.add_argument(
"-s",
"--start",
type=valid_date,
default=(datetime.now() - timedelta(days=366)).strftime("%Y-%m-%d"),
dest="start",
help="The starting date (format YYYY-MM-DD) of the stock",
)
parser.add_argument(
"-e",
"--end",
type=valid_date,
default=datetime.now().strftime("%Y-%m-%d"),
dest="end",
help="The ending date (format YYYY-MM-DD) of the stock",
)
parser.add_argument(
"-i",
"--interval",
action="store",
dest="interval",
type=int,
default=1440,
choices=stocks_helper.INTERVALS,
help="Intraday stock minutes",
)
parser.add_argument(
"--source",
action="store",
dest="source",
choices=stocks_helper.SOURCES,
default="yf",
help="Source of historical data.",
)
parser.add_argument(
"-p",
"--prepost",
action="store_true",
default=False,
dest="prepost",
help="Pre/After market hours. Only works for 'yf' source, and intraday data",
)
# For the case where a user uses: 'load BB'
if other_args and "-t" not in other_args and "-h" not in other_args:
other_args.insert(0, "-t")
ns_parser = parse_known_args_and_warn(
parser, other_args, EXPORT_ONLY_FIGURES_ALLOWED
)
if ns_parser:
df_stock_candidate = stocks_helper.load(
ns_parser.ticker,
ns_parser.start,
ns_parser.interval,
ns_parser.end,
ns_parser.prepost,
ns_parser.source,
)
if not df_stock_candidate.empty:
self.stock = df_stock_candidate
if "." in ns_parser.ticker:
self.ticker = ns_parser.ticker.upper().split(".")[0]
else:
self.ticker = ns_parser.ticker.upper()
self.start = ns_parser.start
self.interval = str(ns_parser.interval) + "min"
self.stock["Returns"] = self.stock["Adj Close"].pct_change()
self.stock["LogRet"] = np.log(self.stock["Adj Close"]) - np.log(
self.stock["Adj Close"].shift(1)
)
self.stock = self.stock.rename(columns={"Adj Close": "AdjClose"})
self.stock = self.stock.dropna()
@try_except
def call_pick(self, other_args: List[str]):
"""Process pick command"""
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
add_help=False,
prog="pick",
description="""
Change target variable
""",
)
parser.add_argument(
"-t",
"--target",
dest="target",
choices=list(self.stock.columns),
help="Select variable to analyze",
)
if other_args and "-t" not in other_args and "-h" not in other_args:
other_args.insert(0, "-t")
ns_parser = parse_known_args_and_warn(
parser, other_args, EXPORT_ONLY_FIGURES_ALLOWED
)
if ns_parser:
self.target = ns_parser.target
print("")
@try_except
def call_ets(self, other_args: List[str]):
"""Process ets command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="ets",
description="""
Exponential Smoothing, see https://otexts.com/fpp2/taxonomy.html
Trend='N', Seasonal='N': Simple Exponential Smoothing
Trend='N', Seasonal='A': Exponential Smoothing
Trend='N', Seasonal='M': Exponential Smoothing
Trend='A', Seasonal='N': Holt’s linear method
Trend='A', Seasonal='A': Additive Holt-Winters’ method
Trend='A', Seasonal='M': Multiplicative Holt-Winters’ method
Trend='Ad', Seasonal='N': Additive damped trend method
Trend='Ad', Seasonal='A': Exponential Smoothing
Trend='Ad', Seasonal='M': Holt-Winters’ damped method
Trend component: N: None, A: Additive, Ad: Additive Damped
Seasonality component: N: None, A: Additive, M: Multiplicative
""",
)
parser.add_argument(
"-d",
"--days",
action="store",
dest="n_days",
type=check_positive,
default=5,
help="prediction days.",
)
parser.add_argument(
"-t",
"--trend",
action="store",
dest="trend",
choices=ets_model.TRENDS,
default="N",
help="Trend component: N: None, A: Additive, Ad: Additive Damped.",
)
parser.add_argument(
"-s",
"--seasonal",
action="store",
dest="seasonal",
choices=ets_model.SEASONS,
default="N",
help="Seasonality component: N: None, A: Additive, M: Multiplicative.",
)
parser.add_argument(
"-p",
"--periods",
action="store",
dest="seasonal_periods",
type=check_positive,
default=5,
help="Seasonal periods.",
)
parser.add_argument(
"-e",
"--end",
action="store",
type=valid_date,
dest="s_end_date",
default=None,
help="The end date (format YYYY-MM-DD) to select - Backtesting",
)
ns_parser = parse_known_args_and_warn(
parser, other_args, export_allowed=EXPORT_ONLY_FIGURES_ALLOWED
)
if ns_parser:
if ns_parser.s_end_date:
if ns_parser.s_end_date < self.stock.index[0]:
print(
"Backtesting not allowed, since End Date is older than Start Date of historical data\n"
)
if ns_parser.s_end_date < get_next_stock_market_days(
last_stock_day=self.stock.index[0],
n_next_days=5 + ns_parser.n_days,
)[-1]:
print(
"Backtesting not allowed, since End Date is too close to Start Date to train model\n"
)
ets_view.display_exponential_smoothing(
ticker=self.ticker,
values=self.stock[self.target],
n_predict=ns_parser.n_days,
trend=ns_parser.trend,
seasonal=ns_parser.seasonal,
seasonal_periods=ns_parser.seasonal_periods,
s_end_date=ns_parser.s_end_date,
export=ns_parser.export,
)
@try_except
def call_knn(self, other_args: List[str]):
"""Process knn command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="knn",
description="""
K nearest neighbors is a simple algorithm that stores all
available cases and predict the numerical target based on a similarity measure
(e.g. distance functions).
""",
)
parser.add_argument(
"-i",
"--input",
action="store",
dest="n_inputs",
type=check_positive,
default=40,
help="number of days to use as input for prediction.",
)
parser.add_argument(
"-d",
"--days",
action="store",
dest="n_days",
type=check_positive,
default=5,
help="prediction days.",
)
parser.add_argument(
"-j",
"--jumps",
action="store",
dest="n_jumps",
type=check_positive,
default=1,
help="number of jumps in training data.",
)
parser.add_argument(
"-n",
"--neighbors",
action="store",
dest="n_neighbors",
type=check_positive,
default=20,
help="number of neighbors to use on the algorithm.",
)
parser.add_argument(
"-e",
"--end",
action="store",
type=valid_date,
dest="s_end_date",
default=None,
help="The end date (format YYYY-MM-DD) to select for testing",
)
parser.add_argument(
"-t",
"--test_size",
default=0.2,
dest="valid_split",
type=float,
help="Percentage of data to validate in sample",
)
parser.add_argument(
"--no_shuffle",
action="store_false",
dest="no_shuffle",
default=True,
help="Specify if shuffling validation inputs.",
)
ns_parser = parse_known_args_and_warn(
parser, other_args, EXPORT_ONLY_FIGURES_ALLOWED
)
if ns_parser:
knn_view.display_k_nearest_neighbors(
ticker=self.ticker,
data=self.stock[self.target],
n_neighbors=ns_parser.n_neighbors,
n_input_days=ns_parser.n_inputs,
n_predict_days=ns_parser.n_days,
test_size=ns_parser.valid_split,
end_date=ns_parser.s_end_date,
no_shuffle=ns_parser.no_shuffle,
)
@try_except
def call_regression(self, other_args: List[str]):
"""Process linear command"""
parser = argparse.ArgumentParser(
add_help=False,
prog="regression",
description="""
Regression attempts to model the relationship between
two variables by fitting a linear/quadratic/cubic/other equation to
observed data. One variable is considered to be an explanatory variable,
and the other is considered to be a dependent variable.
""",
)
parser.add_argument(
"-i",
"--input",
action="store",
dest="n_inputs",
type=check_positive,
default=40,
help="number of days to use for prediction.",
)
parser.add_argument(
"-d",
"--days",
action="store",
dest="n_days",
type=check_positive,
default=5,
help="prediction days.",
)
parser.add_argument(
"-j",
"--jumps",
action="store",
dest="n_jumps",
type=check_positive,
default=1,
help="number of jumps in training data.",
)
parser.add_argument(
"-e",
"--end",
action="store",
type=valid_date,
dest="s_end_date",
default=None,
help="The end date (format YYYY-MM-DD) to select - Backtesting",
)
parser.add_argument(
"-p",
"--polynomial",
action="store",
dest="n_polynomial",
type=check_positive,
default=1,
help="polynomial associated with regression.",
)
if (
other_args
and "-h" not in other_args
and ("-p" not in other_args or "--polynomial" not in other_args)
):
other_args.insert(0, "-p")
ns_parser = parse_known_args_and_warn(
parser, other_args, export_allowed=EXPORT_ONLY_FIGURES_ALLOWED
)
if ns_parser:
# BACKTESTING CHECK
if ns_parser.s_end_date:
if ns_parser.s_end_date < self.stock.index[0]:
print(
"Backtesting not allowed, since End Date is older than Start Date of historical data\n"
)
if ns_parser.s_end_date < get_next_stock_market_days(
last_stock_day=self.stock.index[0],
n_next_days=5 + ns_parser.n_days,
)[-1]:
print(
"Backtesting not allowed, since End Date is too close to Start Date to train model\n"
)
regression_view.display_regression(
dataset=self.ticker,
values=self.stock[self.target],
poly_order=ns_parser.n_polynomial,
n_input=ns_parser.n_inputs,
n_predict=ns_parser.n_days,
n_jumps=ns_parser.n_jumps,
s_end_date=ns_parser.s_end_date,
export=ns_parser.export,
)
@try_except
def call_arima(self, other_args: List[str]):
"""Process arima command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="arima",
description="""
In statistics and econometrics, and in particular in time series analysis, an
autoregressive integrated moving average (ARIMA) model is a generalization of an
autoregressive moving average (ARMA) model. Both of these models are fitted to time
series data either to better understand the data or to predict future points in the
series (forecasting). ARIMA(p,d,q) where parameters p, d, and q are non-negative
integers, p is the order (number of time lags) of the autoregressive model, d is the
degree of differencing (the number of times the data have had past values subtracted),
and q is the order of the moving-average model.
""",
)
parser.add_argument(
"-d",
"--days",
action="store",
dest="n_days",
type=check_positive,
default=5,
help="prediction days.",
)
parser.add_argument(
"-i",
"--ic",
action="store",
dest="s_ic",
type=str,
default="aic",
choices=arima_model.ICS,
help="information criteria.",
)
parser.add_argument(
"-s",
"--seasonal",
action="store_true",
default=False,
dest="b_seasonal",
help="Use weekly seasonal data.",
)
parser.add_argument(
"-o",
"--order",
action="store",
dest="s_order",
default="",
type=str,
help="arima model order (p,d,q) in format: p,d,q.",
)
parser.add_argument(
"-r",
"--results",
action="store_true",
dest="b_results",
default=False,
help="results about ARIMA summary flag.",
)
parser.add_argument(
"-e",
"--end",
action="store",
type=valid_date,
dest="s_end_date",
default=None,
help="The end date (format YYYY-MM-DD) to select - Backtesting",
)
ns_parser = parse_known_args_and_warn(
parser, other_args, export_allowed=EXPORT_ONLY_FIGURES_ALLOWED
)
if ns_parser:
# BACKTESTING CHECK
if ns_parser.s_end_date:
if ns_parser.s_end_date < self.stock.index[0]:
print(
"Backtesting not allowed, since End Date is older than Start Date of historical data\n"
)
if ns_parser.s_end_date < get_next_stock_market_days(
last_stock_day=self.stock.index[0],
n_next_days=5 + ns_parser.n_days,
)[-1]:
print(
"Backtesting not allowed, since End Date is too close to Start Date to train model\n"
)
arima_view.display_arima(
dataset=self.ticker,
values=self.stock[self.target],
arima_order=ns_parser.s_order,
n_predict=ns_parser.n_days,
seasonal=ns_parser.b_seasonal,
ic=ns_parser.s_ic,
results=ns_parser.b_results,
s_end_date=ns_parser.s_end_date,
export=ns_parser.export,
)
@try_except
def call_mlp(self, other_args: List[str]):
"""Process mlp command"""
try:
ns_parser = pred_helper.parse_args(
prog="mlp",
description="""Multi-Layered-Perceptron. """,
other_args=other_args,
)
if ns_parser:
neural_networks_view.display_mlp(
dataset=self.ticker,
data=self.stock[self.target],
n_input_days=ns_parser.n_inputs,
n_predict_days=ns_parser.n_days,
learning_rate=ns_parser.lr,
epochs=ns_parser.n_epochs,
batch_size=ns_parser.n_batch_size,
test_size=ns_parser.valid_split,
n_loops=ns_parser.n_loops,
no_shuffle=ns_parser.no_shuffle,
)
except Exception as e:
print(e, "\n")
finally:
pred_helper.restore_env()
def call_rnn(self, other_args: List[str]):
"""Process rnn command"""
try:
ns_parser = pred_helper.parse_args(
prog="rnn",
description="""Recurrent Neural Network. """,
other_args=other_args,
)
if ns_parser:
neural_networks_view.display_rnn(
dataset=self.ticker,
data=self.stock[self.target],
n_input_days=ns_parser.n_inputs,
n_predict_days=ns_parser.n_days,
learning_rate=ns_parser.lr,
epochs=ns_parser.n_epochs,
batch_size=ns_parser.n_batch_size,
test_size=ns_parser.valid_split,
n_loops=ns_parser.n_loops,
no_shuffle=ns_parser.no_shuffle,
)
except Exception as e:
print(e, "\n")
finally:
pred_helper.restore_env()
def call_lstm(self, other_args: List[str]):
"""Process lstm command"""
try:
ns_parser = pred_helper.parse_args(
prog="lstm",
description="""Long-Short Term Memory. """,
other_args=other_args,
)
if ns_parser:
neural_networks_view.display_lstm(
dataset=self.ticker,
data=self.stock[self.target],
n_input_days=ns_parser.n_inputs,
n_predict_days=ns_parser.n_days,
learning_rate=ns_parser.lr,
epochs=ns_parser.n_epochs,
batch_size=ns_parser.n_batch_size,
test_size=ns_parser.valid_split,
n_loops=ns_parser.n_loops,
no_shuffle=ns_parser.no_shuffle,
)
except Exception as e:
print(e, "\n")
finally:
pred_helper.restore_env()
def call_conv1d(self, other_args: List[str]):
"""Process conv1d command"""
try:
ns_parser = pred_helper.parse_args(
prog="conv1d",
description="""1D CNN.""",
other_args=other_args,
)
if ns_parser:
neural_networks_view.display_conv1d(
dataset=self.ticker,
data=self.stock[self.target],
n_input_days=ns_parser.n_inputs,
n_predict_days=ns_parser.n_days,
learning_rate=ns_parser.lr,
epochs=ns_parser.n_epochs,
batch_size=ns_parser.n_batch_size,
test_size=ns_parser.valid_split,
n_loops=ns_parser.n_loops,
no_shuffle=ns_parser.no_shuffle,
)
except Exception as e:
print(e, "\n")
finally:
pred_helper.restore_env()
@try_except
def call_mc(self, other_args: List[str]):
"""Process mc command"""
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
add_help=False,
prog="mc",
description="""
Perform Monte Carlo forecasting
""",
)
parser.add_argument(
"-d",
"--days",
help="Days to predict",
dest="n_days",
type=check_positive,
default=30,
)
parser.add_argument(
"-n",
"--num",
help="Number of simulations to perform",
dest="n_sims",
default=100,
)
parser.add_argument(
"--dist",
choices=mc_model.DISTRIBUTIONS,
default="lognormal",
dest="dist",
help="Whether to model returns or log returns",
)
ns_parser = parse_known_args_and_warn(
parser, other_args, export_allowed=EXPORT_ONLY_FIGURES_ALLOWED
)
if ns_parser:
if self.target != "AdjClose":
print("MC Prediction designed for AdjClose prices\n")
mc_view.display_mc_forecast(
data=self.stock[self.target],
n_future=ns_parser.n_days,
n_sims=ns_parser.n_sims,
use_log=ns_parser.dist == "lognormal",
export=ns_parser.export,
)
def menu(
ticker: str,
start: datetime,
interval: str,
stock: pd.DataFrame,
queue: List[str] = None,
):
"""Prediction Techniques Menu"""
pred_controller = PredictionTechniquesController(
ticker, start, interval, stock, queue
)
an_input = "HELP_ME"
while True:
# There is a command in the queue
if pred_controller.queue and len(pred_controller.queue) > 0:
# If the command is quitting the menu we want to return in here
if pred_controller.queue[0] in ("q", "..", "quit"):
if len(pred_controller.queue) > 1:
return pred_controller.queue[1:]
return []
# Consume 1 element from the queue
an_input = pred_controller.queue[0]
pred_controller.queue = pred_controller.queue[1:]
# Print the current location because this was an instruction and we want user to know what was the action
if an_input and an_input.split(" ")[0] in pred_controller.CHOICES_COMMANDS:
print(f"{get_flair()} /stocks/pred/ $ {an_input}")
# Get input command from user
else:
# Display help menu when entering on this menu from a level above
if an_input == "HELP_ME":
pred_controller.print_help()
# Get input from user using auto-completion
if session and gtff.USE_PROMPT_TOOLKIT and pred_controller.completer:
an_input = session.prompt(
f"{get_flair()} /stocks/pred/ $ ",
completer=pred_controller.completer,
search_ignore_case=True,
)
# Get input from user without auto-completion
else:
an_input = input(f"{get_flair()} /stocks/pred/ $ ")
try:
# Process the input command
pred_controller.queue = pred_controller.switch(an_input)
except SystemExit:
print(
f"\nThe command '{an_input}' doesn't exist on the /stocks/options menu.",
end="",
)
similar_cmd = difflib.get_close_matches(
an_input.split(" ")[0] if " " in an_input else an_input,
pred_controller.CHOICES,
n=1,
cutoff=0.7,
)
if similar_cmd:
if " " in an_input:
candidate_input = (
f"{similar_cmd[0]} {' '.join(an_input.split(' ')[1:])}"
)
else:
candidate_input = similar_cmd[0]
if candidate_input == an_input:
an_input = ""
pred_controller.queue = []
print("\n")
continue
print(f" Replacing by '{an_input}'.")
pred_controller.queue.insert(0, an_input)
else:
print("\n")
an_input = ""
pred_controller.queue = []

View File

@@ -1,3 +1,6 @@
# Author of preset: James Maslek
# Description: Give a small description of what this preset is trying to do
[Price]
MIN = None
MAX = None

View File

@@ -0,0 +1,471 @@
"""Screener Controller Module"""
__docformat__ = "numpy"
# pylint:disable=too-many-lines
# pylint:disable=R0904,C0201
import os
import argparse
import configparser
import difflib
from typing import List, Union
from prompt_toolkit.completion import NestedCompleter
from gamestonk_terminal import feature_flags as gtff
from gamestonk_terminal.helper_funcs import (
EXPORT_ONLY_RAW_DATA_ALLOWED,
get_flair,
parse_known_args_and_warn,
try_except,
system_clear,
check_positive,
)
from gamestonk_terminal.menu import session
from gamestonk_terminal.etf.screener import screener_view
from gamestonk_terminal.etf import financedatabase_view, financedatabase_model
presets_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "presets/")
class ScreenerController:
"""Screener Controller class"""
CHOICES = [
"cls",
"home",
"h",
"?",
"help",
"q",
"quit",
"..",
"exit",
"r",
"reset",
]
CHOICES_COMMANDS = [
"view",
"set",
"screen",
"sbc",
]
CHOICES += CHOICES_COMMANDS
presets_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "presets/")
preset_choices = [
f.split(".")[0] for f in os.listdir(presets_path) if f.endswith(".ini")
]
sortby_screen_choices = [
"Assets",
"NAV",
"Expense",
"PE",
"SharesOut",
"Div",
"DivYield",
"Volume",
"Open",
"PrevClose",
"YrLow",
"YrHigh",
"Beta",
"N_Hold",
]
def __init__(
self,
queue: List[str] = None,
):
"""Constructor"""
self.scr_parser = argparse.ArgumentParser(add_help=False, prog="scr")
self.scr_parser.add_argument(
"cmd",
choices=self.CHOICES,
)
self.completer: Union[None, NestedCompleter] = None
if session and gtff.USE_PROMPT_TOOLKIT:
choices: dict = {c: {} for c in self.CHOICES}
choices["view"] = {c: None for c in self.preset_choices}
choices["set"] = {c: None for c in self.preset_choices}
choices["sbc"] = {
c: None for c in financedatabase_model.get_etfs_categories()
}
self.completer = NestedCompleter.from_nested_dict(choices)
self.preset = "etf_config"
self.screen_tickers: List = list()
if queue:
self.queue = queue
else:
self.queue = list()
def print_help(self):
"""Print help"""
help_str = f"""
view view available presets
set set one of the available presets
PRESET: {self.preset}
screen screen ETF using preset selected [StockAnalysis]
sbc screen by category [FinanceDatabase]
"""
print(help_str)
def switch(self, an_input: str):
"""Process and dispatch input
Parameters
-------
an_input : str
string with input arguments
Returns
-------
List[str]
List of commands in the queue to execute
"""
# Empty command
if not an_input:
print("")
return self.queue
# Navigation slash is being used
if "/" in an_input:
actions = an_input.split("/")
# Absolute path is specified
if not actions[0]:
an_input = "home"
# Relative path so execute first instruction
else:
an_input = actions[0]
# Add all instructions to the queue
for cmd in actions[1:][::-1]:
if cmd:
self.queue.insert(0, cmd)
(known_args, other_args) = self.scr_parser.parse_known_args(an_input.split())
# Redirect commands to their correct functions
if known_args.cmd:
if known_args.cmd in ("..", "q"):
known_args.cmd = "quit"
elif known_args.cmd in ("?", "h"):
known_args.cmd = "help"
elif known_args.cmd == "r":
known_args.cmd = "reset"
getattr(
self,
"call_" + known_args.cmd,
lambda _: "Command not recognized!",
)(other_args)
return self.queue
def call_cls(self, _):
"""Process cls command"""
system_clear()
def call_home(self, _):
"""Process home command"""
self.queue.insert(0, "quit")
self.queue.insert(0, "quit")
def call_help(self, _):
"""Process help command"""
self.print_help()
def call_quit(self, _):
"""Process quit menu command"""
print("")
self.queue.insert(0, "quit")
def call_exit(self, _):
"""Process exit terminal command"""
self.queue.insert(0, "quit")
self.queue.insert(0, "quit")
self.queue.insert(0, "quit")
def call_reset(self, _):
"""Process reset command"""
self.queue.insert(0, "scr")
self.queue.insert(0, "etf")
self.queue.insert(0, "reset")
self.queue.insert(0, "quit")
self.queue.insert(0, "quit")
@try_except
def call_view(self, other_args: List[str]):
"""Process view command"""
parser = argparse.ArgumentParser(
add_help=False,
prog="view",
description="""View available presets under presets folder.""",
)
parser.add_argument(
"-p",
"--preset",
action="store",
dest="preset",
type=str,
help="View specific custom preset",
default="",
choices=self.preset_choices,
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-p")
ns_parser = parse_known_args_and_warn(parser, other_args)
if ns_parser:
if ns_parser.preset:
preset_filter = configparser.RawConfigParser()
preset_filter.optionxform = str # type: ignore
preset_filter.read(presets_path + ns_parser.preset + ".ini")
headers = [
"Price",
"Assets",
"NAV",
"Expense",
"PE",
"DivYield",
"Volume",
"Beta",
"N_Hold",
"Open",
"PrevClose",
"YrLow",
"YrHigh",
]
print("")
for filter_header in headers:
print(f" - {filter_header} -")
d_filters = {**preset_filter[filter_header]}
d_filters = {k: v for k, v in d_filters.items() if v}
if d_filters:
max_len = len(max(d_filters, key=len))
for key, value in d_filters.items():
print(f"{key}{(max_len-len(key))*' '}: {value}")
print("")
else:
print("\nPresets:")
for preset in self.preset_choices:
with open(
presets_path + preset + ".ini",
encoding="utf8",
) as f:
description = ""
for line in f:
if line.strip() == "[Price]":
break
description += line.strip()
print(
f" {preset}{(30-len(preset)) * ' '}{description.split('Description: ')[1].replace('#', '')}"
)
print("")
@try_except
def call_set(self, other_args: List[str]):
"""Process set command"""
parser = argparse.ArgumentParser(
add_help=False,
prog="set",
description="""Set preset.""",
)
parser.add_argument(
"-p",
"--preset",
action="store",
dest="preset",
type=str,
default="template",
help="Filter presets",
choices=self.preset_choices,
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-p")
ns_parser = parse_known_args_and_warn(parser, other_args)
if ns_parser:
self.preset = ns_parser.preset
print("")
@try_except
def call_screen(self, other_args):
"""Process screen command"""
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="screen",
add_help=False,
description="Screens ETFS from a personal scraping github repository. Data scraped from stockanalysis.com",
)
parser.add_argument(
"-l",
"--limit",
type=int,
help="Limit of etfs to display",
dest="limit",
default=10,
)
parser.add_argument(
"-s",
"--sort",
dest="sortby",
type=str,
help="Sort by given column. Default: Assets",
default="Assets",
choices=self.sortby_screen_choices,
)
parser.add_argument(
"-a",
"--ascend",
action="store_true",
help="Flag to sort in ascending order (lowest on top)",
dest="ascend",
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-l")
ns_parser = parse_known_args_and_warn(
parser, other_args, export_allowed=EXPORT_ONLY_RAW_DATA_ALLOWED
)
if ns_parser:
screener_view.view_screener(
preset=self.preset,
num_to_show=ns_parser.limit,
sortby=ns_parser.sortby,
ascend=ns_parser.ascend,
export=ns_parser.export,
)
@try_except
def call_sbc(self, other_args: List[str]):
"""Process sbc command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="sbc",
description="Search by category [Source: FinanceDatabase/StockAnalysis.com]",
)
parser.add_argument(
"-c",
"--category",
type=str,
dest="category",
nargs="+",
help="Category to look for",
required="-h" not in other_args,
)
parser.add_argument(
"-l",
"--limit",
type=check_positive,
dest="limit",
help="Limit of ETFs to display",
default=5,
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-c")
ns_parser = parse_known_args_and_warn(
parser, other_args, export_allowed=EXPORT_ONLY_RAW_DATA_ALLOWED
)
if ns_parser:
category = " ".join(ns_parser.category)
if category in financedatabase_model.get_etfs_categories():
financedatabase_view.display_etf_by_category(
category=category,
limit=ns_parser.limit,
export=ns_parser.export,
)
else:
print(
"The category selected does not exist, choose one from:"
f" {', '.join(financedatabase_model.get_etfs_categories())}\n"
)
def menu(
queue: List[str] = None,
):
"""Screener Menu"""
scr_controller = ScreenerController(queue)
an_input = "HELP_ME"
while True:
# There is a command in the queue
if scr_controller.queue and len(scr_controller.queue) > 0:
# If the command is quitting the menu we want to return in here
if scr_controller.queue[0] in ("q", "..", "quit"):
if len(scr_controller.queue) > 1:
return scr_controller.queue[1:]
return []
# Consume 1 element from the queue
an_input = scr_controller.queue[0]
scr_controller.queue = scr_controller.queue[1:]
# Print the current location because this was an instruction and we want user to know what was the action
if an_input and an_input.split(" ")[0] in scr_controller.CHOICES_COMMANDS:
print(f"{get_flair()} /etf/scr/ $ {an_input}")
# Get input command from user
else:
# Display help menu when entering on this menu from a level above
if an_input == "HELP_ME":
scr_controller.print_help()
# Get input from user using auto-completion
if session and gtff.USE_PROMPT_TOOLKIT and scr_controller.completer:
an_input = session.prompt(
f"{get_flair()} /etf/scr/ $ ",
completer=scr_controller.completer,
search_ignore_case=True,
)
# Get input from user without auto-completion
else:
an_input = input(f"{get_flair()} /etf/scr/ $ ")
try:
# Process the input command
scr_controller.queue = scr_controller.switch(an_input)
except SystemExit:
print(
f"\nThe command '{an_input}' doesn't exist on the /etf/scr menu.",
end="",
)
similar_cmd = difflib.get_close_matches(
an_input.split(" ")[0] if " " in an_input else an_input,
scr_controller.CHOICES,
n=1,
cutoff=0.7,
)
if similar_cmd:
if " " in an_input:
candidate_input = (
f"{similar_cmd[0]} {' '.join(an_input.split(' ')[1:])}"
)
else:
candidate_input = similar_cmd[0]
if candidate_input == an_input:
an_input = ""
scr_controller.queue = []
print("\n")
continue
print(f" Replacing by '{an_input}'.")
scr_controller.queue.insert(0, an_input)
else:
print("\n")
an_input = ""
scr_controller.queue = []

View File

@@ -0,0 +1,57 @@
"""ETF Screener view"""
__docformat__ = "numpy"
import os
from tabulate import tabulate
from gamestonk_terminal.etf.screener import screener_model
from gamestonk_terminal.helper_funcs import export_data
from gamestonk_terminal import feature_flags as gtff
# pylint:disable=no-member
def view_screener(
preset: str, num_to_show: int, sortby: str, ascend: bool, export: str = ""
):
"""Display screener output
Parameters
----------
preset: str
Preset to use
num_to_show: int
Number of etfs to show
sortby: str
Column to sort by
ascend: bool
Ascend when sorted
export: str
Output format of export
"""
screened_data = screener_model.etf_screener(preset)
screened_data = screened_data.sort_values(by=sortby, ascending=ascend)
if gtff.USE_TABULATE_DF:
print(
tabulate(
screened_data.head(num_to_show).fillna(""),
tablefmt="fancy_grid",
headers=screened_data.columns,
showindex=True,
disable_numparse=True,
)
)
else:
print(screened_data.head(num_to_show).fillna("").to_string())
print("")
export_data(
export,
os.path.dirname(os.path.abspath(__file__)),
"screen",
screened_data,
)

View File

@@ -1,46 +0,0 @@
"""ETF Screener view"""
__docformat__ = "numpy"
import os
from tabulate import tabulate
from gamestonk_terminal.etf import screener_model
from gamestonk_terminal.helper_funcs import export_data
# pylint:disable=no-member
def view_screener(num_to_show: int, preset: str, export: str):
"""Display screener output
Parameters
----------
num_to_show: int
Number of etfs to show
preset: str
Preset to use
export: str
Output format of export
"""
screened_data = screener_model.etf_screener(preset)
if screened_data.shape[0] > int(num_to_show):
screened_data = screened_data.sample(num_to_show)
print(
tabulate(
screened_data.fillna(""),
tablefmt="fancy_grid",
headers=screened_data.columns,
showindex=True,
disable_numparse=True,
)
)
print("")
export_data(
export,
os.path.dirname(os.path.abspath(__file__)),
"screener",
screened_data,
)

View File

@@ -80,22 +80,24 @@ def get_etf_holdings(symbol: str) -> pd.DataFrame:
link = f"https://stockanalysis.com/etf/{symbol}/holdings/"
r = requests.get(link, headers={"User-Agent": get_user_agent()})
soup = bs(r.text, "html.parser")
soup = soup.find("table")
tds = soup.findAll("td")
tickers = []
for i in tds[1::5]:
tickers.append(i.text)
percents = []
for i in tds[3::5]:
percents.append(i.text)
shares = []
for i in tds[4::5]:
shares.append(i.text)
df = pd.DataFrame(index=tickers)
df["% Of Etf"] = percents
df["Shares"] = shares
return df
if r.status_code == 200:
soup = bs(r.text, "html.parser")
soup = soup.find("table")
tds = soup.findAll("td")
tickers = []
for i in tds[1::5]:
tickers.append(i.text)
percents = []
for i in tds[3::5]:
percents.append(i.text)
shares = []
for i in tds[4::5]:
shares.append(i.text)
df = pd.DataFrame(index=tickers)
df["% Of Etf"] = percents
df["Shares"] = shares
return df
return pd.DataFrame()
def compare_etfs(symbols: List[str]) -> pd.DataFrame:
@@ -119,23 +121,30 @@ def compare_etfs(symbols: List[str]) -> pd.DataFrame:
return df_compare
def search_etfs(to_search) -> List[str]:
"""Search for an etf string in list of ETFs
def get_etfs_by_name(name_to_search: str) -> pd.DataFrame:
"""Get an ETF symbol and name based on ETF string to search. [Source: StockAnalysis]
Parameters
----------
to_search: str
String to match
name_to_search: str
ETF name to match
Returns
-------
matching_etfs: List[str]
List of matching ETF names
df: pd.Dataframe
Dataframe with symbols and names
"""
all_symbols, all_names = get_all_names_symbols()
matching_etfs = [
all_symbols[idx] + " - " + etf
for idx, etf in enumerate(all_names)
if to_search.lower() in etf.lower()
]
return matching_etfs
filtered_symbols = list()
filtered_names = list()
for symbol, name in zip(all_symbols, all_names):
if name_to_search.lower() in name.lower():
filtered_symbols.append(symbol)
filtered_names.append(name)
df = pd.DataFrame(
list(zip(filtered_symbols, filtered_names)), columns=["Symbol", "Name"]
)
return df

View File

@@ -4,9 +4,8 @@ __docformat__ = "numpy"
import os
from typing import List
import pandas as pd
from tabulate import tabulate
from gamestonk_terminal import feature_flags as gtff
from gamestonk_terminal.etf import stockanalysis_model
from gamestonk_terminal.helper_funcs import export_data
@@ -47,14 +46,18 @@ def view_holdings(symbol: str, num_to_show: int, export: str):
"""
data = stockanalysis_model.get_etf_holdings(symbol)
print(
tabulate(
data[:num_to_show],
headers=data.columns,
tablefmt="fancy_grid",
),
"\n",
)
if gtff.USE_TABULATE_DF:
print(
tabulate(
data[:num_to_show],
headers=data.columns,
tablefmt="fancy_grid",
),
"\n",
)
else:
print(data.head(num_to_show).to_string(), "\n")
export_data(export, os.path.dirname(os.path.abspath(__file__)), "holdings", data)
@@ -72,34 +75,50 @@ def view_comparisons(symbols: List[str], export: str):
etf_list = stockanalysis_model.get_all_names_symbols()[0]
for etf in symbols:
if etf not in etf_list:
print(f"{etf} not a known symbol. ")
print(f"{etf} not a known symbol.\n")
etf_list.remove(etf)
data = stockanalysis_model.compare_etfs(symbols)
print(tabulate(data, headers=data.columns, tablefmt="fancy_grid"), "\n")
if data.empty:
print("No data found for given ETFs\n")
return
if gtff.USE_TABULATE_DF:
print(tabulate(data, headers=data.columns, tablefmt="fancy_grid"), "\n")
else:
print(data.to_string())
export_data(export, os.path.dirname(os.path.abspath(__file__)), "overview", data)
def view_search(to_match: str, export: str):
"""Display ETFs matching search string
def display_etf_by_name(name: str, limit: int, export: str):
"""Display ETFs matching search string. [Source: StockAnalysis]
Parameters
----------
to_match: str
name: str
String being matched
limit: int
Limit of ETFs to display
export: str
Export to given file type
"""
matching_etfs = stockanalysis_model.search_etfs(to_match)
matching_etfs = stockanalysis_model.get_etfs_by_name(name)
if gtff.USE_TABULATE_DF:
print(
tabulate(
matching_etfs.head(limit),
showindex=False,
tablefmt="fancy_grid",
),
"\n",
)
else:
print(matching_etfs.head(limit).to_string(), "\n")
export_data(
export,
os.path.dirname(os.path.abspath(__file__)),
"search",
pd.DataFrame(data=matching_etfs),
"ln_sa",
matching_etfs,
)
print(*matching_etfs, sep="\n")
if len(matching_etfs) == 0:
print("No matches found")
print("")

File diff suppressed because it is too large Load Diff

View File

@@ -1,42 +0,0 @@
"""WSJ view"""
__docformat__ = "numpy"
import os
from tabulate import tabulate
from gamestonk_terminal.etf import wsj_model
from gamestonk_terminal.helper_funcs import (
export_data,
)
def show_top_mover(sort_type: str, limit: int = 20, export=""):
"""
Show top ETF movers from wsj.com
Parameters
----------
sort_type: str
What to show. Either Gainers, Decliners or Activity
limit: int
Number of etfs to show
export: str
Format to export data
"""
data = wsj_model.etf_movers(sort_type)
print(
tabulate(
data.iloc[:limit],
showindex=False,
headers=data.columns,
floatfmt=".2f",
tablefmt="fancy_grid",
)
)
export_data(
export,
os.path.join(os.path.dirname(os.path.abspath(__file__)), "movers"),
sort_type,
data,
)
print("")

View File

@@ -0,0 +1,54 @@
"""Yahoo Finance model"""
__docformat__ = "numpy"
from typing import Dict
import yfinance as yf
def get_etf_sector_weightings(name: str) -> Dict:
"""Return sector weightings allocation of ETF. [Source: Yahoo Finance]
Parameters
----------
name: str
ETF name
Returns
----------
Dict
Dictionary with sector weightings allocation
"""
weights = yf.Ticker(name).info
if "sectorWeightings" in weights:
sectors = {}
for item in weights["sectorWeightings"]:
k = list(item.keys())[0].replace("_", " ").title()
k = "Real Estate" if k == "Realestate" else k
v = round(100 * list(item.values())[0], 2)
sectors[k] = v
sectors = dict(sorted(sectors.items(), key=lambda x: x[1], reverse=True))
return sectors
return dict()
def get_etf_summary_description(name: str) -> str:
"""Return summary description of ETF. [Source: Yahoo Finance]
Parameters
----------
name: str
ETF name
Returns
----------
str
Summary description of the ETF
"""
data = yf.Ticker(name).info
if "longBusinessSummary" in data:
return data["longBusinessSummary"]
return ""

View File

@@ -0,0 +1,101 @@
"""Yahoo Finance view"""
__docformat__ = "numpy"
import os
import pandas as pd
from matplotlib import pyplot as plt
from tabulate import tabulate
from gamestonk_terminal.etf import yfinance_model
from gamestonk_terminal import feature_flags as gtff
from gamestonk_terminal.config_plot import PLOT_DPI
from gamestonk_terminal.helper_funcs import plot_autoscale, export_data
def display_etf_weightings(
name: str,
raw: bool = False,
min_pct_to_display: float = 5,
export: str = "",
):
"""Display sector weightings allocation of ETF. [Source: Yahoo Finance]
Parameters
----------
name: str
ETF name
raw: bool
Display sector weighting allocation
min_pct_to_display: float
Minimum percentage to display sector
export: str
Type of format to export data
"""
sectors = yfinance_model.get_etf_sector_weightings(name)
if not sectors:
print("No data was found for that ETF\n")
return
holdings = pd.DataFrame(sectors, index=[0]).T
title = f"Sector holdings of {name}"
if raw:
print(f"\n{title}")
holdings.columns = ["% of holdings in the sector"]
if gtff.USE_TABULATE_DF:
print(
tabulate(
holdings,
headers=holdings.columns,
showindex=True,
tablefmt="fancy_grid",
),
)
else:
print(holdings.to_string, "\n")
else:
main_holdings = holdings[holdings.values > min_pct_to_display].to_dict()[
holdings.columns[0]
]
if len(main_holdings) < len(holdings):
main_holdings["Others"] = 100 - sum(main_holdings.values())
legend, values = zip(*main_holdings.items())
leg = [f"{le}\n{round(v,2)}%" for le, v in zip(legend, values)]
plt.subplots(figsize=plot_autoscale(), dpi=PLOT_DPI)
plt.pie(
values,
labels=leg,
wedgeprops={"linewidth": 0.5, "edgecolor": "white"},
labeldistance=1.05,
startangle=90,
)
plt.title(title)
plt.tight_layout()
if gtff.USE_ION:
plt.ion()
plt.show()
print("")
export_data(export, os.path.dirname(os.path.abspath(__file__)), "weights", holdings)
def display_etf_description(
name: str,
):
"""Display ETF description summary. [Source: Yahoo Finance]
Parameters
----------
name: str
ETF name
"""
description = yfinance_model.get_etf_summary_description(name)
if not description:
print("No data was found for that ETF\n")
return
print(description, "\n")

View File

@@ -129,7 +129,7 @@ class ScreenerController:
view view available presets (defaults and customs)
set set one of the available presets
PRESET: {self.preset}
PRESET: {self.preset}
historical view historical price
overview overview (e.g. Sector, Industry, Market Cap, Volume)

View File

@@ -734,6 +734,7 @@ Custom:
# Daily
if self.interval == "1440min":
print("VWAP should be used with intraday data. \n")
return
overlap_view.view_vwap(
s_ticker=self.ticker,

19
scripts/test_etf.gst vendored
View File

@@ -1,12 +1,11 @@
etf
gainers
decliners
active
q
etf
reset
search blackrock
holdings qqq -l 5
overview spy
compare spy,qqq
load spy
overview
holdings
weights
summary
candle
news
pir
compare spy,ivv
exit

7
scripts/test_etf_ca.gst vendored Normal file
View File

@@ -0,0 +1,7 @@
etf
ln oil
ld oil
load spy
ca
volume
exit

6
scripts/test_etf_disc.gst vendored Normal file
View File

@@ -0,0 +1,6 @@
etf
disc
gainers
decliners
active
exit

5
scripts/test_etf_pred.gst vendored Normal file
View File

@@ -0,0 +1,5 @@
etf
load spy
pred
mc
exit

8
scripts/test_etf_scr.gst vendored Normal file
View File

@@ -0,0 +1,8 @@
etf
scr
view
view etf_config
set etf_config
screen
sbc Bank Loan
exit

5
scripts/test_etf_ta.gst vendored Normal file
View File

@@ -0,0 +1,5 @@
etf
load spy
ta
ema
exit