diff --git a/gamestonk_terminal/README.md b/gamestonk_terminal/README.md index 59adaff809d..2f22eb0c8ea 100644 --- a/gamestonk_terminal/README.md +++ b/gamestonk_terminal/README.md @@ -65,10 +65,10 @@ quote -t S_TICKER * Show the current price of a stock. ``` -candle +candle -s START_DATE ``` -* Visualize candles historical data from the past 6 months, with support and resistance bars, and moving averages of 20 and 50 +* Visualize candles historical data, with support and resistance bars, and moving averages of 20 and 50. ![nio](https://user-images.githubusercontent.com/25267873/111053397-4d609e00-845b-11eb-9c94-89b8892a8e81.png) diff --git a/gamestonk_terminal/config_terminal.py b/gamestonk_terminal/config_terminal.py index b09c6671f47..b66ccf3e650 100644 --- a/gamestonk_terminal/config_terminal.py +++ b/gamestonk_terminal/config_terminal.py @@ -48,8 +48,8 @@ DG_PASSWORD = os.getenv("GT_DG_PASSWORD") or "REPLACE_ME" DG_TOTP_SECRET = os.getenv("GT_DG_TOTP_SECRET") or None # https://developer.oanda.com -OANDA_ACCOUNT = os.getenv("GT_OANDA_ACCOUNT") or "REPLACE ME" -OANDA_TOKEN = os.getenv("GT_OANDA_TOKEN") or "REPLACE ME" +OANDA_ACCOUNT = os.getenv("GT_OANDA_ACCOUNT") or "REPLACE_ME" +OANDA_TOKEN = os.getenv("GT_OANDA_TOKEN") or "REPLACE_ME" # https://tradier.com/products/market-data-api TRADIER_TOKEN = os.getenv("GT_API_TRADIER_TOKEN") or "REPLACE_ME" diff --git a/gamestonk_terminal/helper_funcs.py b/gamestonk_terminal/helper_funcs.py index 23197765506..a302f65a2f8 100644 --- a/gamestonk_terminal/helper_funcs.py +++ b/gamestonk_terminal/helper_funcs.py @@ -1,6 +1,7 @@ """Helper functions""" __docformat__ = "numpy" import argparse +from typing import List from datetime import datetime, timedelta, time as Time import os import random @@ -27,6 +28,23 @@ if cfgPlot.BACKEND is not None: def check_valid_path(path: str) -> str: + """Argparse type function to test is path is valid + + Parameters + ---------- + path: str + Path supplied + + Returns + ------- + path: str + Valid path + + Raises + ------- + argparse.ArgumentTypeError + Given path does not exist + """ if not os.path.exists( os.path.abspath( os.path.join( @@ -38,14 +56,41 @@ def check_valid_path(path: str) -> str: return path -def check_int_range(mini, maxi): - """ - Checks if argparse argument is an int between 2 values. - https://stackoverflow.com/questions/55324449/how-to-specify-a-minimum-or-maximum-float-value-with-argparse +def check_int_range(mini: int, maxi: int): + """Checks if argparse argument is an int between 2 values. + + Parameters + ---------- + mini: int + Min value to compare + maxi: int + Max value to compare + + Returns + ------- + int_range_checker: + Function that compares the three integers """ # Define the function with default arguments - def int_range_checker(num) -> int: + def int_range_checker(num: int) -> int: + """Checks if int is between a high and low value + + Parameters + ---------- + num: int + Input integer + + Returns + ------- + num: int + Input number if conditions are met + + Raises + ------- + argparse.ArgumentTypeError + Input number not between min and max values + """ num = int(num) if num < mini or num > maxi: raise argparse.ArgumentTypeError(f"must be in range [{mini},{maxi}]") @@ -437,7 +482,21 @@ def patch_pandas_text_adjustment(): pandas.io.formats.format.TextAdjustment.adjoin = text_adjustment_adjoin -def parse_known_args_and_warn(parser, l_args): +def parse_known_args_and_warn(parser: argparse.ArgumentParser, other_args: List[str]): + """Parses list of arguments into the supplied parser + + Parameters + ---------- + parser: argparse.ArgumentParser + Parser with predefined arguments + other_args: List[str] + List of arguments to parse + + Returns + ------- + ns_parser: + Namespace with parsed arguments + """ parser.add_argument( "-h", "--help", action="store_true", help="show this help message" ) @@ -445,7 +504,7 @@ def parse_known_args_and_warn(parser, l_args): if gtff.USE_CLEAR_AFTER_CMD: os.system("cls||clear") - (ns_parser, l_unknown_args) = parser.parse_known_args(l_args) + (ns_parser, l_unknown_args) = parser.parse_known_args(other_args) if ns_parser.help: parser.print_help() diff --git a/gamestonk_terminal/main_helper.py b/gamestonk_terminal/main_helper.py index 26a22c65847..40d5b8eb141 100644 --- a/gamestonk_terminal/main_helper.py +++ b/gamestonk_terminal/main_helper.py @@ -8,15 +8,19 @@ import subprocess import random from datetime import datetime, timedelta import hashlib +import requests from colorama import Fore, Style import matplotlib.pyplot as plt from numpy.core.fromnumeric import transpose import pandas as pd from alpha_vantage.timeseries import TimeSeries +import quandl import mplfinance as mpf import yfinance as yf +from coinmarketcapapi import CoinMarketCapAPI, CoinMarketCapAPIError import pytz import pyEX +from pyEX.common.exception import PyEXception from tabulate import tabulate # import git @@ -408,49 +412,85 @@ def load(other_args: List[str], s_ticker, s_start, s_interval, df_stock): return [s_ticker, s_start, s_interval, df_stock] -def candle(s_ticker: str, s_start: str): - df_stock = trend.load_ticker(s_ticker, s_start) - df_stock = trend.find_trendline(df_stock, "OC_High", "high") - df_stock = trend.find_trendline(df_stock, "OC_Low", "low") +def candle(s_ticker: str, other_args: List[str]): + """Shows candle plot of loaded ticker - mc = mpf.make_marketcolors( - up="green", down="red", edge="black", wick="black", volume="in", ohlc="i" + Parameters + ---------- + s_ticker: str + Ticker to display + other_args: str + Argparse arguments + """ + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="candle", + description="Displays candle chart of loaded ticker", ) - s = mpf.make_mpf_style(marketcolors=mc, gridstyle=":", y_on_right=True) - - ap0 = [] - - if "OC_High_trend" in df_stock.columns: - ap0.append( - mpf.make_addplot(df_stock["OC_High_trend"], color="g"), - ) - - if "OC_Low_trend" in df_stock.columns: - ap0.append( - mpf.make_addplot(df_stock["OC_Low_trend"], color="b"), - ) - - if gtff.USE_ION: - plt.ion() - - mpf.plot( - df_stock, - type="candle", - mav=(20, 50), - volume=True, - title=f"\n{s_ticker} - Last 6 months", - 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 - ), + parser.add_argument( + "-s", + "--start_date", + dest="s_start", + type=valid_date, + default=(datetime.now() - timedelta(days=180)).strftime("%Y-%m-%d"), + help="Start date for candle data", ) - print("") + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + if not s_ticker: + print("No ticker loaded. First use `load {ticker}`", "\n") + return + + df_stock = trend.load_ticker(s_ticker, ns_parser.s_start) + df_stock = trend.find_trendline(df_stock, "OC_High", "high") + df_stock = trend.find_trendline(df_stock, "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_stock.columns: + ap0.append( + mpf.make_addplot(df_stock["OC_High_trend"], color="g"), + ) + + if "OC_Low_trend" in df_stock.columns: + ap0.append( + mpf.make_addplot(df_stock["OC_Low_trend"], color="b"), + ) + + if gtff.USE_ION: + plt.ion() + + mpf.plot( + df_stock, + type="candle", + mav=(20, 50), + volume=True, + title=f"\n{s_ticker} - Starting {ns_parser.s_start.strftime('%Y-%m-%d')}", + 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("") + + except Exception as e: + print(e, "\n") def quote(other_args: List[str], s_ticker: str): @@ -601,7 +641,219 @@ def view(other_args: List[str], s_ticker: str, s_interval, df_stock): return +# pylint: disable=too-many-statements + + +def check_api_keys(): + """Check api keys and if they are supplied""" + + key_dict = {} + if cfg.API_KEY_ALPHAVANTAGE == "REPLACE_ME": # pragma: allowlist secret + key_dict["ALPHA_VANTAGE"] = "Not defined" + else: + df = TimeSeries( + key=cfg.API_KEY.ALPHAVANTAGE, output_format="pandas" + ).get_intraday(symbol="AAPL") + if df.empty: + key_dict["ALPHA_VANTAGE"] = "defined, test failed" + else: + key_dict["ALPHA_VANTAGE"] = "defined, test passed" + + if cfg.API_KEY_FINANCIALMODELINGPREP == "REPLACE_ME": + key_dict["FINANCIAL_MODELING_PREP"] = "Not defined" + else: + r = requests.get( + f"https://financialmodelingprep.com/api/v3/profile/AAPL?apikey={cfg.API_KEY_FINANCIALMODELINGPREP}" + ) + if r.status_code in [403, 401]: + key_dict["FINANCIAL_MODELING_PREP"] = "defined, test failed" + elif r.status_code == 200: + key_dict["FINANCIAL_MODELING_PREP"] = "defined, test passed" + else: + key_dict["FINANCIAL_MODELING_PREP"] = "defined, test inconclusive" + + if cfg.API_KEY_QUANDL == "REPLACE_ME": + key_dict["QUANDL"] = "Not defined" + else: + try: + quandl.save_key(cfg.API_KEY_QUANDL) + quandl.get_table( + "ZACKS/FC", + paginate=True, + ticker=["AAPL", "MSFT"], + per_end_date={"gte": "2015-01-01"}, + qopts={"columns": ["ticker", "per_end_date"]}, + ) + key_dict["QUANDL"] = "defined, test passed" + except quandl.errors.quandl_error.ForbiddenError: + key_dict["QUANDL"] = "defined, test failed" + + if cfg.API_POLYGON_KEY == "REPLACE_ME": + key_dict["POLYGON"] = "Not defined" + else: + r = requests.get( + f"https://api.polygon.io/v2/aggs/ticker/AAPL/range/1/day/2020-06-01/2020-06-17?apiKey={cfg.API_POLYGON_KEY}" + ) + if r.status_code in [403, 401]: + key_dict["POLYGON"] = "defined, test failed" + elif r.status_code == 200: + key_dict["POLYGON"] = "defined, test passed" + else: + key_dict["POLYGON"] = "defined, test inconclusive" + + if cfg.API_FRED_KEY == "REPLACE_ME": + key_dict["FRED"] = "Not defined" + else: + r = requests.get( + f"https://api.stlouisfed.org/fred/series?series_id=GNPCA&api_key={cfg.API_FRED_KEY}" + ) + if r.status_code in [403, 401, 400]: + key_dict["FRED"] = "defined, test failed" + elif r.status_code == 200: + key_dict["FRED"] = "defined, test passed" + else: + key_dict["FRED"] = "defined, test inconclusive" + + if cfg.API_NEWS_TOKEN == "REPLACE_ME": + key_dict["NEWSAPI"] = "Not defined" + else: + r = requests.get( + f"https://newsapi.org/v2/everything?q=keyword&apiKey={cfg.API_NEWS_TOKEN}" + ) + if r.status_code in [401, 403]: + key_dict["NEWSAPI"] = "defined, test failed" + elif r.status_code == 200: + key_dict["NEWSAPI"] = "defined, test passed" + else: + key_dict["NEWSAPI"] = "defined, test inconclusive" + + if cfg.TRADIER_TOKEN == "REPLACE_ME": + key_dict["TRADIER"] = "Not defined" + else: + r = requests.get( + "https://sandbox.tradier.com/v1/markets/quotes", + params={"symbols": "AAPL"}, + headers={ + "Authorization": f"Bearer {cfg.TRADIER_TOKEN}", + "Accept": "application/json", + }, + ) + if r.status_code in [401, 403]: + key_dict["TRADIER"] = "defined, test failed" + elif r.status_code == 200: + key_dict["TRADIER"] = "defined, test passed" + else: + key_dict["TRADIER"] = "defined, test inconclusive" + + if cfg.API_CMC_KEY == "REPLACE_ME": + key_dict["COINMARKETCAP"] = "Not defined" + else: + cmc = CoinMarketCapAPI(cfg.API_CMC_KEY) + try: + cmc.exchange_info() + key_dict["COINMARKETCAP"] = "defined, test passed" + except CoinMarketCapAPIError: + key_dict["COINMARKETCAP"] = "defined, test failed" + + if cfg.API_FINNHUB_KEY == "REPLACE_ME": + key_dict["FINNHUB"] = "Not defined" + else: + r = r = requests.get( + f"https://finnhub.io/api/v1/quote?symbol=AAPL&token={cfg.API_FINNHUB_KEY}" + ) + if r.status_code in [403, 401, 400]: + key_dict["FINNHUB"] = "defined, test failed" + elif r.status_code == 200: + key_dict["FINNHUB"] = "defined, test passed" + else: + key_dict["FINNHUB"] = "defined, test inconclusive" + + if cfg.API_IEX_TOKEN == "REPLACE_ME": + key_dict["IEXCLOUD"] = "Not defined" + else: + try: + pyEX.Client(api_token=cfg.API_IEX_TOKEN, version="v1") + key_dict["IEXCLOUD"] = "defined, test passed" + except PyEXception: + key_dict["IEXCLOUD"] = "defined, test failed" + + # Reddit + reddit_keys = [ + cfg.API_REDDIT_CLIENT_ID, + cfg.API_REDDIT_CLIENT_SECRET, + cfg.API_REDDIT_USERNAME, + cfg.API_REDDIT_PASSWORD, + cfg.API_REDDIT_USER_AGENT, + ] + if "REPLACE_ME" in reddit_keys: + key_dict["REDDIT"] = "Not defined" + else: + key_dict["REDDIT"] = "defined, not tested" + + # Reddit keys + twitter_keys = [ + cfg.API_TWITTER_KEY, + cfg.API_TWITTER_SECRET_KEY, + cfg.API_TWITTER_BEARER_TOKEN, + ] + if "REPLACE_ME" in twitter_keys: + key_dict["TWITTER"] = "Not defined" + else: + params = { + "query": "(\\$AAPL) (lang:en)", + "max_results": "10", + "tweet.fields": "created_at,lang", + } + r = requests.get( + "https://api.twitter.com/2/tweets/search/recent", + params=params, # type: ignore + headers={"authorization": "Bearer " + cfg.API_TWITTER_BEARER_TOKEN}, + ) + if r.status_code == 200: + key_dict["TWITTER"] = "defined, test passed" + elif r.status_code in [401, 403]: + key_dict["TWITTER"] = "defined, test failed" + else: + key_dict["TWITTER"] = "defined, test inconclusive" + + # Robinhood keys + rh_keys = [cfg.RH_USERNAME, cfg.RH_PASSWORD] + if "REPLACE_ME" in rh_keys: + key_dict["ROBINHOOD"] = "Not defined" + else: + key_dict["ROBINHOOD"] = "defined, not tested" + # Degiro keys + dg_keys = [cfg.DG_USERNAME, cfg.DG_PASSWORD, cfg.DG_TOTP_SECRET] + if "REPLACE_ME" in dg_keys: + key_dict["DEGIRO"] = "Not defined" + else: + key_dict["DEGIRO"] = "defined, not tested" + # OANDA keys + oanda_keys = [cfg.OANDA_TOKEN, cfg.OANDA_ACCOUNT] + if "REPLACE_ME" in oanda_keys: + key_dict["OANDA"] = "Not defined" + else: + key_dict["OANDA"] = "defined, not tested" + # Binance keys + bn_keys = [cfg.API_BINANCE_KEY, cfg.API_BINANCE_SECRET] + if "REPLACE_ME" in bn_keys: + key_dict["BINANCE"] = "Not defined" + else: + key_dict["BINANCE"] = "defined, not tested" + + print( + tabulate( + pd.DataFrame(key_dict.items()), + showindex=False, + headers=[], + tablefmt="fancy_grid", + ), + "\n", + ) + + def print_goodbye(): + """Prints a goodbye message when quitting the terminal""" goodbye_msg = [ "An informed ape, is a strong ape. ", "Remember that stonks only go up. ", @@ -641,6 +893,7 @@ def sha256sum(filename): def update_terminal(): + """Updates the terminal by running git pull in the directory. Runs poetry install if needed""" poetry_hash = sha256sum("poetry.lock") completed_process = subprocess.run("git pull", shell=True, check=False) @@ -664,6 +917,7 @@ def update_terminal(): def about_us(): + """Prints an about us section""" print( f"\n{Fore.GREEN}Thanks for using Gamestonk Terminal. This is our way!{Style.RESET_ALL}\n" "\n" @@ -728,6 +982,7 @@ def bootup(): def reset(): + """Resets the terminal. Allows for checking code or keys without quitting""" print("resetting...") plt.close("all") completed_process = subprocess.run("python terminal.py", shell=True, check=False) diff --git a/terminal.py b/terminal.py index 52675522df2..f42dd927fb0 100644 --- a/terminal.py +++ b/terminal.py @@ -5,14 +5,17 @@ __docformat__ = "numpy" import argparse import os import sys -from datetime import datetime, timedelta +from datetime import datetime from typing import List import pandas as pd import yfinance as yf from alpha_vantage.timeseries import TimeSeries from prompt_toolkit.completion import NestedCompleter -from gamestonk_terminal.helper_funcs import b_is_stock_market_open, get_flair +from gamestonk_terminal.helper_funcs import ( + b_is_stock_market_open, + get_flair, +) from gamestonk_terminal.main_helper import ( clear, load, @@ -24,6 +27,7 @@ from gamestonk_terminal.main_helper import ( about_us, bootup, reset, + check_api_keys, ) from gamestonk_terminal.menu import session from gamestonk_terminal import config_terminal as cfg @@ -103,6 +107,7 @@ class TerminalController: "about", "bro", "ins", + "keys", ] CHOICES += CHOICES_TICKER_DEPENDENT @@ -136,6 +141,7 @@ What do you want to do? cls clear screen ?/help show this menu again update update terminal from remote + keys check for defined api keys reset reset terminal and reload configs about about us q(uit) to abandon the program @@ -189,6 +195,7 @@ Contexts: help_text += """\n> disc discover trending stocks, \t e.g. map, sectors, high short interest > scr screener stocks, \t\t e.g. overview/performance, using preset filters """ + print(help_text) def switch(self, an_input: str): @@ -265,11 +272,11 @@ Contexts: other_args, self.ticker + "." + self.suffix if self.suffix else self.ticker ) - def call_candle(self, _): + def call_candle(self, other_args: List[str]): """Process candle command""" candle( self.ticker + "." + self.suffix if self.suffix else self.ticker, - (datetime.now() - timedelta(days=180)).strftime("%Y-%m-%d"), + other_args, ) def call_view(self, other_args: List[str]): @@ -333,6 +340,10 @@ Contexts: self.update_succcess = not update_terminal() return True + def call_keys(self, _): + """Process keys command""" + check_api_keys() + def call_about(self, _): """Process about command""" about_us()