From 78f110b389ad0a8885de3c6e8f5dbc4819d21d8e Mon Sep 17 00:00:00 2001 From: Didier Rodrigues Lopes Date: Thu, 30 Dec 2021 03:21:21 +0000 Subject: [PATCH] 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 --- .github/workflows/test.yml | 2 +- .pre-commit-config.yaml | 2 +- .../etf/discovery/disc_controller.py | 307 ++++ .../etf/{ => discovery}/wsj_model.py | 0 gamestonk_terminal/etf/discovery/wsj_view.py | 50 + gamestonk_terminal/etf/etf_controller.py | 797 ++++++---- .../etf/financedatabase_model.py | 73 + .../etf/financedatabase_view.py | 149 +- .../prediction_techniques/pred_controller.py | 983 ++++++++++++ .../etf/{ => screener}/presets/etf_config.ini | 3 + .../etf/screener/screener_controller.py | 471 ++++++ .../etf/{ => screener}/screener_model.py | 0 .../etf/screener/screener_view.py | 57 + gamestonk_terminal/etf/screener_view.py | 46 - gamestonk_terminal/etf/stockanalysis_model.py | 65 +- gamestonk_terminal/etf/stockanalysis_view.py | 63 +- .../etf/technical_analysis/ta_controller.py | 1351 +++++++++++++++++ gamestonk_terminal/etf/wsj_view.py | 42 - gamestonk_terminal/etf/yfinance_model.py | 54 + gamestonk_terminal/etf/yfinance_view.py | 101 ++ .../stocks/screener/screener_controller.py | 2 +- .../technical_analysis/ta_controller.py | 1 + scripts/test_etf.gst | 19 +- scripts/test_etf_ca.gst | 7 + scripts/test_etf_disc.gst | 6 + scripts/test_etf_pred.gst | 5 + scripts/test_etf_scr.gst | 8 + scripts/test_etf_ta.gst | 5 + 28 files changed, 4209 insertions(+), 460 deletions(-) create mode 100644 gamestonk_terminal/etf/discovery/disc_controller.py rename gamestonk_terminal/etf/{ => discovery}/wsj_model.py (100%) create mode 100644 gamestonk_terminal/etf/discovery/wsj_view.py create mode 100644 gamestonk_terminal/etf/financedatabase_model.py create mode 100644 gamestonk_terminal/etf/prediction_techniques/pred_controller.py rename gamestonk_terminal/etf/{ => screener}/presets/etf_config.ini (78%) create mode 100644 gamestonk_terminal/etf/screener/screener_controller.py rename gamestonk_terminal/etf/{ => screener}/screener_model.py (100%) create mode 100644 gamestonk_terminal/etf/screener/screener_view.py delete mode 100644 gamestonk_terminal/etf/screener_view.py create mode 100644 gamestonk_terminal/etf/technical_analysis/ta_controller.py delete mode 100644 gamestonk_terminal/etf/wsj_view.py create mode 100644 gamestonk_terminal/etf/yfinance_model.py create mode 100644 gamestonk_terminal/etf/yfinance_view.py create mode 100644 scripts/test_etf_ca.gst create mode 100644 scripts/test_etf_disc.gst create mode 100644 scripts/test_etf_pred.gst create mode 100644 scripts/test_etf_scr.gst create mode 100644 scripts/test_etf_ta.gst diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6ec9696af03..da895e9778a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7d12a8a88ad..a869afa75c8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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: diff --git a/gamestonk_terminal/etf/discovery/disc_controller.py b/gamestonk_terminal/etf/discovery/disc_controller.py new file mode 100644 index 00000000000..958ab0353ad --- /dev/null +++ b/gamestonk_terminal/etf/discovery/disc_controller.py @@ -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 = [] diff --git a/gamestonk_terminal/etf/wsj_model.py b/gamestonk_terminal/etf/discovery/wsj_model.py similarity index 100% rename from gamestonk_terminal/etf/wsj_model.py rename to gamestonk_terminal/etf/discovery/wsj_model.py diff --git a/gamestonk_terminal/etf/discovery/wsj_view.py b/gamestonk_terminal/etf/discovery/wsj_view.py new file mode 100644 index 00000000000..1efe3d84f3c --- /dev/null +++ b/gamestonk_terminal/etf/discovery/wsj_view.py @@ -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("") diff --git a/gamestonk_terminal/etf/etf_controller.py b/gamestonk_terminal/etf/etf_controller.py index 4bead1a0186..19e21f09bfb 100644 --- a/gamestonk_terminal/etf/etf_controller.py +++ b/gamestonk_terminal/etf/etf_controller.py @@ -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 ' 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 ' 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 ' 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) diff --git a/gamestonk_terminal/etf/financedatabase_model.py b/gamestonk_terminal/etf/financedatabase_model.py new file mode 100644 index 00000000000..49e0b10c2bd --- /dev/null +++ b/gamestonk_terminal/etf/financedatabase_model.py @@ -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") diff --git a/gamestonk_terminal/etf/financedatabase_view.py b/gamestonk_terminal/etf/financedatabase_view.py index ef039ecb3a7..8c76ebd85af 100644 --- a/gamestonk_terminal/etf/financedatabase_view.py +++ b/gamestonk_terminal/etf/financedatabase_view.py @@ -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, + ) diff --git a/gamestonk_terminal/etf/prediction_techniques/pred_controller.py b/gamestonk_terminal/etf/prediction_techniques/pred_controller.py new file mode 100644 index 00000000000..0c4c88a7cf2 --- /dev/null +++ b/gamestonk_terminal/etf/prediction_techniques/pred_controller.py @@ -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 = [] diff --git a/gamestonk_terminal/etf/presets/etf_config.ini b/gamestonk_terminal/etf/screener/presets/etf_config.ini similarity index 78% rename from gamestonk_terminal/etf/presets/etf_config.ini rename to gamestonk_terminal/etf/screener/presets/etf_config.ini index c0ae48f612a..a067cd4cd37 100644 --- a/gamestonk_terminal/etf/presets/etf_config.ini +++ b/gamestonk_terminal/etf/screener/presets/etf_config.ini @@ -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 diff --git a/gamestonk_terminal/etf/screener/screener_controller.py b/gamestonk_terminal/etf/screener/screener_controller.py new file mode 100644 index 00000000000..aeb9d9378be --- /dev/null +++ b/gamestonk_terminal/etf/screener/screener_controller.py @@ -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 = [] diff --git a/gamestonk_terminal/etf/screener_model.py b/gamestonk_terminal/etf/screener/screener_model.py similarity index 100% rename from gamestonk_terminal/etf/screener_model.py rename to gamestonk_terminal/etf/screener/screener_model.py diff --git a/gamestonk_terminal/etf/screener/screener_view.py b/gamestonk_terminal/etf/screener/screener_view.py new file mode 100644 index 00000000000..4933852e712 --- /dev/null +++ b/gamestonk_terminal/etf/screener/screener_view.py @@ -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, + ) diff --git a/gamestonk_terminal/etf/screener_view.py b/gamestonk_terminal/etf/screener_view.py deleted file mode 100644 index 1b8c89e797c..00000000000 --- a/gamestonk_terminal/etf/screener_view.py +++ /dev/null @@ -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, - ) diff --git a/gamestonk_terminal/etf/stockanalysis_model.py b/gamestonk_terminal/etf/stockanalysis_model.py index 530e4152cd1..569b36d4331 100644 --- a/gamestonk_terminal/etf/stockanalysis_model.py +++ b/gamestonk_terminal/etf/stockanalysis_model.py @@ -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 diff --git a/gamestonk_terminal/etf/stockanalysis_view.py b/gamestonk_terminal/etf/stockanalysis_view.py index 94bb28fe729..f4453abf837 100644 --- a/gamestonk_terminal/etf/stockanalysis_view.py +++ b/gamestonk_terminal/etf/stockanalysis_view.py @@ -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("") diff --git a/gamestonk_terminal/etf/technical_analysis/ta_controller.py b/gamestonk_terminal/etf/technical_analysis/ta_controller.py new file mode 100644 index 00000000000..0eb54199184 --- /dev/null +++ b/gamestonk_terminal/etf/technical_analysis/ta_controller.py @@ -0,0 +1,1351 @@ +"""Technical Analysis Controller Module""" +__docformat__ = "numpy" +# pylint:disable=too-many-lines +# pylint:disable=R0904,C0201 + +import argparse +import difflib +from typing import List, Union +from datetime import datetime + +import pandas as pd +from prompt_toolkit.completion import NestedCompleter + +from gamestonk_terminal import feature_flags as gtff +from gamestonk_terminal.helper_funcs import ( + EXPORT_BOTH_RAW_DATA_AND_FIGURES, + get_flair, + parse_known_args_and_warn, + check_positive_list, + check_positive, + try_except, + valid_date, + system_clear, +) +from gamestonk_terminal.menu import session +from gamestonk_terminal.common.technical_analysis import ( + custom_indicators_view, + momentum_view, + overlap_model, + overlap_view, + trend_indicators_view, + volatility_model, + volatility_view, + volume_view, +) + + +class TechnicalAnalysisController: + """Technical Analysis Controller class""" + + # Command choices + CHOICES = [ + "cls", + "home", + "h", + "?", + "help", + "q", + "quit", + "..", + "exit", + "r", + "reset", + ] + + CHOICES_COMMANDS = [ + "load", + "ema", + "sma", + "wma", + "hma", + "zlma", + "cci", + "macd", + "rsi", + "stoch", + "fisher", + "cg", + "adx", + "aroon", + "bbands", + "donchian", + "kc", + "ad", + "adosc", + "obv", + "fib", + ] + + CHOICES += CHOICES_COMMANDS + + def __init__( + self, + ticker: str, + start: datetime, + data: pd.DataFrame, + queue: List[str] = None, + ): + """Constructor""" + self.ticker = ticker + self.start = start + self.data = data + + self.ta_parser = argparse.ArgumentParser(add_help=False, prog="ta") + self.ta_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 = f""" + +ETF: {self.ticker} + +Overlap: + ema exponential moving average + sma simple moving average + wma weighted moving average + hma hull moving average + zlma zero lag moving average +Momentum: + cci commodity channel index + macd moving average convergence/divergence + rsi relative strength index + stoch stochastic oscillator + fisher fisher transform + cg centre of gravity +Trend: + adx average directional movement index + aroon aroon indicator +Volatility: + bbands bollinger bands + donchian donchian channels + kc keltner channels +Volume: + ad accumulation/distribution line + adosc chaikin oscillator + obv on balance volume +Custom: + fib fibonacci retracement +""" + 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.ta_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, "ta") + if self.ticker: + self.queue.insert(0, f"load {self.ticker}") + self.queue.insert(0, "etf") + self.queue.insert(0, "reset") + self.queue.insert(0, "quit") + self.queue.insert(0, "quit") + + @try_except + def call_ema(self, other_args: List[str]): + """Process ema command""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="ema", + description=""" + The Exponential Moving Average is a staple of technical + analysis and is used in countless technical indicators. In a Simple Moving + Average, each value in the time period carries equal weight, and values outside + of the time period are not included in the average. However, the Exponential + Moving Average is a cumulative calculation, including all data. Past values have + a diminishing contribution to the average, while more recent values have a greater + contribution. This method allows the moving average to be more responsive to changes + in the data. + """, + ) + parser.add_argument( + "-l", + "--length", + action="store", + dest="n_length", + type=check_positive_list, + default=overlap_model.WINDOW_LENGTHS, + help="Window lengths. Multiple values indicated as comma separated values.", + ) + parser.add_argument( + "-o", + "--offset", + action="store", + dest="n_offset", + type=int, + default=0, + help="offset", + ) + + 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_BOTH_RAW_DATA_AND_FIGURES + ) + if ns_parser: + overlap_view.view_ma( + ma_type="EMA", + s_ticker=self.ticker, + s_interval="1440min", + df_stock=self.data, + length=ns_parser.n_length, + offset=ns_parser.n_offset, + export=ns_parser.export, + ) + + @try_except + def call_sma(self, other_args: List[str]): + """Process sma command""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="sma", + description=""" + Moving Averages are used to smooth the data in an array to + help eliminate noise and identify trends. The Simple Moving Average is literally + the simplest form of a moving average. Each output value is the average of the + previous n values. In a Simple Moving Average, each value in the time period carries + equal weight, and values outside of the time period are not included in the average. + This makes it less responsive to recent changes in the data, which can be useful for + filtering out those changes. + """, + ) + parser.add_argument( + "-l", + "--length", + action="store", + dest="n_length", + type=check_positive_list, + default=overlap_model.WINDOW_LENGTHS, + help="Window lengths. Multiple values indicated as comma separated values. ", + ) + parser.add_argument( + "-o", + "--offset", + action="store", + dest="n_offset", + type=int, + default=0, + help="offset", + ) + + 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_BOTH_RAW_DATA_AND_FIGURES + ) + if ns_parser: + overlap_view.view_ma( + ma_type="SMA", + s_ticker=self.ticker, + s_interval="1440min", + df_stock=self.data, + length=ns_parser.n_length, + offset=ns_parser.n_offset, + export=ns_parser.export, + ) + + @try_except + def call_wma(self, other_args: List[str]): + """Process wma command""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="wma", + description=""" + A Weighted Moving Average puts more weight on recent data and less on past data. + This is done by multiplying each bar’s price by a weighting factor. Because of its + unique calculation, WMA will follow prices more closely than a corresponding Simple + Moving Average. + """, + ) + parser.add_argument( + "-l", + "--length", + action="store", + dest="n_length", + type=check_positive_list, + default=overlap_model.WINDOW_LENGTHS, + help="Window lengths. Multiple values indicated as comma separated values. ", + ) + parser.add_argument( + "-o", + "--offset", + action="store", + dest="n_offset", + type=int, + default=0, + help="offset", + ) + + 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_BOTH_RAW_DATA_AND_FIGURES + ) + if ns_parser: + overlap_view.view_ma( + ma_type="WMA", + s_ticker=self.ticker, + s_interval="1440min", + df_stock=self.data, + length=ns_parser.n_length, + offset=ns_parser.n_offset, + export=ns_parser.export, + ) + + @try_except + def call_hma(self, other_args: List[str]): + """Process hma command""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="hma", + description=""" + The Hull Moving Average solves the age old dilemma of making a moving average + more responsive to current price activity whilst maintaining curve smoothness. + In fact the HMA almost eliminates lag altogether and manages to improve smoothing + at the same time. + """, + ) + parser.add_argument( + "-l", + "--length", + action="store", + dest="n_length", + type=check_positive_list, + default=overlap_model.WINDOW_LENGTHS2, + help="Window lengths. Multiple values indicated as comma separated values. ", + ) + parser.add_argument( + "-o", + "--offset", + action="store", + dest="n_offset", + type=int, + default=0, + help="offset", + ) + + 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_BOTH_RAW_DATA_AND_FIGURES + ) + if ns_parser: + overlap_view.view_ma( + ma_type="HMA", + s_ticker=self.ticker, + s_interval="1440min", + df_stock=self.data, + length=ns_parser.n_length, + offset=ns_parser.n_offset, + export=ns_parser.export, + ) + + @try_except + def call_zlma(self, other_args: List[str]): + """Process zlma command""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="zlma", + description=""" + The zero lag exponential moving average (ZLEMA) indicator + was created by John Ehlers and Ric Way. The idea is do a + regular exponential moving average (EMA) calculation but + on a de-lagged data instead of doing it on the regular data. + Data is de-lagged by removing the data from "lag" days ago + thus removing (or attempting to) the cumulative effect of + the moving average. + """, + ) + parser.add_argument( + "-l", + "--length", + action="store", + dest="n_length", + type=check_positive_list, + default=[20], + help="Window lengths. Multiple values indicated as comma separated values.", + ) + parser.add_argument( + "-o", + "--offset", + action="store", + dest="n_offset", + type=int, + default=0, + help="offset", + ) + + 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_BOTH_RAW_DATA_AND_FIGURES + ) + if ns_parser: + overlap_view.view_ma( + ma_type="ZLMA", + s_ticker=self.ticker, + s_interval="1440min", + df_stock=self.data, + length=ns_parser.n_length, + offset=ns_parser.n_offset, + export=ns_parser.export, + ) + + @try_except + def call_cci(self, other_args: List[str]): + """Process cci command""" + + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="cci", + description=""" + The CCI is designed to detect beginning and ending market trends. + The range of 100 to -100 is the normal trading range. CCI values outside of this + range indicate overbought or oversold conditions. You can also look for price + divergence in the CCI. If the price is making new highs, and the CCI is not, + then a price correction is likely. + """, + ) + + parser.add_argument( + "-l", + "--length", + action="store", + dest="n_length", + type=check_positive, + default=14, + help="length", + ) + parser.add_argument( + "-s", + "--scalar", + action="store", + dest="n_scalar", + type=check_positive, + default=0.015, + help="scalar", + ) + + ns_parser = parse_known_args_and_warn( + parser, other_args, EXPORT_BOTH_RAW_DATA_AND_FIGURES + ) + if ns_parser: + momentum_view.plot_cci( + s_ticker=self.ticker, + s_interval="1440min", + df_stock=self.data, + length=ns_parser.n_length, + scalar=ns_parser.n_scalar, + export=ns_parser.export, + ) + + @try_except + def call_macd(self, other_args: List[str]): + """Process macd command""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="macd", + description=""" + The Moving Average Convergence Divergence (MACD) is the difference + between two Exponential Moving Averages. The Signal line is an Exponential Moving + Average of the MACD. \n \n The MACD signals trend changes and indicates the start + of new trend direction. High values indicate overbought conditions, low values + indicate oversold conditions. Divergence with the price indicates an end to the + current trend, especially if the MACD is at extreme high or low values. When the MACD + line crosses above the signal line a buy signal is generated. When the MACD crosses + below the signal line a sell signal is generated. To confirm the signal, the MACD + should be above zero for a buy, and below zero for a sell. + """, + ) + + parser.add_argument( + "-f", + "--fast", + action="store", + dest="n_fast", + type=check_positive, + default=12, + help="The short period.", + ) + parser.add_argument( + "-s", + "--slow", + action="store", + dest="n_slow", + type=check_positive, + default=26, + help="The long period.", + ) + parser.add_argument( + "--signal", + action="store", + dest="n_signal", + type=check_positive, + default=9, + help="The signal period.", + ) + + ns_parser = parse_known_args_and_warn( + parser, other_args, EXPORT_BOTH_RAW_DATA_AND_FIGURES + ) + if ns_parser: + momentum_view.view_macd( + s_ticker=self.ticker, + s_interval="1440min", + df_stock=self.data, + n_fast=ns_parser.n_fast, + n_slow=ns_parser.n_slow, + n_signal=ns_parser.n_signal, + export=ns_parser.export, + ) + + @try_except + def call_rsi(self, other_args: List[str]): + """Process rsi command""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="rsi", + description=""" + The Relative Strength Index (RSI) calculates a ratio of the + recent upward price movements to the absolute price movement. The RSI ranges + from 0 to 100. The RSI is interpreted as an overbought/oversold indicator when + the value is over 70/below 30. You can also look for divergence with price. If + the price is making new highs/lows, and the RSI is not, it indicates a reversal. + """, + ) + + parser.add_argument( + "-l", + "--length", + action="store", + dest="n_length", + type=check_positive, + default=14, + help="length", + ) + parser.add_argument( + "-s", + "--scalar", + action="store", + dest="n_scalar", + type=check_positive, + default=100, + help="scalar", + ) + parser.add_argument( + "-d", + "--drift", + action="store", + dest="n_drift", + type=check_positive, + default=1, + help="drift", + ) + + 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_BOTH_RAW_DATA_AND_FIGURES + ) + if ns_parser: + momentum_view.view_rsi( + s_ticker=self.ticker, + s_interval="1440min", + df_stock=self.data, + length=ns_parser.n_length, + scalar=ns_parser.n_scalar, + drift=ns_parser.n_drift, + export=ns_parser.export, + ) + + @try_except + def call_stoch(self, other_args: List[str]): + """Process stoch command""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="stoch", + description=""" + The Stochastic Oscillator measures where the close is in relation + to the recent trading range. The values range from zero to 100. %D values over 75 + indicate an overbought condition; values under 25 indicate an oversold condition. + When the Fast %D crosses above the Slow %D, it is a buy signal; when it crosses + below, it is a sell signal. The Raw %K is generally considered too erratic to use + for crossover signals. + """, + ) + + parser.add_argument( + "-k", + "--fastkperiod", + action="store", + dest="n_fastkperiod", + type=check_positive, + default=14, + help="The time period of the fastk moving average", + ) + parser.add_argument( + "-d", + "--slowdperiod", + action="store", + dest="n_slowdperiod", + type=check_positive, + default=3, + help="The time period of the slowd moving average", + ) + parser.add_argument( + "--slowkperiod", + action="store", + dest="n_slowkperiod", + type=check_positive, + default=3, + help="The time period of the slowk moving average", + ) + + ns_parser = parse_known_args_and_warn( + parser, other_args, EXPORT_BOTH_RAW_DATA_AND_FIGURES + ) + if ns_parser: + momentum_view.view_stoch( + s_ticker=self.ticker, + s_interval="1440min", + df_stock=self.data, + fastkperiod=ns_parser.n_fastkperiod, + slowdperiod=ns_parser.n_slowdperiod, + slowkperiod=ns_parser.n_slowkperiod, + export=ns_parser.export, + ) + + @try_except + def call_fisher(self, other_args: List[str]): + """Process fisher command""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="fisher", + description=""" + The Fisher Transform is a technical indicator created by John F. Ehlers + that converts prices into a Gaussian normal distribution.1 The indicator + highlights when prices have moved to an extreme, based on recent prices. + This may help in spotting turning points in the price of an asset. It also + helps show the trend and isolate the price waves within a trend. + """, + ) + + parser.add_argument( + "-l", + "--length", + action="store", + dest="n_length", + type=check_positive, + default=14, + help="length", + ) + + 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_BOTH_RAW_DATA_AND_FIGURES + ) + if ns_parser: + momentum_view.view_fisher( + s_ticker=self.ticker, + s_interval="1440min", + df_stock=self.data, + length=ns_parser.n_length, + export=ns_parser.export, + ) + + @try_except + def call_cg(self, other_args: List[str]): + """Process cg command""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="cg", + description=""" + The Center of Gravity indicator, in short, is used to anticipate future price movements + and to trade on price reversals as soon as they happen. However, just like other oscillators, + the COG indicator returns the best results in range-bound markets and should be avoided when + the price is trending. Traders who use it will be able to closely speculate the upcoming + price change of the asset. + """, + ) + + parser.add_argument( + "-l", + "--length", + action="store", + dest="n_length", + type=check_positive, + default=14, + help="length", + ) + + 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_BOTH_RAW_DATA_AND_FIGURES + ) + if ns_parser: + momentum_view.view_cg( + s_ticker=self.ticker, + s_interval="1440min", + df_stock=self.data, + length=ns_parser.n_length, + export=ns_parser.export, + ) + + @try_except + def call_adx(self, other_args: List[str]): + """Process adx command""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="adx", + description=""" + The ADX is a Welles Wilder style moving average of the Directional Movement Index (DX). + The values range from 0 to 100, but rarely get above 60. To interpret the ADX, consider + a high number to be a strong trend, and a low number, a weak trend. + """, + ) + parser.add_argument( + "-l", + "--length", + action="store", + dest="n_length", + type=check_positive, + default=14, + help="length", + ) + parser.add_argument( + "-s", + "--scalar", + action="store", + dest="n_scalar", + type=check_positive, + default=100, + help="scalar", + ) + parser.add_argument( + "-d", + "--drift", + action="store", + dest="n_drift", + type=check_positive, + default=1, + help="drift", + ) + + 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_BOTH_RAW_DATA_AND_FIGURES + ) + if ns_parser: + trend_indicators_view.plot_adx( + s_ticker=self.ticker, + s_interval="1440min", + df_stock=self.data, + length=ns_parser.n_length, + scalar=ns_parser.n_scalar, + drift=ns_parser.n_drift, + export=ns_parser.export, + ) + + @try_except + def call_aroon(self, other_args: List[str]): + """Process aroon command""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="aroon", + description=""" + The word aroon is Sanskrit for "dawn's early light." The Aroon + indicator attempts to show when a new trend is dawning. The indicator consists + of two lines (Up and Down) that measure how long it has been since the highest + high/lowest low has occurred within an n period range. \n \n When the Aroon Up is + staying between 70 and 100 then it indicates an upward trend. When the Aroon Down + is staying between 70 and 100 then it indicates an downward trend. A strong upward + trend is indicated when the Aroon Up is above 70 while the Aroon Down is below 30. + Likewise, a strong downward trend is indicated when the Aroon Down is above 70 while + the Aroon Up is below 30. Also look for crossovers. When the Aroon Down crosses above + the Aroon Up, it indicates a weakening of the upward trend (and vice versa). + """, + ) + + parser.add_argument( + "-l", + "--length", + action="store", + dest="n_length", + type=check_positive, + default=25, + help="length", + ) + parser.add_argument( + "-s", + "--scalar", + action="store", + dest="n_scalar", + type=check_positive, + default=100, + help="scalar", + ) + + 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_BOTH_RAW_DATA_AND_FIGURES + ) + if ns_parser: + trend_indicators_view.plot_aroon( + s_ticker=self.ticker, + s_interval="1440min", + df_stock=self.data, + length=ns_parser.n_length, + scalar=ns_parser.n_scalar, + export=ns_parser.export, + ) + + @try_except + def call_bbands(self, other_args: List[str]): + """Process bbands command""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="bbands", + description=""" + Bollinger Bands consist of three lines. The middle band is a simple + moving average (generally 20 periods) of the typical price (TP). The upper and lower + bands are F standard deviations (generally 2) above and below the middle band. + The bands widen and narrow when the volatility of the price is higher or lower, + respectively. \n \nBollinger Bands do not, in themselves, generate buy or sell signals; + they are an indicator of overbought or oversold conditions. When the price is near the + upper or lower band it indicates that a reversal may be imminent. The middle band + becomes a support or resistance level. The upper and lower bands can also be + interpreted as price targets. When the price bounces off of the lower band and crosses + the middle band, then the upper band becomes the price target. + """, + ) + + parser.add_argument( + "-l", + "--length", + action="store", + dest="n_length", + type=check_positive, + default=5, + help="length", + ) + parser.add_argument( + "-s", + "--std", + action="store", + dest="n_std", + type=check_positive, + default=2, + help="std", + ) + parser.add_argument( + "-m", + "--mamode", + action="store", + dest="s_mamode", + default="sma", + help="mamode", + ) + + 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_BOTH_RAW_DATA_AND_FIGURES + ) + if ns_parser: + volatility_view.view_bbands( + ticker=self.ticker, + s_interval="1440min", + df_stock=self.data, + length=ns_parser.n_length, + n_std=ns_parser.n_std, + mamode=ns_parser.s_mamode, + export=ns_parser.export, + ) + + @try_except + def call_donchian(self, other_args: List[str]): + """Process donchian command""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="donchian", + description=""" + Donchian Channels are three lines generated by moving average + calculations that comprise an indicator formed by upper and lower + bands around a midrange or median band. The upper band marks the + highest price of a security over N periods while the lower band + marks the lowest price of a security over N periods. The area + between the upper and lower bands represents the Donchian Channel. + """, + ) + parser.add_argument( + "-u", + "--length_upper", + action="store", + dest="n_length_upper", + type=check_positive, + default=20, + help="length", + ) + parser.add_argument( + "-l", + "--length_lower", + action="store", + dest="n_length_lower", + type=check_positive, + default=20, + help="length", + ) + + ns_parser = parse_known_args_and_warn( + parser, other_args, EXPORT_BOTH_RAW_DATA_AND_FIGURES + ) + if ns_parser: + volatility_view.view_donchian( + ticker=self.ticker, + s_interval="1440min", + df_stock=self.data, + upper_length=ns_parser.n_length_upper, + lower_length=ns_parser.n_length_lower, + export=ns_parser.export, + ) + + @try_except + def call_kc(self, other_args: List[str]): + """Process kc command""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="kc", + description=""" + Keltner Channels are volatility-based bands that are placed + on either side of an asset's price and can aid in determining + the direction of a trend.The Keltner channel uses the average + true range (ATR) or volatility, with breaks above or below the top + and bottom barriers signaling a continuation. + """, + ) + parser.add_argument( + "-l", + "--length", + action="store", + dest="n_length", + type=check_positive, + default=20, + help="Window length", + ) + parser.add_argument( + "-s", + "--scalar", + action="store", + dest="n_scalar", + type=check_positive, + default=2, + help="scalar", + ) + parser.add_argument( + "-m", + "--mamode", + action="store", + dest="s_mamode", + default="ema", + choices=volatility_model.MAMODES, + help="mamode", + ) + parser.add_argument( + "-o", + "--offset", + action="store", + dest="n_offset", + type=int, + default=0, + help="offset", + ) + + 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_BOTH_RAW_DATA_AND_FIGURES + ) + if ns_parser: + volatility_view.view_kc( + s_ticker=self.ticker, + s_interval="1440min", + df_stock=self.data, + length=ns_parser.n_length, + scalar=ns_parser.n_scalar, + mamode=ns_parser.s_mamode, + offset=ns_parser.n_offset, + export=ns_parser.export, + ) + + @try_except + def call_ad(self, other_args: List[str]): + """Process ad command""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="ad", + description=""" + The Accumulation/Distribution Line is similar to the On Balance + Volume (OBV), which sums the volume times +1/-1 based on whether the close is + higher than the previous close. The Accumulation/Distribution indicator, however + multiplies the volume by the close location value (CLV). The CLV is based on the + movement of the issue within a single bar and can be +1, -1 or zero. \n \n + The Accumulation/Distribution Line is interpreted by looking for a divergence in + the direction of the indicator relative to price. If the Accumulation/Distribution + Line is trending upward it indicates that the price may follow. Also, if the + Accumulation/Distribution Line becomes flat while the price is still rising (or falling) + then it signals an impending flattening of the price. + """, + ) + parser.add_argument( + "--open", + action="store_true", + default=False, + dest="b_use_open", + help="uses open value of stock", + ) + + ns_parser = parse_known_args_and_warn( + parser, other_args, EXPORT_BOTH_RAW_DATA_AND_FIGURES + ) + if ns_parser: + volume_view.plot_ad( + s_ticker=self.ticker, + s_interval="1440min", + df_stock=self.data, + use_open=ns_parser.b_use_open, + export=ns_parser.export, + ) + + @try_except + def call_adosc(self, other_args: List[str]): + """Process adosc command""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="adosc", + description=""" + Accumulation/Distribution Oscillator, also known as the Chaikin Oscillator + is essentially a momentum indicator, but of the Accumulation-Distribution line + rather than merely price. It looks at both the strength of price moves and the + underlying buying and selling pressure during a given time period. The oscillator + reading above zero indicates net buying pressure, while one below zero registers + net selling pressure. Divergence between the indicator and pure price moves are + the most common signals from the indicator, and often flag market turning points. + """, + ) + parser.add_argument( + "--open", + action="store_true", + default=False, + dest="b_use_open", + help="uses open value of stock", + ) + parser.add_argument( + "-f", + "--fast_length", + action="store", + dest="n_length_fast", + type=check_positive, + default=3, + help="fast length", + ) + parser.add_argument( + "-s", + "--slow_length", + action="store", + dest="n_length_slow", + type=check_positive, + default=10, + help="slow length", + ) + + ns_parser = parse_known_args_and_warn( + parser, other_args, EXPORT_BOTH_RAW_DATA_AND_FIGURES + ) + if ns_parser: + volume_view.plot_adosc( + s_ticker=self.ticker, + s_interval="1440min", + df_stock=self.data, + use_open=ns_parser.b_use_open, + fast=ns_parser.n_length_fast, + slow=ns_parser.n_length_slow, + export=ns_parser.export, + ) + + def call_obv(self, other_args: List[str]): + """Process obv command""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="obv", + description=""" + The On Balance Volume (OBV) is a cumulative total of the up and + down volume. When the close is higher than the previous close, the volume is added + to the running total, and when the close is lower than the previous close, the volume + is subtracted from the running total. \n \n To interpret the OBV, look for the OBV + to move with the price or precede price moves. If the price moves before the OBV, + then it is a non-confirmed move. A series of rising peaks, or falling troughs, in the + OBV indicates a strong trend. If the OBV is flat, then the market is not trending. + """, + ) + + ns_parser = parse_known_args_and_warn( + parser, other_args, EXPORT_BOTH_RAW_DATA_AND_FIGURES + ) + if ns_parser: + volume_view.plot_obv( + s_ticker=self.ticker, + s_interval="1440min", + df_stock=self.data, + export=ns_parser.export, + ) + + @try_except + def call_fib(self, other_args: List[str]): + """Process fib command""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="fib", + description="Calculates the fibonacci retracement levels", + ) + parser.add_argument( + "-p", + "--period", + dest="period", + type=int, + help="Days to lookback for retracement", + default=120, + ) + parser.add_argument( + "--start", + dest="start", + type=valid_date, + help="Starting date to select", + required="--end" in other_args, + ) + parser.add_argument( + "--end", + dest="end", + type=valid_date, + help="Ending date to select", + required="--start" in other_args, + ) + + 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, EXPORT_BOTH_RAW_DATA_AND_FIGURES + ) + if ns_parser: + custom_indicators_view.fibonacci_retracement( + s_ticker=self.ticker, + df_stock=self.data, + period=ns_parser.period, + start_date=ns_parser.start, + end_date=ns_parser.end, + export=ns_parser.export, + ) + + +def menu( + ticker: str, + start: datetime, + data: pd.DataFrame, + queue: List[str] = None, +): + """Technical Analysis Menu""" + + ta_controller = TechnicalAnalysisController(ticker, start, data, queue) + an_input = "HELP_ME" + + while True: + # There is a command in the queue + if ta_controller.queue and len(ta_controller.queue) > 0: + # If the command is quitting the menu we want to return in here + if ta_controller.queue[0] in ("q", "..", "quit"): + if len(ta_controller.queue) > 1: + return ta_controller.queue[1:] + return [] + + # Consume 1 element from the queue + an_input = ta_controller.queue[0] + ta_controller.queue = ta_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 ta_controller.CHOICES_COMMANDS: + print(f"{get_flair()} /etf/ta/ $ {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": + ta_controller.print_help() + + # Get input from user using auto-completion + if session and gtff.USE_PROMPT_TOOLKIT and ta_controller.completer: + an_input = session.prompt( + f"{get_flair()} /etf/ta/ $ ", + completer=ta_controller.completer, + search_ignore_case=True, + ) + # Get input from user without auto-completion + else: + an_input = input(f"{get_flair()} /etf/ta/ $ ") + + try: + # Process the input command + ta_controller.queue = ta_controller.switch(an_input) + + except SystemExit: + print( + f"\nThe command '{an_input}' doesn't exist on the /etf/ta menu.", + end="", + ) + similar_cmd = difflib.get_close_matches( + an_input.split(" ")[0] if " " in an_input else an_input, + ta_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 = "" + ta_controller.queue = [] + print("\n") + continue + + print(f" Replacing by '{an_input}'.") + ta_controller.queue.insert(0, an_input) + else: + print("\n") + an_input = "" + ta_controller.queue = [] diff --git a/gamestonk_terminal/etf/wsj_view.py b/gamestonk_terminal/etf/wsj_view.py deleted file mode 100644 index f753fe578f5..00000000000 --- a/gamestonk_terminal/etf/wsj_view.py +++ /dev/null @@ -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("") diff --git a/gamestonk_terminal/etf/yfinance_model.py b/gamestonk_terminal/etf/yfinance_model.py new file mode 100644 index 00000000000..51988ac645c --- /dev/null +++ b/gamestonk_terminal/etf/yfinance_model.py @@ -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 "" diff --git a/gamestonk_terminal/etf/yfinance_view.py b/gamestonk_terminal/etf/yfinance_view.py new file mode 100644 index 00000000000..541c08b91f4 --- /dev/null +++ b/gamestonk_terminal/etf/yfinance_view.py @@ -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") diff --git a/gamestonk_terminal/stocks/screener/screener_controller.py b/gamestonk_terminal/stocks/screener/screener_controller.py index 7514a241aa9..467c2fd4c77 100644 --- a/gamestonk_terminal/stocks/screener/screener_controller.py +++ b/gamestonk_terminal/stocks/screener/screener_controller.py @@ -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) diff --git a/gamestonk_terminal/stocks/technical_analysis/ta_controller.py b/gamestonk_terminal/stocks/technical_analysis/ta_controller.py index 454214218b3..c68965280d3 100644 --- a/gamestonk_terminal/stocks/technical_analysis/ta_controller.py +++ b/gamestonk_terminal/stocks/technical_analysis/ta_controller.py @@ -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, diff --git a/scripts/test_etf.gst b/scripts/test_etf.gst index 63bc1f01c3f..afaac3e948b 100644 --- a/scripts/test_etf.gst +++ b/scripts/test_etf.gst @@ -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 \ No newline at end of file diff --git a/scripts/test_etf_ca.gst b/scripts/test_etf_ca.gst new file mode 100644 index 00000000000..bbc48ec3630 --- /dev/null +++ b/scripts/test_etf_ca.gst @@ -0,0 +1,7 @@ +etf +ln oil +ld oil +load spy +ca +volume +exit \ No newline at end of file diff --git a/scripts/test_etf_disc.gst b/scripts/test_etf_disc.gst new file mode 100644 index 00000000000..15d9356a5fe --- /dev/null +++ b/scripts/test_etf_disc.gst @@ -0,0 +1,6 @@ +etf +disc +gainers +decliners +active +exit \ No newline at end of file diff --git a/scripts/test_etf_pred.gst b/scripts/test_etf_pred.gst new file mode 100644 index 00000000000..38981a36ca5 --- /dev/null +++ b/scripts/test_etf_pred.gst @@ -0,0 +1,5 @@ +etf +load spy +pred +mc +exit \ No newline at end of file diff --git a/scripts/test_etf_scr.gst b/scripts/test_etf_scr.gst new file mode 100644 index 00000000000..03c5c76c594 --- /dev/null +++ b/scripts/test_etf_scr.gst @@ -0,0 +1,8 @@ +etf +scr +view +view etf_config +set etf_config +screen +sbc Bank Loan +exit \ No newline at end of file diff --git a/scripts/test_etf_ta.gst b/scripts/test_etf_ta.gst new file mode 100644 index 00000000000..8e107274ceb --- /dev/null +++ b/scripts/test_etf_ta.gst @@ -0,0 +1,5 @@ +etf +load spy +ta +ema +exit \ No newline at end of file