From ff8be2405ed836c7046ca7d3787ac96627d58e68 Mon Sep 17 00:00:00 2001 From: Jamie Date: Fri, 30 Apr 2021 21:40:08 -0400 Subject: [PATCH 01/74] add binance keys, trending gecko coins --- gamestonk_terminal/config_terminal.py | 4 ++ .../cryptocurrency/binance_model.py | 23 ++++++++++ .../cryptocurrency/crypto_controller.py | 44 ++++++++++++++++--- .../cryptocurrency/pycoingecko_view.py | 21 +++++++++ 4 files changed, 85 insertions(+), 7 deletions(-) create mode 100644 gamestonk_terminal/cryptocurrency/binance_model.py diff --git a/gamestonk_terminal/config_terminal.py b/gamestonk_terminal/config_terminal.py index dd4092eb45d..77a2417965b 100644 --- a/gamestonk_terminal/config_terminal.py +++ b/gamestonk_terminal/config_terminal.py @@ -59,3 +59,7 @@ PATH_TO_SELENIUM_DRIVER = None # Replace with "PATH" # https://coinmarketcap.com/api/ COINMARKETCAP_KEY = os.getenv("GT_CMC_API_KEY") or "REPLACE_ME" + +# https://www.binance.com/en/ +BINANCE_API_KEY = os.getenv("GT_BINANCE_API_KEY") or "REPLACE_ME" +BINANCE_SECRET_KEY = os.getenv("GT_BINANCE_SECRET_KEY") or "REPLACE_ME" diff --git a/gamestonk_terminal/cryptocurrency/binance_model.py b/gamestonk_terminal/cryptocurrency/binance_model.py new file mode 100644 index 00000000000..44b8ecd5f4e --- /dev/null +++ b/gamestonk_terminal/cryptocurrency/binance_model.py @@ -0,0 +1,23 @@ +"""Binance model""" +__docformat__ = "numpy" + +import argparse +from typing import List +from binance.client import Client + +def order_book(coin:str, other_args:List[str]): + """Get order book for currency""" + pass + +def recent_trades(coin:str, other_args:List[str]): + """Get recent trades for currency""" + pass + +def candles(coin:str, other_args:List[str]): + """Get order book for currency""" + pass + + +def balance(coin:str, other_args:List[str]): + """Get order book for currency""" + pass \ No newline at end of file diff --git a/gamestonk_terminal/cryptocurrency/crypto_controller.py b/gamestonk_terminal/cryptocurrency/crypto_controller.py index 0361dcbbb23..afeabb8cae8 100644 --- a/gamestonk_terminal/cryptocurrency/crypto_controller.py +++ b/gamestonk_terminal/cryptocurrency/crypto_controller.py @@ -1,3 +1,4 @@ +"""Cryptocurrency Controller""" __docformat__ = "numpy" import argparse @@ -7,13 +8,28 @@ from prompt_toolkit.completion import NestedCompleter from gamestonk_terminal import feature_flags as gtff from gamestonk_terminal.helper_funcs import get_flair from gamestonk_terminal.menu import session -from gamestonk_terminal.cryptocurrency import pycoingecko_view -from gamestonk_terminal.cryptocurrency import coinmarketcap_view as cmc_view +from gamestonk_terminal.cryptocurrency import ( + binance_model, + pycoingecko_view, + coinmarketcap_view as cmc_view, +) class CryptoController: - CHOICES = ["help", "q", "quit", "load", "view", "top"] + CHOICES = [ + "help", + "q", + "quit", + "load", + "view", + "top", + "trend", + "book", + "trades", + "candle", + "balance", + ] def __init__(self): """CONSTRUCTOR""" @@ -24,19 +40,28 @@ class CryptoController: self.current_df = pd.DataFrame() @staticmethod - def print_help(current_coin): + def print_help(self): """Print help""" print("\nCryptocurrency:") print(" help show this menu again") print(" q quit this menu, and shows back to main menu") print(" quit quit to abandon program") - print(f"\nCurrent Coin: {current_coin}") + print(f"\nCurrent Coin: {self.current_coin}") print("") + print("Coingecko:") print(" load load cryptocurrency data") print(" view load and view cryptocurrency data") + print(" trend show top 7 trending coins") print("") + print("CoinMarketCap:") print(" top view top coins from coinmarketcap") print("") + print("Binance:") + print(" book get order book") + print(" trades get recent trades") + print(" candle get klines/candles and plot") + print(" balance show coin balance") + print("") def switch(self, an_input: str): """Process and dispatch input @@ -56,7 +81,7 @@ class CryptoController: def call_help(self, _): """Process Help command""" - self.print_help(self.current_coin) + self.print_help(self) def call_q(self, _): """Process Q command - quit the menu""" @@ -77,13 +102,18 @@ class CryptoController: print("No coin selected. Use 'load' to load the coin you want to look at.") print("") + def call_trend(self, _): + pycoingecko_view.trend() + def call_top(self, other_args): cmc_view.get_cmc_top_n(other_args) + def call_book(self,other_args): + binance_model.order_book(other_args) def menu(): crypto_controller = CryptoController() - crypto_controller.print_help(crypto_controller.current_coin) + crypto_controller.print_help(crypto_controller) plt.close("all") while True: # Get input command from user diff --git a/gamestonk_terminal/cryptocurrency/pycoingecko_view.py b/gamestonk_terminal/cryptocurrency/pycoingecko_view.py index 3dd11a7663f..03f335467d5 100644 --- a/gamestonk_terminal/cryptocurrency/pycoingecko_view.py +++ b/gamestonk_terminal/cryptocurrency/pycoingecko_view.py @@ -6,6 +6,7 @@ from typing import List import pandas as pd from pandas.plotting import register_matplotlib_converters import matplotlib.pyplot as plt +from tabulate import tabulate from pycoingecko import CoinGeckoAPI from gamestonk_terminal.helper_funcs import parse_known_args_and_warn, plot_autoscale from gamestonk_terminal.cryptocurrency.crypto_helper import coin_symbol_to_id, coin_ids @@ -135,3 +136,23 @@ def view(coin: str, prices: pd.DataFrame, other_args: List[str]): except Exception as e: print(e) print("") + +def trend(): + """Prints top 7 coins from pycoingecko""" + cg = CoinGeckoAPI() + trending = cg.get_search_trending()["coins"] + name, symbol, price, rank = [], [], [], [] + for coin in trending: + name.append( coin["item"]["name"] ) + symbol.append( coin["item"]["symbol"] ) + coin_id = coin["item"]["id"] + price.append( cg.get_price(coin_id, vs_currencies="USD")[coin_id]["usd"]) + rank.append( coin["item"]["market_cap_rank"] ) + + df = pd.DataFrame() + df["name"]= name + df["symbol"] = symbol + df["last_price"] = price + df["market_cap_rank"] = rank + print( tabulate(df, headers=df.columns, floatfmt = ".2f", showindex=False, tablefmt="fancy_grid")) + print("") \ No newline at end of file From 5f9ef3ddeee7e27f68e0ff5f20f171a181e3d069 Mon Sep 17 00:00:00 2001 From: Jamie Date: Fri, 30 Apr 2021 22:49:30 -0400 Subject: [PATCH 02/74] add order book command --- README.md | 6 +- .../cryptocurrency/binance_model.py | 85 ++++++++++++++++--- .../cryptocurrency/binance_view.py | 43 ++++++++++ .../cryptocurrency/crypto_controller.py | 27 +++++- .../cryptocurrency/pycoingecko_view.py | 23 +++-- 5 files changed, 163 insertions(+), 21 deletions(-) create mode 100644 gamestonk_terminal/cryptocurrency/binance_view.py diff --git a/README.md b/README.md index 2809b6a6025..ac56f9069c9 100644 --- a/README.md +++ b/README.md @@ -225,10 +225,14 @@ These are the ones where a key is necessary: * Tradier: https://developer.tradier.com/getting_started * Oanda API: https://developer.oanda.com * CoinMarketCap API: https://coinmarketcap.com/api/ + * Binance: https://binance.us When these are obtained, don't forget to update [config_terminal.py](/gamestonk_terminal/config_terminal.py). -Alternatively, you can also set them to the following environment variables: GT_API_KEY_ALPHAVANTAGE, GT_API_KEY_FINANCIALMODELINGPREP, GT_API_KEY_QUANDL, GT_API_REDDIT_CLIENT_ID, GT_API_REDDIT_CLIENT_SECRET, GT_API_REDDIT_USERNAME, GT_API_REDDIT_USER_AGENT, GT_API_REDDIT_PASSWORD, GT_API_TWITTER_KEY, GT_API_TWITTER_SECRET_KEY, GT_API_TWITTER_BEARER_TOKEN, GT_API_POLYGON_KEY, GT_FRED_API_KEY, GT_API_NEWS_TOKEN, GT_OANDA_TOKEN, GT_OANDA_ACCOUNT, GT_TRADIER_TOKEN, GT_CMC_API_KEY. +Alternatively, you can also set them to the following environment variables: GT_API_KEY_ALPHAVANTAGE, GT_API_KEY_FINANCIALMODELINGPREP, +GT_API_KEY_QUANDL, GT_API_REDDIT_CLIENT_ID, GT_API_REDDIT_CLIENT_SECRET, GT_API_REDDIT_USERNAME, GT_API_REDDIT_USER_AGENT, GT_API_REDDIT_PASSWORD, +GT_API_TWITTER_KEY, GT_API_TWITTER_SECRET_KEY, GT_API_TWITTER_BEARER_TOKEN, GT_API_POLYGON_KEY, GT_FRED_API_KEY, +GT_API_NEWS_TOKEN, GT_OANDA_TOKEN, GT_OANDA_ACCOUNT, GT_TRADIER_TOKEN, GT_CMC_API_KEY, GT_BINANCE_API_KEY, GT_BINANCE_SECRET_KEY. Example: ``` diff --git a/gamestonk_terminal/cryptocurrency/binance_model.py b/gamestonk_terminal/cryptocurrency/binance_model.py index 44b8ecd5f4e..148d8c6a59d 100644 --- a/gamestonk_terminal/cryptocurrency/binance_model.py +++ b/gamestonk_terminal/cryptocurrency/binance_model.py @@ -3,21 +3,84 @@ __docformat__ = "numpy" import argparse from typing import List +import matplotlib.pyplot as plt +import numpy as np from binance.client import Client +from gamestonk_terminal.main_helper import parse_known_args_and_warn +from gamestonk_terminal.config_terminal import BINANCE_API_KEY, BINANCE_SECRET_KEY +from gamestonk_terminal.cryptocurrency.binance_view import plot_order_book -def order_book(coin:str, other_args:List[str]): - """Get order book for currency""" - pass -def recent_trades(coin:str, other_args:List[str]): - """Get recent trades for currency""" - pass +def order_book(coin: str, other_args: List[str]): + """ + Get order book for currency + Parameters + ---------- + coin: str + Coin to get order book for + other_args: List[str] + Argparse arguments + + Returns + ------- + + """ + client = Client(BINANCE_API_KEY, BINANCE_SECRET_KEY) + order_book = client.get_order_book(symbol=coin) + bids = np.asarray(order_book["bids"], dtype=float) + asks = np.asarray(order_book["asks"], dtype=float) + bids = np.insert(bids, 2, bids[:, 1].cumsum(), axis=1) + asks = np.insert(asks, 2, np.flipud(asks[:, 1]).cumsum(), axis=1) + plot_order_book(bids, asks, coin) + + +def recent_trades(coin: str, other_args: List[str]): + """ + Get recent trades for currency + Parameters + ---------- + coin: str + Coin to get recent trades for + other_args: List[str] + Argparse arguments + + Returns + ------- + + """ -def candles(coin:str, other_args:List[str]): - """Get order book for currency""" pass -def balance(coin:str, other_args:List[str]): - """Get order book for currency""" - pass \ No newline at end of file +def candles(coin: str, other_args: List[str]): + """ + Get klines/candles for coin + Parameters + ---------- + coin: str + Coin to get symbol of + other_args: List[str] + Argparse arguments + + Returns + ------- + + """ + pass + + +def balance(coin: str, other_args: List[str]): + """ + Get accont holdings for asset + Parameters + ---------- + coin: str + Coin to get holdings of + other_args : List[str] + Argparse arguments + + Returns + ------- + + """ + pass diff --git a/gamestonk_terminal/cryptocurrency/binance_view.py b/gamestonk_terminal/cryptocurrency/binance_view.py new file mode 100644 index 00000000000..da8ccd3a4c2 --- /dev/null +++ b/gamestonk_terminal/cryptocurrency/binance_view.py @@ -0,0 +1,43 @@ +"""Binance view functions""" +__docformat__ = "numpy" + +import numpy as np +import matplotlib.pyplot as plt +from gamestonk_terminal.main_helper import plot_autoscale +from gamestonk_terminal.config_plot import PLOT_DPI +from gamestonk_terminal.feature_flags import USE_ION as ion + + +def plot_order_book(bids: np.array, asks: np.array, coin: str): + """ + Plots Bid/Ask + Parameters + ---------- + bids : np.array + array of bids with columns: price, size, cumulative size + asks : np.array + array of asks with columns: price, size, cumulative size + coin : str + Coin being plotted + + Returns + ------- + + """ + + _, ax = plt.subplots(figsize=plot_autoscale(), dpi=PLOT_DPI) + ax.plot(bids[:, 0], bids[:, 2], "g", label="bids") + ax.fill_between(bids[:, 0], bids[:, 2], color="g", alpha=0.4) + ax.plot(asks[:, 0], asks[:, 2], "r", label="asks") + ax.fill_between(asks[:, 0], asks[:, 2], color="r", alpha=0.4) + plt.grid(b=True, which="major", color="#666666", linestyle="-") + plt.minorticks_on() + plt.grid(b=True, which="minor", color="#999999", linestyle="-", alpha=0.2) + plt.legend(loc=0) + plt.xlabel("Price") + plt.ylabel("Size (Coins) ") + plt.title(f"Order Book for {coin}") + # if ion: + # plt.ion() + plt.show() + print("") diff --git a/gamestonk_terminal/cryptocurrency/crypto_controller.py b/gamestonk_terminal/cryptocurrency/crypto_controller.py index afeabb8cae8..e95633d8fdb 100644 --- a/gamestonk_terminal/cryptocurrency/crypto_controller.py +++ b/gamestonk_terminal/cryptocurrency/crypto_controller.py @@ -29,6 +29,7 @@ class CryptoController: "trades", "candle", "balance", + "add", ] def __init__(self): @@ -57,6 +58,7 @@ class CryptoController: print(" top view top coins from coinmarketcap") print("") print("Binance:") + print(" add add coin as current") print(" book get order book") print(" trades get recent trades") print(" candle get klines/candles and plot") @@ -92,9 +94,11 @@ class CryptoController: return True def call_load(self, other_args): + """Load command - get df from gecko""" self.current_coin, self.current_df = pycoingecko_view.load(other_args) def call_view(self, other_args): + """View command - plot loaded coin""" if self.current_coin: pycoingecko_view.view(self.current_coin, self.current_df, other_args) @@ -103,13 +107,32 @@ class CryptoController: print("") def call_trend(self, _): + """Trend command - show top 7 gecko coins""" pycoingecko_view.trend() def call_top(self, other_args): + """Top command - get top n coins from coinmarketcap""" cmc_view.get_cmc_top_n(other_args) - def call_book(self,other_args): - binance_model.order_book(other_args) + def call_book(self, other_args): + """Book command - get order book from binance""" + binance_model.order_book(self.current_coin, other_args) + + def call_trades(self, other_args): + """Trades command - get recent trades from binance""" + binance_model.recent_trades(self.current_coin, other_args) + + def call_candle(self, other_args): + """Candle command - show candle chart from binance""" + binance_model.candles(self.current_coin, other_args) + + def call_balance(self, other_args): + """Balance command - check current holdings of coin in binance""" + binance_model.balance(self.current_coin, other_args) + + def call_add(self, _): + self.current_coin = input("Coin?").upper() + def menu(): crypto_controller = CryptoController() diff --git a/gamestonk_terminal/cryptocurrency/pycoingecko_view.py b/gamestonk_terminal/cryptocurrency/pycoingecko_view.py index 03f335467d5..bb042a60432 100644 --- a/gamestonk_terminal/cryptocurrency/pycoingecko_view.py +++ b/gamestonk_terminal/cryptocurrency/pycoingecko_view.py @@ -137,22 +137,31 @@ def view(coin: str, prices: pd.DataFrame, other_args: List[str]): print(e) print("") + def trend(): """Prints top 7 coins from pycoingecko""" cg = CoinGeckoAPI() trending = cg.get_search_trending()["coins"] name, symbol, price, rank = [], [], [], [] for coin in trending: - name.append( coin["item"]["name"] ) - symbol.append( coin["item"]["symbol"] ) + name.append(coin["item"]["name"]) + symbol.append(coin["item"]["symbol"]) coin_id = coin["item"]["id"] - price.append( cg.get_price(coin_id, vs_currencies="USD")[coin_id]["usd"]) - rank.append( coin["item"]["market_cap_rank"] ) + price.append(cg.get_price(coin_id, vs_currencies="USD")[coin_id]["usd"]) + rank.append(coin["item"]["market_cap_rank"]) df = pd.DataFrame() - df["name"]= name + df["name"] = name df["symbol"] = symbol df["last_price"] = price df["market_cap_rank"] = rank - print( tabulate(df, headers=df.columns, floatfmt = ".2f", showindex=False, tablefmt="fancy_grid")) - print("") \ No newline at end of file + print( + tabulate( + df, + headers=df.columns, + floatfmt=".2f", + showindex=False, + tablefmt="fancy_grid", + ) + ) + print("") From 366e2f56a5883d7fe1134ba5c087bc748547a9da Mon Sep 17 00:00:00 2001 From: Jamie Date: Sat, 1 May 2021 17:15:04 -0400 Subject: [PATCH 03/74] add 'add' and candle plots --- .../cryptocurrency/binance_model.py | 137 +++++++++++++++--- .../cryptocurrency/binance_view.py | 39 ++++- .../cryptocurrency/crypto_controller.py | 15 +- 3 files changed, 157 insertions(+), 34 deletions(-) diff --git a/gamestonk_terminal/cryptocurrency/binance_model.py b/gamestonk_terminal/cryptocurrency/binance_model.py index 148d8c6a59d..e7b1f5de1d8 100644 --- a/gamestonk_terminal/cryptocurrency/binance_model.py +++ b/gamestonk_terminal/cryptocurrency/binance_model.py @@ -3,12 +3,77 @@ __docformat__ = "numpy" import argparse from typing import List -import matplotlib.pyplot as plt +import pandas as pd import numpy as np from binance.client import Client +from binance.exceptions import BinanceAPIException from gamestonk_terminal.main_helper import parse_known_args_and_warn from gamestonk_terminal.config_terminal import BINANCE_API_KEY, BINANCE_SECRET_KEY -from gamestonk_terminal.cryptocurrency.binance_view import plot_order_book +from gamestonk_terminal.cryptocurrency.binance_view import plot_order_book, plot_candles + + +def check_valid_binance_str(symbol: str): + """Check if symbol is in defined binance""" + client = Client(BINANCE_API_KEY, BINANCE_SECRET_KEY) + try: + client.get_avg_price(symbol=symbol.upper()) + return symbol.upper() + except BinanceAPIException: + raise argparse.ArgumentTypeError(f"{symbol} is not a valid binance symbol") + + +def add_binance_coin(other_args:List[str]): + """ + Define current_coin from binance + Parameters + ---------- + other_args: List[str] + Argparse arguments + + Returns + ------- + coin: str + Coin that is defined on binance + """ + parser = argparse.ArgumentParser( + prog="add_coin", + add_help=False, + description="Define the coin to be used from binance", + ) + parser.add_argument( + "-c", "--coin", help="Coin to get", dest="coin", type=str, default="BTC" + ) + parser.add_argument( + "-q", + "--quote", + help="Quote currency (what to view coin vs)", + dest="quote", + type=str, + default="USDT", + ) + parser.add_argument( + "-s", + "--symbol", + help="binance symbol (overrides other arguments)", + dest="symbol", + default=None, + type=check_valid_binance_str, + ) + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return None + + if ns_parser.symbol: + return ns_parser.symbol + + coin = ns_parser.coin + ns_parser.quote + return check_valid_binance_str(coin) + + except Exception as e: + print(e) + print("") + return None def order_book(coin: str, other_args: List[str]): @@ -34,24 +99,6 @@ def order_book(coin: str, other_args: List[str]): plot_order_book(bids, asks, coin) -def recent_trades(coin: str, other_args: List[str]): - """ - Get recent trades for currency - Parameters - ---------- - coin: str - Coin to get recent trades for - other_args: List[str] - Argparse arguments - - Returns - ------- - - """ - - pass - - def candles(coin: str, other_args: List[str]): """ Get klines/candles for coin @@ -66,12 +113,51 @@ def candles(coin: str, other_args: List[str]): ------- """ - pass + client = Client(BINANCE_API_KEY, BINANCE_SECRET_KEY) + try: + candles = client.get_klines( + symbol=coin, interval=Client.KLINE_INTERVAL_30MINUTE + ) + + # Response + # 1499040000000, // Open time + # "0.01634790", // Open + # "0.80000000", // High + # "0.01575800", // Low + # "0.01577100", // Close + # "148976.11427815", // Volume + # 1499644799999, // Close time + # "2434.19055334", // Quote asset volume + # 308, // Number of trades + # "1756.87402397", // Taker buy base asset volume + # "28.46694368", // Taker buy quote asset volume + # "17928899.62484339" // Ignore. + + candles_df = pd.DataFrame(candles).astype(float).iloc[:, :7] + + candles_df.columns = [ + "Time0", + "Open", + "High", + "Low", + "Close", + "Volume", + "Time1", + ] + candles_df.index = pd.to_datetime( + (candles_df["Time0"] + candles_df["Time1"]) / 2, unit="ms" + ) + + plot_candles(candles_df, "") + + except Exception as e: + print(e) + print("") def balance(coin: str, other_args: List[str]): """ - Get accont holdings for asset + Get account holdings for asset Parameters ---------- coin: str @@ -83,4 +169,9 @@ def balance(coin: str, other_args: List[str]): ------- """ - pass + client = Client(BINANCE_API_KEY, BINANCE_SECRET_KEY) + try: + client.get_asset_balance(asset=coin) + except Exception as e: + print(e) + print("") diff --git a/gamestonk_terminal/cryptocurrency/binance_view.py b/gamestonk_terminal/cryptocurrency/binance_view.py index da8ccd3a4c2..c59196fe4d4 100644 --- a/gamestonk_terminal/cryptocurrency/binance_view.py +++ b/gamestonk_terminal/cryptocurrency/binance_view.py @@ -3,6 +3,8 @@ __docformat__ = "numpy" import numpy as np import matplotlib.pyplot as plt +import mplfinance as mpf +import pandas as pd from gamestonk_terminal.main_helper import plot_autoscale from gamestonk_terminal.config_plot import PLOT_DPI from gamestonk_terminal.feature_flags import USE_ION as ion @@ -37,7 +39,40 @@ def plot_order_book(bids: np.array, asks: np.array, coin: str): plt.xlabel("Price") plt.ylabel("Size (Coins) ") plt.title(f"Order Book for {coin}") - # if ion: - # plt.ion() + if ion: + plt.ion() plt.show() print("") + + +def plot_candles(candles_df: pd.DataFrame, coin: str): + """ + Plot candle chart from dataframe + Parameters + ---------- + candles_df: pd.DataFrame + Dataframe containing time and OHLVC + coin: str + Coin plotting for + + Returns + ------- + + """ + mpf.plot( + candles_df, + type="candle", + volume=True, + title=f"{coin}", + xrotation=10, + style="binance", + 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 + ), + ) + if ion: + plt.ion() + plt.show() diff --git a/gamestonk_terminal/cryptocurrency/crypto_controller.py b/gamestonk_terminal/cryptocurrency/crypto_controller.py index e95633d8fdb..f93af43132d 100644 --- a/gamestonk_terminal/cryptocurrency/crypto_controller.py +++ b/gamestonk_terminal/cryptocurrency/crypto_controller.py @@ -59,8 +59,7 @@ class CryptoController: print("") print("Binance:") print(" add add coin as current") - print(" book get order book") - print(" trades get recent trades") + print(" book show order book") print(" candle get klines/candles and plot") print(" balance show coin balance") print("") @@ -118,10 +117,6 @@ class CryptoController: """Book command - get order book from binance""" binance_model.order_book(self.current_coin, other_args) - def call_trades(self, other_args): - """Trades command - get recent trades from binance""" - binance_model.recent_trades(self.current_coin, other_args) - def call_candle(self, other_args): """Candle command - show candle chart from binance""" binance_model.candles(self.current_coin, other_args) @@ -130,9 +125,11 @@ class CryptoController: """Balance command - check current holdings of coin in binance""" binance_model.balance(self.current_coin, other_args) - def call_add(self, _): - self.current_coin = input("Coin?").upper() - + def call_add(self, other_args): + """Add command - set current coin for binance operations""" + self.current_coin = binance_model.add_binance_coin(other_args) + print(f"{self.current_coin} loaded") + print("") def menu(): crypto_controller = CryptoController() From f0bdbda48b8309ad5b004f3016c782a308832de7 Mon Sep 17 00:00:00 2001 From: Jamie Date: Sat, 1 May 2021 19:09:12 -0400 Subject: [PATCH 04/74] refactor + add argparse ability for coins --- .../cryptocurrency/binance_model.py | 108 ++++++++++++++---- .../cryptocurrency/binance_view.py | 2 +- .../cryptocurrency/crypto_controller.py | 12 +- .../cryptocurrency/crypto_helper.py | 15 --- .../cryptocurrency/pycoingecko_view.py | 12 +- 5 files changed, 104 insertions(+), 45 deletions(-) delete mode 100644 gamestonk_terminal/cryptocurrency/crypto_helper.py diff --git a/gamestonk_terminal/cryptocurrency/binance_model.py b/gamestonk_terminal/cryptocurrency/binance_model.py index e7b1f5de1d8..8346035e643 100644 --- a/gamestonk_terminal/cryptocurrency/binance_model.py +++ b/gamestonk_terminal/cryptocurrency/binance_model.py @@ -22,7 +22,7 @@ def check_valid_binance_str(symbol: str): raise argparse.ArgumentTypeError(f"{symbol} is not a valid binance symbol") -def add_binance_coin(other_args:List[str]): +def add_binance_coin(other_args: List[str]): """ Define current_coin from binance Parameters @@ -51,29 +51,22 @@ def add_binance_coin(other_args:List[str]): type=str, default="USDT", ) - parser.add_argument( - "-s", - "--symbol", - help="binance symbol (overrides other arguments)", - dest="symbol", - default=None, - type=check_valid_binance_str, - ) try: + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-c") ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: - return None - - if ns_parser.symbol: - return ns_parser.symbol + return None, None coin = ns_parser.coin + ns_parser.quote - return check_valid_binance_str(coin) + if check_valid_binance_str(coin): + return ns_parser.coin.upper(), ns_parser.quote.upper() except Exception as e: print(e) print("") - return None + return None, None def order_book(coin: str, other_args: List[str]): @@ -90,13 +83,32 @@ def order_book(coin: str, other_args: List[str]): ------- """ - client = Client(BINANCE_API_KEY, BINANCE_SECRET_KEY) - order_book = client.get_order_book(symbol=coin) - bids = np.asarray(order_book["bids"], dtype=float) - asks = np.asarray(order_book["asks"], dtype=float) - bids = np.insert(bids, 2, bids[:, 1].cumsum(), axis=1) - asks = np.insert(asks, 2, np.flipud(asks[:, 1]).cumsum(), axis=1) - plot_order_book(bids, asks, coin) + limit_list = [5, 10, 20, 50, 100, 500, 1000, 5000] + parser = argparse.ArgumentParser(prog = "book", + add_help = False, + description="Get the order book for selected coin") + parser.add_argument("-l", + "--limit", + dest="limit", + help="Limit parameter. Adjusts the weight", + default = 100, + type = int, + choices=limit_list) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + client = Client(BINANCE_API_KEY, BINANCE_SECRET_KEY) + order_book = client.get_order_book(symbol=coin, limit = ns_parser.limit) + bids = np.asarray(order_book["bids"], dtype=float) + asks = np.asarray(order_book["asks"], dtype=float) + bids = np.insert(bids, 2, bids[:, 1].cumsum(), axis=1) + asks = np.insert(asks, 2, np.flipud(asks[:, 1]).cumsum(), axis=1) + plot_order_book(bids, asks, coin) + except Exception as e: + print(e) + print("") def candles(coin: str, other_args: List[str]): @@ -114,9 +126,53 @@ def candles(coin: str, other_args: List[str]): """ client = Client(BINANCE_API_KEY, BINANCE_SECRET_KEY) + + interval_map = { + "1day": client.KLINE_INTERVAL_1DAY, + "3day": client.KLINE_INTERVAL_3DAY, + "1hour": client.KLINE_INTERVAL_1HOUR, + "2hour": client.KLINE_INTERVAL_2HOUR, + "4hour": client.KLINE_INTERVAL_4HOUR, + "6hour": client.KLINE_INTERVAL_6HOUR, + "8hour": client.KLINE_INTERVAL_8HOUR, + "12hour": client.KLINE_INTERVAL_12HOUR, + "1week": client.KLINE_INTERVAL_1WEEK, + "1min": client.KLINE_INTERVAL_1MINUTE, + "3min": client.KLINE_INTERVAL_3MINUTE, + "5min": client.KLINE_INTERVAL_5MINUTE, + "15min": client.KLINE_INTERVAL_15MINUTE, + "30min": client.KLINE_INTERVAL_30MINUTE, + "1month": client.KLINE_INTERVAL_1MONTH, + } + interval_choices = list(interval_map.keys()) + + parser = argparse.ArgumentParser( + prog="candle", + add_help=False, + description="Program to plot candles for binance data", + ) + parser.add_argument( + "-i", + "--interval", + help="Interval to get data", + choices=interval_choices, + dest="interval", + default="1day", + type=str, + ) + + parser.add_argument( + "-l", "--limit", dest="limit", default=200, help="Number to get", type=int + ) + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return candles = client.get_klines( - symbol=coin, interval=Client.KLINE_INTERVAL_30MINUTE + symbol=coin, + interval=interval_map[ns_parser.interval], + limit=ns_parser.limit, ) # Response @@ -148,7 +204,11 @@ def candles(coin: str, other_args: List[str]): (candles_df["Time0"] + candles_df["Time1"]) / 2, unit="ms" ) - plot_candles(candles_df, "") + plot_candles( + candles_df, + f"{coin} from {candles_df.index[0].strftime('%Y/%m/%d')} to " + f"{candles_df.index[-1].strftime('%Y/%m/%d')}", + ) except Exception as e: print(e) diff --git a/gamestonk_terminal/cryptocurrency/binance_view.py b/gamestonk_terminal/cryptocurrency/binance_view.py index c59196fe4d4..1a988c94ad9 100644 --- a/gamestonk_terminal/cryptocurrency/binance_view.py +++ b/gamestonk_terminal/cryptocurrency/binance_view.py @@ -26,7 +26,6 @@ def plot_order_book(bids: np.array, asks: np.array, coin: str): ------- """ - _, ax = plt.subplots(figsize=plot_autoscale(), dpi=PLOT_DPI) ax.plot(bids[:, 0], bids[:, 2], "g", label="bids") ax.fill_between(bids[:, 0], bids[:, 2], color="g", alpha=0.4) @@ -76,3 +75,4 @@ def plot_candles(candles_df: pd.DataFrame, coin: str): if ion: plt.ion() plt.show() + print("") diff --git a/gamestonk_terminal/cryptocurrency/crypto_controller.py b/gamestonk_terminal/cryptocurrency/crypto_controller.py index f93af43132d..a3f07271ed8 100644 --- a/gamestonk_terminal/cryptocurrency/crypto_controller.py +++ b/gamestonk_terminal/cryptocurrency/crypto_controller.py @@ -38,6 +38,7 @@ class CryptoController: self.crypto_parser = argparse.ArgumentParser(add_help=False, prog="crypto") self.crypto_parser.add_argument("cmd", choices=self.CHOICES) self.current_coin = None + self.current_currency = None self.current_df = pd.DataFrame() @staticmethod @@ -115,11 +116,11 @@ class CryptoController: def call_book(self, other_args): """Book command - get order book from binance""" - binance_model.order_book(self.current_coin, other_args) + binance_model.order_book(self.current_coin + self.current_currency, other_args) def call_candle(self, other_args): """Candle command - show candle chart from binance""" - binance_model.candles(self.current_coin, other_args) + binance_model.candles(self.current_coin + self.current_currency, other_args) def call_balance(self, other_args): """Balance command - check current holdings of coin in binance""" @@ -127,10 +128,13 @@ class CryptoController: def call_add(self, other_args): """Add command - set current coin for binance operations""" - self.current_coin = binance_model.add_binance_coin(other_args) - print(f"{self.current_coin} loaded") + self.current_coin, self.current_currency = binance_model.add_binance_coin( + other_args + ) + print(f"{self.current_coin} loaded vs {self.current_currency}") print("") + def menu(): crypto_controller = CryptoController() crypto_controller.print_help(crypto_controller) diff --git a/gamestonk_terminal/cryptocurrency/crypto_helper.py b/gamestonk_terminal/cryptocurrency/crypto_helper.py deleted file mode 100644 index f895f264bd5..00000000000 --- a/gamestonk_terminal/cryptocurrency/crypto_helper.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -Helper script that generates a dictionary to convert between symbols and IDs. -This allows the user to input (for example) "btc" instead of the expected "bitcoin" - -""" -from pycoingecko import CoinGeckoAPI - -cg = CoinGeckoAPI() -coins = cg.get_coins() -coin_symbol_to_id = {} -coin_ids = [] - -for coin in coins: - coin_symbol_to_id[coin["symbol"]] = coin["id"] - coin_ids.append(coin["id"]) diff --git a/gamestonk_terminal/cryptocurrency/pycoingecko_view.py b/gamestonk_terminal/cryptocurrency/pycoingecko_view.py index bb042a60432..4402703165d 100644 --- a/gamestonk_terminal/cryptocurrency/pycoingecko_view.py +++ b/gamestonk_terminal/cryptocurrency/pycoingecko_view.py @@ -9,10 +9,20 @@ import matplotlib.pyplot as plt from tabulate import tabulate from pycoingecko import CoinGeckoAPI from gamestonk_terminal.helper_funcs import parse_known_args_and_warn, plot_autoscale -from gamestonk_terminal.cryptocurrency.crypto_helper import coin_symbol_to_id, coin_ids from gamestonk_terminal.config_plot import PLOT_DPI register_matplotlib_converters() + +# Generate a list of valid coins to be checked against later +cg_api = CoinGeckoAPI() +coins = cg_api.get_coins() +coin_symbol_to_id = {} +coin_ids = [] + +for single_coin in coins: + coin_symbol_to_id[single_coin["symbol"]] = single_coin["id"] + coin_ids.append(single_coin["id"]) + # pylint: disable=inconsistent-return-statements From af58e97c63ea54e62b8a4f383413ac99b44140bd Mon Sep 17 00:00:00 2001 From: Jamie Date: Sat, 1 May 2021 19:20:46 -0400 Subject: [PATCH 05/74] add balance command --- .../cryptocurrency/binance_model.py | 38 +++++++++++-------- .../cryptocurrency/binance_view.py | 10 ++--- .../cryptocurrency/crypto_controller.py | 4 +- gamestonk_terminal/main_helper.py | 2 +- 4 files changed, 31 insertions(+), 23 deletions(-) diff --git a/gamestonk_terminal/cryptocurrency/binance_model.py b/gamestonk_terminal/cryptocurrency/binance_model.py index 8346035e643..8346ca43ab5 100644 --- a/gamestonk_terminal/cryptocurrency/binance_model.py +++ b/gamestonk_terminal/cryptocurrency/binance_model.py @@ -84,23 +84,25 @@ def order_book(coin: str, other_args: List[str]): """ limit_list = [5, 10, 20, 50, 100, 500, 1000, 5000] - parser = argparse.ArgumentParser(prog = "book", - add_help = False, - description="Get the order book for selected coin") - parser.add_argument("-l", - "--limit", - dest="limit", - help="Limit parameter. Adjusts the weight", - default = 100, - type = int, - choices=limit_list) + parser = argparse.ArgumentParser( + prog="book", add_help=False, description="Get the order book for selected coin" + ) + parser.add_argument( + "-l", + "--limit", + dest="limit", + help="Limit parameter. Adjusts the weight", + default=100, + type=int, + choices=limit_list, + ) try: ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return client = Client(BINANCE_API_KEY, BINANCE_SECRET_KEY) - order_book = client.get_order_book(symbol=coin, limit = ns_parser.limit) + order_book = client.get_order_book(symbol=coin, limit=ns_parser.limit) bids = np.asarray(order_book["bids"], dtype=float) asks = np.asarray(order_book["asks"], dtype=float) bids = np.insert(bids, 2, bids[:, 1].cumsum(), axis=1) @@ -215,15 +217,13 @@ def candles(coin: str, other_args: List[str]): print("") -def balance(coin: str, other_args: List[str]): +def balance(coin: str): """ Get account holdings for asset Parameters ---------- coin: str Coin to get holdings of - other_args : List[str] - Argparse arguments Returns ------- @@ -231,7 +231,15 @@ def balance(coin: str, other_args: List[str]): """ client = Client(BINANCE_API_KEY, BINANCE_SECRET_KEY) try: - client.get_asset_balance(asset=coin) + balance = client.get_asset_balance(asset=coin) + if balance is None: + print("Check loaded coin") + return + print("") + print(f"You currently have {balance['free']} free and {balance['locked']} locked") + print("") + return except Exception as e: print(e) print("") + return diff --git a/gamestonk_terminal/cryptocurrency/binance_view.py b/gamestonk_terminal/cryptocurrency/binance_view.py index 1a988c94ad9..8ddbebee199 100644 --- a/gamestonk_terminal/cryptocurrency/binance_view.py +++ b/gamestonk_terminal/cryptocurrency/binance_view.py @@ -44,15 +44,15 @@ def plot_order_book(bids: np.array, asks: np.array, coin: str): print("") -def plot_candles(candles_df: pd.DataFrame, coin: str): +def plot_candles(candles_df: pd.DataFrame, title: str): """ Plot candle chart from dataframe Parameters ---------- candles_df: pd.DataFrame Dataframe containing time and OHLVC - coin: str - Coin plotting for + title: str + title of graph Returns ------- @@ -62,8 +62,8 @@ def plot_candles(candles_df: pd.DataFrame, coin: str): candles_df, type="candle", volume=True, - title=f"{coin}", - xrotation=10, + title=f"{title}", + xrotation=20, style="binance", figratio=(10, 7), figscale=1.10, diff --git a/gamestonk_terminal/cryptocurrency/crypto_controller.py b/gamestonk_terminal/cryptocurrency/crypto_controller.py index a3f07271ed8..8f6704e816e 100644 --- a/gamestonk_terminal/cryptocurrency/crypto_controller.py +++ b/gamestonk_terminal/cryptocurrency/crypto_controller.py @@ -122,9 +122,9 @@ class CryptoController: """Candle command - show candle chart from binance""" binance_model.candles(self.current_coin + self.current_currency, other_args) - def call_balance(self, other_args): + def call_balance(self, _): """Balance command - check current holdings of coin in binance""" - binance_model.balance(self.current_coin, other_args) + binance_model.balance(self.current_coin) def call_add(self, other_args): """Add command - set current coin for binance operations""" diff --git a/gamestonk_terminal/main_helper.py b/gamestonk_terminal/main_helper.py index 2c813d24a46..eef982c7349 100644 --- a/gamestonk_terminal/main_helper.py +++ b/gamestonk_terminal/main_helper.py @@ -57,7 +57,7 @@ def print_help(s_ticker, s_start, s_interval, b_is_market_open): print(" mill papermill menu, \t\t\t menu to generate notebook reports") print(" econ economic data, \t\t\t from: FRED, VIX") print(" pa portfolio analysis, \t\t supports: robinhood, alpaca, ally ") - print(" crypto cryptocurrencies, \t\t uses coingecko api") + print(" crypto cryptocurrencies, \t\t coingecko, coinmarketcap and binance") print( " po portfolio optimization, \t\t optimal portfolio weights from pyportfolioopt" ) From 5ee71c08f2cf96f966711f9cc88583e3a9efa67f Mon Sep 17 00:00:00 2001 From: Jamie Date: Sat, 1 May 2021 20:05:05 -0400 Subject: [PATCH 06/74] crypto readme --- gamestonk_terminal/cryptocurrency/README.md | 56 ++++++++++++++++++- .../cryptocurrency/binance_model.py | 19 ++++--- .../cryptocurrency/crypto_controller.py | 5 +- 3 files changed, 69 insertions(+), 11 deletions(-) diff --git a/gamestonk_terminal/cryptocurrency/README.md b/gamestonk_terminal/cryptocurrency/README.md index 4a1b7b48fea..882e2756a51 100644 --- a/gamestonk_terminal/cryptocurrency/README.md +++ b/gamestonk_terminal/cryptocurrency/README.md @@ -6,9 +6,21 @@ This menu aims to explore crypto world, and the usage of the following commands * load a given coin vs a given currency [CoinGecko] * [view](#view) * plot the loaded crypto data +* [trend](#trend) + * show the top 7 coins from coingecko * [top](#top) * view top coins from coinmarketcap [coinmarketcap.com] - +* [add](#add) + * add a coin/currency pair for Binance + +* [book](#book) + * get and show market (order) book + +* [candle](#candle) + * show candle plot of selected coin/currency + +* [balance](#balance) + * show current holdings in your account for given coin ## load @@ -35,6 +47,12 @@ Plot the loaded crypto data. ![crypto_view](https://user-images.githubusercontent.com/25267873/115787452-20889a80-a3ba-11eb-9216-f7fd1ffc98cf.png) +## trend +Print the top 7 trending coins from coingecko +```` +usage: trend +```` + ## top @@ -50,3 +68,39 @@ This command displays the top n cryptocurrencies from coinmarketcap.com. crypto +## add +Add a coin/currency to the current object. Note that in binance, the exchange "ticker" is usually COINCURR, such as BTCEUR +for BTC to EURO. There is no USD, but it uses a coin tethered to the USD (USDT), which is the default. Some symbols +are a combination of coins, as `ETHBTC` is a valid symbol +```` +usage: add [-c --coin] [-q --quote] +```` +* -c/--coin Coin to load. If not specified, BTC will be loaded to prevent errors later. +* -q/--quote Quote currency. Defaults to `USDT` (which is 1-to-1 with USD) + +Note that the usage `load btc` will also work, as it will add `-c` if not detected. + +## book +Gets and shows the order book for the given coin/currency. Shows the cumulative amount of orders. +```` +usage: book -l/--limit +```` +* -l/--limit Number of orders to get on each side. One of [5,10,20,50,100,500,1000,5000]. Defaults to 100. + +## candle +Show candle chart for loaded coin/currency. (Terminal will quit if coin is not defined). +```` +usage: candle [-i --interval] [-l --limit] +```` +* -i/--interval. Interval for candles. One of [1day,3day,1hour,2hour,4hour,6hour,8hour,12hour,1week,1min,3min,5min,15min,30min,1month]. + Defaults to 1day. +* -l/--limit. Number of candles to get. Defaults to 100. +Example output (using 3day interval and 50 candles) + +![im](https://user-images.githubusercontent.com/18151143/116797645-f455d380-aab5-11eb-8dbb-df257425302d.png) + +## balance +Shows the current holding balance in your account. +```` +usage: balance +```` diff --git a/gamestonk_terminal/cryptocurrency/binance_model.py b/gamestonk_terminal/cryptocurrency/binance_model.py index 8346ca43ab5..d9e1e0a56dc 100644 --- a/gamestonk_terminal/cryptocurrency/binance_model.py +++ b/gamestonk_terminal/cryptocurrency/binance_model.py @@ -102,18 +102,19 @@ def order_book(coin: str, other_args: List[str]): if not ns_parser: return client = Client(BINANCE_API_KEY, BINANCE_SECRET_KEY) - order_book = client.get_order_book(symbol=coin, limit=ns_parser.limit) - bids = np.asarray(order_book["bids"], dtype=float) - asks = np.asarray(order_book["asks"], dtype=float) + market_book = client.get_order_book(symbol=coin, limit=ns_parser.limit) + bids = np.asarray(market_book["bids"], dtype=float) + asks = np.asarray(market_book["asks"], dtype=float) bids = np.insert(bids, 2, bids[:, 1].cumsum(), axis=1) asks = np.insert(asks, 2, np.flipud(asks[:, 1]).cumsum(), axis=1) plot_order_book(bids, asks, coin) + except Exception as e: print(e) print("") -def candles(coin: str, other_args: List[str]): +def show_candles(coin: str, other_args: List[str]): """ Get klines/candles for coin Parameters @@ -164,7 +165,7 @@ def candles(coin: str, other_args: List[str]): ) parser.add_argument( - "-l", "--limit", dest="limit", default=200, help="Number to get", type=int + "-l", "--limit", dest="limit", default=100, help="Number to get", type=int ) try: @@ -231,12 +232,14 @@ def balance(coin: str): """ client = Client(BINANCE_API_KEY, BINANCE_SECRET_KEY) try: - balance = client.get_asset_balance(asset=coin) - if balance is None: + current_balance = client.get_asset_balance(asset=coin) + if current_balance is None: print("Check loaded coin") return print("") - print(f"You currently have {balance['free']} free and {balance['locked']} locked") + print( + f"You currently have {current_balance['free']} free and {current_balance['locked']} locked" + ) print("") return except Exception as e: diff --git a/gamestonk_terminal/cryptocurrency/crypto_controller.py b/gamestonk_terminal/cryptocurrency/crypto_controller.py index 8f6704e816e..a40596315be 100644 --- a/gamestonk_terminal/cryptocurrency/crypto_controller.py +++ b/gamestonk_terminal/cryptocurrency/crypto_controller.py @@ -120,7 +120,9 @@ class CryptoController: def call_candle(self, other_args): """Candle command - show candle chart from binance""" - binance_model.candles(self.current_coin + self.current_currency, other_args) + binance_model.show_candles( + self.current_coin + self.current_currency, other_args + ) def call_balance(self, _): """Balance command - check current holdings of coin in binance""" @@ -134,7 +136,6 @@ class CryptoController: print(f"{self.current_coin} loaded vs {self.current_currency}") print("") - def menu(): crypto_controller = CryptoController() crypto_controller.print_help(crypto_controller) From 31f9fbb0825b4e79d611d80275bd4c8ac4dee9a1 Mon Sep 17 00:00:00 2001 From: Jamie Date: Sat, 1 May 2021 20:07:14 -0400 Subject: [PATCH 07/74] gamestonk readme --- gamestonk_terminal/README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/gamestonk_terminal/README.md b/gamestonk_terminal/README.md index a3aab2939c8..d9dd64c259f 100644 --- a/gamestonk_terminal/README.md +++ b/gamestonk_terminal/README.md @@ -272,9 +272,14 @@ Command|Description Command|Description ------ | ------------ -`load`| load cryptocurrency data -`view`| load and view cryptocurrency data +`load`| load cryptocurrency data from coingecko +`view`| view loaded cryptocurrency data +`trend`| view top 7 coins from coingecko `top` | view top coins from coinmarketcap +`add` | add binance ticker +`book`| get and show order book for symbol +`candle`| show candlechart for symbol +`balance`| look at current binance holdings for loaded coin   ## Comparison Analysis [ยป](comparison_analysis/README.md) From fe5f550e1c3c7eba84619dd74cc61bf26350f478 Mon Sep 17 00:00:00 2001 From: Jamie Date: Sat, 1 May 2021 20:08:29 -0400 Subject: [PATCH 08/74] roadmap + black --- ROADMAP.md | 2 +- gamestonk_terminal/cryptocurrency/crypto_controller.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ROADMAP.md b/ROADMAP.md index 64a4900b9e2..7b70578079b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -198,7 +198,7 @@ ___ **NEXT** * [ ] Add [Coinpaprika](https://coinpaprika.com/api/) * [ ] Add crypto charts -* [ ] Add binance +* [x] Add binance (@jmaslek) * [ ] Add top altcoin lists --- diff --git a/gamestonk_terminal/cryptocurrency/crypto_controller.py b/gamestonk_terminal/cryptocurrency/crypto_controller.py index a40596315be..c4c2901bef6 100644 --- a/gamestonk_terminal/cryptocurrency/crypto_controller.py +++ b/gamestonk_terminal/cryptocurrency/crypto_controller.py @@ -136,6 +136,7 @@ class CryptoController: print(f"{self.current_coin} loaded vs {self.current_currency}") print("") + def menu(): crypto_controller = CryptoController() crypto_controller.print_help(crypto_controller) From 552602311c0647d0e63741a47871fd58679661e0 Mon Sep 17 00:00:00 2001 From: Jamie Date: Sat, 1 May 2021 20:13:26 -0400 Subject: [PATCH 09/74] reqs + poetry --- poetry.lock | 89 ++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + requirements-full.txt | 1 + requirements-old.txt | 1 + requirements-win.txt | 1 + requirements.txt | 1 + 6 files changed, 93 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 3b481c673f8..38f2f7879e1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -436,6 +436,23 @@ category = "main" optional = false python-versions = ">=3.6, <3.7" +[[package]] +name = "dateparser" +version = "1.0.0" +description = "Date parsing library designed to parse dates from HTML pages" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +python-dateutil = "*" +pytz = "*" +regex = "!=2019.02.19" +tzlocal = "*" + +[package.extras] +calendars = ["convertdate", "hijri-converter", "convertdate"] + [[package]] name = "datetime" version = "4.3" @@ -2120,6 +2137,22 @@ toml = "*" [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] +[[package]] +name = "python-binance" +version = "1.0.1" +description = "Binance REST API python implementation" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +aiohttp = "*" +dateparser = "*" +requests = "*" +six = "*" +ujson = "*" +websockets = "*" + [[package]] name = "python-coinmarketcap" version = "0.2" @@ -2863,6 +2896,25 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "tzlocal" +version = "2.1" +description = "tzinfo object for the local timezone" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pytz = "*" + +[[package]] +name = "ujson" +version = "4.0.2" +description = "Ultra fast JSON encoder and decoder for Python" +category = "main" +optional = false +python-versions = ">=3.6" + [[package]] name = "update-checker" version = "0.18.0" @@ -3036,7 +3088,7 @@ prediction = ["fbprophet", "tensorflow", "pmdarima", "transformers", "flair", "t [metadata] lock-version = "1.1" python-versions = "^3.6.8" -content-hash = "51e9191a579c58d229a6a32b419f3f8d891d2e7ce12decb6f74a283fb46a851a" +content-hash = "9f2c8b046494863b5a83e395b8a79edbca42fd2b61b5e977877e08aedc53e165" [metadata.files] absl-py = [ @@ -3329,6 +3381,10 @@ dataclasses = [ {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"}, {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"}, ] +dateparser = [ + {file = "dateparser-1.0.0-py2.py3-none-any.whl", hash = "sha256:17202df32c7a36e773136ff353aa3767e987f8b3e27374c39fd21a30a803d6f8"}, + {file = "dateparser-1.0.0.tar.gz", hash = "sha256:159cc4e01a593706a15cd4e269a0b3345edf3aef8bf9278a57dac8adf5bf1e4a"}, +] datetime = [ {file = "DateTime-4.3-py2.py3-none-any.whl", hash = "sha256:371dba07417b929a4fa685c2f7a3eaa6a62d60c02947831f97d4df9a9e70dfd0"}, {file = "DateTime-4.3.tar.gz", hash = "sha256:5cef605bab8259ff61281762cdf3290e459fbf0b4719951d5fab967d5f2ea0ea"}, @@ -4400,6 +4456,10 @@ pytest = [ {file = "pytest-6.2.3-py3-none-any.whl", hash = "sha256:6ad9c7bdf517a808242b998ac20063c41532a570d088d77eec1ee12b0b5574bc"}, {file = "pytest-6.2.3.tar.gz", hash = "sha256:671238a46e4df0f3498d1c3270e5deb9b32d25134c99b7d75370a68cfbe9b634"}, ] +python-binance = [ + {file = "python-binance-1.0.1.tar.gz", hash = "sha256:7987189651938bb81a4e2f86993a32c77c55255d9c33891efb1d2bca918b8142"}, + {file = "python_binance-1.0.1-py2.py3-none-any.whl", hash = "sha256:80dc4402837103024c44f0f57c5ac90c43e5ebe1da4f53104ebd4b9b788bd17d"}, +] python-coinmarketcap = [ {file = "python-coinmarketcap-0.2.tar.gz", hash = "sha256:b1e0e8b098fc910a5ec943a06774d5ee3b190e915123366e2f16238ef0a8a433"}, {file = "python_coinmarketcap-0.2-py2-none-any.whl", hash = "sha256:54c278154ee11ca78837fb73eae8c322a60af93dc0ce75b515fecf895d181cf5"}, @@ -5009,6 +5069,33 @@ typing-extensions = [ {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, ] +tzlocal = [ + {file = "tzlocal-2.1-py2.py3-none-any.whl", hash = "sha256:e2cb6c6b5b604af38597403e9852872d7f534962ae2954c7f35efcb1ccacf4a4"}, + {file = "tzlocal-2.1.tar.gz", hash = "sha256:643c97c5294aedc737780a49d9df30889321cbe1204eac2c2ec6134035a92e44"}, +] +ujson = [ + {file = "ujson-4.0.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:e390df0dcc7897ffb98e17eae1f4c442c39c91814c298ad84d935a3c5c7a32fa"}, + {file = "ujson-4.0.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:84b1dca0d53b0a8d58835f72ea2894e4d6cf7a5dd8f520ab4cbd698c81e49737"}, + {file = "ujson-4.0.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:91396a585ba51f84dc71c8da60cdc86de6b60ba0272c389b6482020a1fac9394"}, + {file = "ujson-4.0.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:eb6b25a7670c7537a5998e695fa62ff13c7f9c33faf82927adf4daa460d5f62e"}, + {file = "ujson-4.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f8aded54c2bc554ce20b397f72101737dd61ee7b81c771684a7dd7805e6cca0c"}, + {file = "ujson-4.0.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:30962467c36ff6de6161d784cd2a6aac1097f0128b522d6e9291678e34fb2b47"}, + {file = "ujson-4.0.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:fc51e545d65689c398161f07fd405104956ec27f22453de85898fa088b2cd4bb"}, + {file = "ujson-4.0.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e6e90330670c78e727d6637bb5a215d3e093d8e3570d439fd4922942f88da361"}, + {file = "ujson-4.0.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:5e1636b94c7f1f59a8ead4c8a7bab1b12cc52d4c21ababa295ffec56b445fd2a"}, + {file = "ujson-4.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:e2cadeb0ddc98e3963bea266cc5b884e5d77d73adf807f0bda9eca64d1c509d5"}, + {file = "ujson-4.0.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:a214ba5a21dad71a43c0f5aef917cd56a2d70bc974d845be211c66b6742a471c"}, + {file = "ujson-4.0.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:0190d26c0e990c17ad072ec8593647218fe1c675d11089cd3d1440175b568967"}, + {file = "ujson-4.0.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:f273a875c0b42c2a019c337631bc1907f6fdfbc84210cc0d1fff0e2019bbfaec"}, + {file = "ujson-4.0.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:d3a87888c40b5bfcf69b4030427cd666893e826e82cc8608d1ba8b4b5e04ea99"}, + {file = "ujson-4.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:7333e8bc45ea28c74ae26157eacaed5e5629dbada32e0103c23eb368f93af108"}, + {file = "ujson-4.0.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:b3a6dcc660220539aa718bcc9dbd6dedf2a01d19c875d1033f028f212e36d6bb"}, + {file = "ujson-4.0.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:0ea07fe57f9157118ca689e7f6db72759395b99121c0ff038d2e38649c626fb1"}, + {file = "ujson-4.0.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:4d6d061563470cac889c0a9fd367013a5dbd8efc36ad01ab3e67a57e56cad720"}, + {file = "ujson-4.0.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b5c70704962cf93ec6ea3271a47d952b75ae1980d6c56b8496cec2a722075939"}, + {file = "ujson-4.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:aad6d92f4d71e37ea70e966500f1951ecd065edca3a70d3861b37b176dd6702c"}, + {file = "ujson-4.0.2.tar.gz", hash = "sha256:c615a9e9e378a7383b756b7e7a73c38b22aeb8967a8bfbffd4741f7ffd043c4d"}, +] update-checker = [ {file = "update_checker-0.18.0-py3-none-any.whl", hash = "sha256:cbba64760a36fe2640d80d85306e8fe82b6816659190993b7bdabadee4d4bbfd"}, {file = "update_checker-0.18.0.tar.gz", hash = "sha256:6a2d45bb4ac585884a6b03f9eade9161cedd9e8111545141e9aa9058932acb13"}, diff --git a/pyproject.toml b/pyproject.toml index 65ac1931eb2..43efb105290 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,7 @@ oandapyV20 = "^0.6.3" degiroapi = "^0.9.5" valinvest = "^0.0.2" bt = "^0.2.9" +python-binance = "^1.0.1" [tool.poetry.dev-dependencies] pytest = "^6.2.2" diff --git a/requirements-full.txt b/requirements-full.txt index a8c07a921e4..a3f79bc3a5d 100644 --- a/requirements-full.txt +++ b/requirements-full.txt @@ -149,6 +149,7 @@ pyportfolioopt==1.4.1; python_full_version >= "3.6.1" and python_full_version < pyrsistent==0.14.11 pysocks==1.7.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" pystan==2.19.1.1 +python-binance==1.0.1 python-coinmarketcap==0.2 python-dateutil==2.8.1; python_full_version >= "3.6.1" and python_version >= "3.6" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6") and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0") and (python_version >= "2.7" and python_full_version < "3.0.0" and python_version < "4" or python_version >= "2.7" and python_version < "4" and python_full_version >= "3.3.0") python-dotenv==0.17.0; python_version >= "3" diff --git a/requirements-old.txt b/requirements-old.txt index 77b359de143..71b5b6d736b 100644 --- a/requirements-old.txt +++ b/requirements-old.txt @@ -94,6 +94,7 @@ pyparsing==2.4.7; python_full_version >= "3.6.0" and python_version >= "3.6" pyportfolioopt==1.4.1 pysocks==1.7.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" pystan==2.18 +python-binance==1.0.1 python-coinmarketcap==0.2 python-dateutil==2.8.1; python_full_version >= "3.6.1" and python_version >= "3.5" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6") and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0") and (python_version >= "2.7" and python_full_version < "3.0.0" and python_version < "4" or python_version >= "2.7" and python_version < "4" and python_full_version >= "3.3.0") pytest==6.2.2; python_version >= "3.6" diff --git a/requirements-win.txt b/requirements-win.txt index 1f658e78cc9..b12bd43282f 100644 --- a/requirements-win.txt +++ b/requirements-win.txt @@ -96,6 +96,7 @@ pyparsing==2.4.7; python_full_version >= "3.6.0" and python_version >= "3.6" pyportfolioopt==1.4.1 pysocks==1.7.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" pystan==2.19.1.1 +python-binance==1.0.1 python-coinmarketcap==0.2 python-dateutil==2.8.1; python_full_version >= "3.6.1" and python_version >= "3.5" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6") and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0") and (python_version >= "2.7" and python_full_version < "3.0.0" and python_version < "4" or python_version >= "2.7" and python_version < "4" and python_full_version >= "3.3.0") pytest==6.2.2; python_version >= "3.6" diff --git a/requirements.txt b/requirements.txt index 4a189ab8556..04cdb8207db 100644 --- a/requirements.txt +++ b/requirements.txt @@ -113,6 +113,7 @@ pyparsing==2.4.7; python_version >= "3.6" and python_full_version < "3.0.0" or p pyportfolioopt==1.4.1; python_full_version >= "3.6.1" and python_full_version < "4.0.0" pyrsistent==0.14.11 pysocks==1.7.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" +python-binance==1.0.1 python-coinmarketcap==0.2 python-dateutil==2.8.1; python_full_version >= "3.6.1" and python_version >= "3.6" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6") and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0") python-dotenv==0.17.0; python_version >= "3" From e5985287ae6fe04c3396097ffcdd90e95d2b9c15 Mon Sep 17 00:00:00 2001 From: Jamie Date: Sat, 1 May 2021 20:19:13 -0400 Subject: [PATCH 10/74] black --- gamestonk_terminal/main_helper.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gamestonk_terminal/main_helper.py b/gamestonk_terminal/main_helper.py index 173aa345dec..5edac9f0840 100644 --- a/gamestonk_terminal/main_helper.py +++ b/gamestonk_terminal/main_helper.py @@ -59,7 +59,9 @@ def print_help(s_ticker, s_start, s_interval, b_is_market_open): print( " > pa portfolio analysis, \t\t supports: robinhood, alpaca, ally " ) - print(" > crypto cryptocurrencies, \t\t from: coingecko, coinmarketcap, binance") + print( + " > crypto cryptocurrencies, \t\t from: coingecko, coinmarketcap, binance" + ) print( " > po portfolio optimization, \t optimal portfolio weights from pyportfolioopt" ) From 84fa8bc12276bd0c4a2cf640e39dc03ac6459552 Mon Sep 17 00:00:00 2001 From: Jamie Date: Sat, 1 May 2021 20:25:54 -0400 Subject: [PATCH 11/74] pylint / black --- gamestonk_terminal/cryptocurrency/binance_model.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/gamestonk_terminal/cryptocurrency/binance_model.py b/gamestonk_terminal/cryptocurrency/binance_model.py index d9e1e0a56dc..dfa1ff049e0 100644 --- a/gamestonk_terminal/cryptocurrency/binance_model.py +++ b/gamestonk_terminal/cryptocurrency/binance_model.py @@ -18,8 +18,13 @@ def check_valid_binance_str(symbol: str): try: client.get_avg_price(symbol=symbol.upper()) return symbol.upper() - except BinanceAPIException: - raise argparse.ArgumentTypeError(f"{symbol} is not a valid binance symbol") + except BinanceAPIException as e: + raise argparse.ArgumentTypeError( + f"{symbol} is not a valid binance symbol" + ) from e + + +# pylint: disable=inconsistent-return-statements def add_binance_coin(other_args: List[str]): From ee44855ee332e546aedf6de39a9a55dead61a38b Mon Sep 17 00:00:00 2001 From: Jamie Date: Sat, 1 May 2021 20:30:06 -0400 Subject: [PATCH 12/74] pylint / black --- gamestonk_terminal/cryptocurrency/crypto_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gamestonk_terminal/cryptocurrency/crypto_controller.py b/gamestonk_terminal/cryptocurrency/crypto_controller.py index c4c2901bef6..c58da931f3f 100644 --- a/gamestonk_terminal/cryptocurrency/crypto_controller.py +++ b/gamestonk_terminal/cryptocurrency/crypto_controller.py @@ -42,13 +42,13 @@ class CryptoController: self.current_df = pd.DataFrame() @staticmethod - def print_help(self): + def print_help(cont): """Print help""" print("\nCryptocurrency:") print(" help show this menu again") print(" q quit this menu, and shows back to main menu") print(" quit quit to abandon program") - print(f"\nCurrent Coin: {self.current_coin}") + print(f"\nCurrent Coin: {cont.current_coin}") print("") print("Coingecko:") print(" load load cryptocurrency data") From c7daf13529e93b8058a90129dc3e6d423c90ea8b Mon Sep 17 00:00:00 2001 From: didier Date: Sun, 2 May 2021 02:28:41 +0100 Subject: [PATCH 13/74] Add pattern recognition to technical analysis --- .../technical_analysis/finhub_view.py | 206 ++++++++++++++++++ .../technical_analysis/ta_controller.py | 8 + 2 files changed, 214 insertions(+) create mode 100644 gamestonk_terminal/technical_analysis/finhub_view.py diff --git a/gamestonk_terminal/technical_analysis/finhub_view.py b/gamestonk_terminal/technical_analysis/finhub_view.py new file mode 100644 index 00000000000..3aa05d3eb28 --- /dev/null +++ b/gamestonk_terminal/technical_analysis/finhub_view.py @@ -0,0 +1,206 @@ +import argparse +from typing import List +import requests +import yfinance as yf +import mplfinance as mpf +import pandas as pd +import math +from gamestonk_terminal import config_terminal as cfg +from gamestonk_terminal.helper_funcs import ( + parse_known_args_and_warn, + plot_autoscale, +) + +from datetime import datetime + + +def get_pattern_recognition(ticker: str, resolution: str) -> pd.DataFrame: + """Get pattern recognition data + + Parameters + ---------- + ticker : str + Ticker to get pattern recognition data + resolution : str + Resolution of data to get pattern recognition from + + Returns + ------- + pd.DataFrame + Get datapoints corresponding to pattern signal data + """ + response = requests.get( + f"https://finnhub.io/api/v1/scan/pattern?symbol={ticker}&resolution={resolution}&token={cfg.API_FINNHUB_KEY}" + ) + if response.status_code == 200: + d_data = response.json() + if "points" in d_data: + return pd.DataFrame(d_data["points"]).T + + return pd.DataFrame() + + +def plot_pattern_recognition(ticker: str, pattern: pd.DataFrame, segments: List): + """Plot pattern recognition signal + + Parameters + ---------- + ticker : str + Ticker to display pattern recognition on top of the data + pattern : pd.DataFrame + Pattern recognition signal data + segments : List + Segment lines that indicates some pattern signal in data + """ + + start_time = 999999999999 + for i in pattern: + if pattern[i]["atime"] < start_time: + start_time = pattern[i]["atime"] + + df_stock = yf.download( + ticker, + start=datetime.utcfromtimestamp(start_time).strftime("%Y-%m-%d"), + progress=False, + ) + + df_stock["date_id"] = (df_stock.index.date - df_stock.index.date.min()).astype( + "timedelta64[D]" + ) + df_stock["date_id"] = df_stock["date_id"].dt.days + 1 + + df_stock["OC_High"] = df_stock[["Open", "Close"]].max(axis=1) + df_stock["OC_Low"] = df_stock[["Open", "Close"]].min(axis=1) + + 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=False) + + mpf.plot( + df_stock, + type="candle", + volume=False, + title=f"\n{ticker}", + alines=segments, + 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 + ), + ) + + +def pattern_recognition_view(other_args: List[str], ticker: str): + """Display pattern recognition signals on the data + + Parameters + ---------- + other_args : List[str] + Command line arguments to be processed with argparse + ticker : str + Ticker to display pattern recognition on top of the data + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="pr", + description=""" + Display pattern recognition signals on the data. [Source: https://finnhub.io] + """, + ) + parser.add_argument( + "-r", + "--resolution", + action="store", + dest="resolution", + type=str, + default="D", + choices=["1", "5", "15", "30", "60", "D", "W", "M"], + help="Plot resolution to look for pattern signals", + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + df_pattern = get_pattern_recognition(ticker, ns_parser.resolution) + + if df_pattern.empty: + print("No pattern identified in this data", "\n") + return + + l_segments = list() + for i in df_pattern: + a_part = "" + if "aprice" in df_pattern[i]: + if df_pattern[i]["aprice"] != 0 and not math.isnan( + df_pattern[i]["aprice"] + ): + a_part = ( + datetime.utcfromtimestamp(df_pattern[i]["atime"]).strftime( + "%Y-%m-%d" + ), + df_pattern[i]["aprice"], + ) + + b_part = "" + if "bprice" in df_pattern[i]: + if df_pattern[i]["bprice"] != 0 and not math.isnan( + df_pattern[i]["bprice"] + ): + b_part = ( + datetime.utcfromtimestamp(df_pattern[i]["btime"]).strftime( + "%Y-%m-%d" + ), + df_pattern[i]["bprice"], + ) + + c_part = "" + if "cprice" in df_pattern[i]: + if df_pattern[i]["cprice"] != 0 and not math.isnan( + df_pattern[i]["cprice"] + ): + c_part = ( + datetime.utcfromtimestamp(df_pattern[i]["ctime"]).strftime( + "%Y-%m-%d" + ), + df_pattern[i]["cprice"], + ) + + d_part = "" + if "dprice" in df_pattern[i]: + if df_pattern[i]["dprice"] != 0 and not math.isnan( + df_pattern[i]["dprice"] + ): + d_part = ( + datetime.utcfromtimestamp(df_pattern[i]["dtime"]).strftime( + "%Y-%m-%d" + ), + df_pattern[i]["dprice"], + ) + + segment = (a_part, b_part, c_part, d_part) + + l_segment = list(segment) + while "" in l_segment: + l_segment.remove("") + segment = tuple(l_segment) + + l_segments.append(segment) + + plot_pattern_recognition(ticker, df_pattern, l_segments) + + for ix in range(len(df_pattern.columns)): + print(f"From {l_segments[ix][0][0]} to {l_segments[ix][-1][0]}") + print( + f"Pattern: {df_pattern[0]['patternname']} ({df_pattern[0]['patterntype']})" + ) + print("") + + except Exception as e: + print(e, "\n") diff --git a/gamestonk_terminal/technical_analysis/ta_controller.py b/gamestonk_terminal/technical_analysis/ta_controller.py index 620e13ab55f..685d4b104fa 100644 --- a/gamestonk_terminal/technical_analysis/ta_controller.py +++ b/gamestonk_terminal/technical_analysis/ta_controller.py @@ -20,6 +20,7 @@ from gamestonk_terminal.technical_analysis import volume as ta_volume from gamestonk_terminal.technical_analysis import finbrain_view from gamestonk_terminal.technical_analysis import tradingview_view from gamestonk_terminal.technical_analysis import finviz_view +from gamestonk_terminal.technical_analysis import finhub_view class TechnicalAnalysisController: @@ -33,6 +34,7 @@ class TechnicalAnalysisController: "view", "summary", "recom", + "pr", "ema", "sma", "vwap", @@ -88,6 +90,8 @@ class TechnicalAnalysisController: print( " recom recommendation based on Technical Indicators [Tradingview API]" ) + print(" pr pattern recognition [Finhub]") + print("") print("overlap:") print(" ema exponential moving average") print(" sma simple moving average") @@ -157,6 +161,10 @@ class TechnicalAnalysisController: """Process recom command""" tradingview_view.print_recommendation(other_args, self.ticker) + def call_pr(self, other_args: List[str]): + """Process pr command""" + finhub_view.pattern_recognition_view(other_args, self.ticker) + # OVERLAP def call_ema(self, other_args: List[str]): """Process ema command""" From cab21e566aeaf18b5a9852cee1a4610964aaec87 Mon Sep 17 00:00:00 2001 From: didier Date: Sun, 2 May 2021 12:29:00 +0100 Subject: [PATCH 14/74] fix mypy --- .../technical_analysis/finhub_view.py | 124 ++++++++---------- 1 file changed, 52 insertions(+), 72 deletions(-) diff --git a/gamestonk_terminal/technical_analysis/finhub_view.py b/gamestonk_terminal/technical_analysis/finhub_view.py index 3aa05d3eb28..9dd06f4d756 100644 --- a/gamestonk_terminal/technical_analysis/finhub_view.py +++ b/gamestonk_terminal/technical_analysis/finhub_view.py @@ -1,5 +1,5 @@ import argparse -from typing import List +from typing import List, Any import requests import yfinance as yf import mplfinance as mpf @@ -40,7 +40,7 @@ def get_pattern_recognition(ticker: str, resolution: str) -> pd.DataFrame: return pd.DataFrame() -def plot_pattern_recognition(ticker: str, pattern: pd.DataFrame, segments: List): +def plot_pattern_recognition(ticker: str, pattern: pd.DataFrame): """Plot pattern recognition signal Parameters @@ -49,10 +49,51 @@ def plot_pattern_recognition(ticker: str, pattern: pd.DataFrame, segments: List) Ticker to display pattern recognition on top of the data pattern : pd.DataFrame Pattern recognition signal data - segments : List - Segment lines that indicates some pattern signal in data """ + l_segments = list() + for i in pattern: + a_part = "" + if "aprice" in pattern[i]: + if pattern[i]["aprice"] != 0 and not math.isnan(pattern[i]["aprice"]): + a_part = ( + datetime.utcfromtimestamp(pattern[i]["atime"]).strftime("%Y-%m-%d"), + pattern[i]["aprice"], + ) + + b_part = "" + if "bprice" in pattern[i]: + if pattern[i]["bprice"] != 0 and not math.isnan(pattern[i]["bprice"]): + b_part = ( + datetime.utcfromtimestamp(pattern[i]["btime"]).strftime("%Y-%m-%d"), + pattern[i]["bprice"], + ) + + c_part = "" + if "cprice" in pattern[i]: + if pattern[i]["cprice"] != 0 and not math.isnan(pattern[i]["cprice"]): + c_part = ( + datetime.utcfromtimestamp(pattern[i]["ctime"]).strftime("%Y-%m-%d"), + pattern[i]["cprice"], + ) + + d_part = "" + if "dprice" in pattern[i]: + if pattern[i]["dprice"] != 0 and not math.isnan(pattern[i]["dprice"]): + d_part = ( + datetime.utcfromtimestamp(pattern[i]["dtime"]).strftime("%Y-%m-%d"), + pattern[i]["dprice"], + ) + + segment = (a_part, b_part, c_part, d_part) + + l_segment = list(segment) + while "" in l_segment: + l_segment.remove("") + segment = tuple(l_segment) + + l_segments.append(segment) + start_time = 999999999999 for i in pattern: if pattern[i]["atime"] < start_time: @@ -83,7 +124,7 @@ def plot_pattern_recognition(ticker: str, pattern: pd.DataFrame, segments: List) type="candle", volume=False, title=f"\n{ticker}", - alines=segments, + alines=l_segments, xrotation=10, style=s, figratio=(10, 7), @@ -94,6 +135,11 @@ def plot_pattern_recognition(ticker: str, pattern: pd.DataFrame, segments: List) ), ) + for ix in range(len(pattern.columns)): + print(f"From {l_segments[ix][0][0]} to {l_segments[ix][-1][0]}") + print(f"Pattern: {pattern[0]['patternname']} ({pattern[0]['patterntype']})") + print("") + def pattern_recognition_view(other_args: List[str], ticker: str): """Display pattern recognition signals on the data @@ -134,73 +180,7 @@ def pattern_recognition_view(other_args: List[str], ticker: str): print("No pattern identified in this data", "\n") return - l_segments = list() - for i in df_pattern: - a_part = "" - if "aprice" in df_pattern[i]: - if df_pattern[i]["aprice"] != 0 and not math.isnan( - df_pattern[i]["aprice"] - ): - a_part = ( - datetime.utcfromtimestamp(df_pattern[i]["atime"]).strftime( - "%Y-%m-%d" - ), - df_pattern[i]["aprice"], - ) - - b_part = "" - if "bprice" in df_pattern[i]: - if df_pattern[i]["bprice"] != 0 and not math.isnan( - df_pattern[i]["bprice"] - ): - b_part = ( - datetime.utcfromtimestamp(df_pattern[i]["btime"]).strftime( - "%Y-%m-%d" - ), - df_pattern[i]["bprice"], - ) - - c_part = "" - if "cprice" in df_pattern[i]: - if df_pattern[i]["cprice"] != 0 and not math.isnan( - df_pattern[i]["cprice"] - ): - c_part = ( - datetime.utcfromtimestamp(df_pattern[i]["ctime"]).strftime( - "%Y-%m-%d" - ), - df_pattern[i]["cprice"], - ) - - d_part = "" - if "dprice" in df_pattern[i]: - if df_pattern[i]["dprice"] != 0 and not math.isnan( - df_pattern[i]["dprice"] - ): - d_part = ( - datetime.utcfromtimestamp(df_pattern[i]["dtime"]).strftime( - "%Y-%m-%d" - ), - df_pattern[i]["dprice"], - ) - - segment = (a_part, b_part, c_part, d_part) - - l_segment = list(segment) - while "" in l_segment: - l_segment.remove("") - segment = tuple(l_segment) - - l_segments.append(segment) - - plot_pattern_recognition(ticker, df_pattern, l_segments) - - for ix in range(len(df_pattern.columns)): - print(f"From {l_segments[ix][0][0]} to {l_segments[ix][-1][0]}") - print( - f"Pattern: {df_pattern[0]['patternname']} ({df_pattern[0]['patterntype']})" - ) - print("") + plot_pattern_recognition(ticker, df_pattern) except Exception as e: print(e, "\n") From c4b87531ad74976cf6e76cec5ab849416600bfc3 Mon Sep 17 00:00:00 2001 From: didier Date: Sun, 2 May 2021 12:30:38 +0100 Subject: [PATCH 15/74] remove unused import --- gamestonk_terminal/technical_analysis/finhub_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gamestonk_terminal/technical_analysis/finhub_view.py b/gamestonk_terminal/technical_analysis/finhub_view.py index 9dd06f4d756..dc4aa570078 100644 --- a/gamestonk_terminal/technical_analysis/finhub_view.py +++ b/gamestonk_terminal/technical_analysis/finhub_view.py @@ -1,5 +1,5 @@ import argparse -from typing import List, Any +from typing import List import requests import yfinance as yf import mplfinance as mpf From 153a8ce9fd5e317975973a5922a9f8d474e3cbce Mon Sep 17 00:00:00 2001 From: didier Date: Sun, 2 May 2021 12:33:15 +0100 Subject: [PATCH 16/74] add finhub config key --- gamestonk_terminal/config_terminal.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gamestonk_terminal/config_terminal.py b/gamestonk_terminal/config_terminal.py index dd4092eb45d..a3405ed207e 100644 --- a/gamestonk_terminal/config_terminal.py +++ b/gamestonk_terminal/config_terminal.py @@ -33,7 +33,7 @@ API_TWITTER_SECRET_KEY = os.getenv("GT_API_TWITTER_SECRET_KEY") or "REPLACE_ME" API_TWITTER_BEARER_TOKEN = os.getenv("GT_API_TWITTER_BEARER_TOKEN") or "REPLACE_ME" # https://fred.stlouisfed.org/docs/api/api_key.html -API_FRED_KEY = os.getenv("GT_FRED_API_KEY") or "REPLACE_ME" +API_FRED_KEY = os.getenv("GT_FRED_API_KEY") or "b98e15f1fde39212bce9213f2e3e5afb" # https://newsapi.org API_NEWS_TOKEN = os.getenv("GT_API_NEWS_TOKEN") or "REPLACE_ME" @@ -59,3 +59,6 @@ PATH_TO_SELENIUM_DRIVER = None # Replace with "PATH" # https://coinmarketcap.com/api/ COINMARKETCAP_KEY = os.getenv("GT_CMC_API_KEY") or "REPLACE_ME" + +# https://finnhub.io +API_FINNHUB_KEY = os.getenv("GT_API_FINNHUB_KEY") or "REPLACE_ME" \ No newline at end of file From 9c32da41a981fd4d79e65a3c9f8cd0f396d3dffc Mon Sep 17 00:00:00 2001 From: didier Date: Sun, 2 May 2021 12:36:33 +0100 Subject: [PATCH 17/74] cmon black, we got this --- gamestonk_terminal/config_terminal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gamestonk_terminal/config_terminal.py b/gamestonk_terminal/config_terminal.py index a3405ed207e..3dfa270ae34 100644 --- a/gamestonk_terminal/config_terminal.py +++ b/gamestonk_terminal/config_terminal.py @@ -61,4 +61,4 @@ PATH_TO_SELENIUM_DRIVER = None # Replace with "PATH" COINMARKETCAP_KEY = os.getenv("GT_CMC_API_KEY") or "REPLACE_ME" # https://finnhub.io -API_FINNHUB_KEY = os.getenv("GT_API_FINNHUB_KEY") or "REPLACE_ME" \ No newline at end of file +API_FINNHUB_KEY = os.getenv("GT_API_FINNHUB_KEY") or "REPLACE_ME" From a3614fb3b7ea56921b6845514786a36d4260675b Mon Sep 17 00:00:00 2001 From: didier Date: Sun, 2 May 2021 12:41:34 +0100 Subject: [PATCH 18/74] fix incompatible type mypy --- .../technical_analysis/finhub_view.py | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/gamestonk_terminal/technical_analysis/finhub_view.py b/gamestonk_terminal/technical_analysis/finhub_view.py index dc4aa570078..6c700b7b12d 100644 --- a/gamestonk_terminal/technical_analysis/finhub_view.py +++ b/gamestonk_terminal/technical_analysis/finhub_view.py @@ -53,37 +53,49 @@ def plot_pattern_recognition(ticker: str, pattern: pd.DataFrame): l_segments = list() for i in pattern: - a_part = "" if "aprice" in pattern[i]: if pattern[i]["aprice"] != 0 and not math.isnan(pattern[i]["aprice"]): a_part = ( datetime.utcfromtimestamp(pattern[i]["atime"]).strftime("%Y-%m-%d"), pattern[i]["aprice"], ) + else: + a_part = "" + else: + a_part = "" - b_part = "" if "bprice" in pattern[i]: if pattern[i]["bprice"] != 0 and not math.isnan(pattern[i]["bprice"]): b_part = ( datetime.utcfromtimestamp(pattern[i]["btime"]).strftime("%Y-%m-%d"), pattern[i]["bprice"], ) + else: + b_part = "" + else: + b_part = "" - c_part = "" if "cprice" in pattern[i]: if pattern[i]["cprice"] != 0 and not math.isnan(pattern[i]["cprice"]): c_part = ( datetime.utcfromtimestamp(pattern[i]["ctime"]).strftime("%Y-%m-%d"), pattern[i]["cprice"], ) + else: + c_part = "" + else: + c_part = "" - d_part = "" if "dprice" in pattern[i]: if pattern[i]["dprice"] != 0 and not math.isnan(pattern[i]["dprice"]): d_part = ( datetime.utcfromtimestamp(pattern[i]["dtime"]).strftime("%Y-%m-%d"), pattern[i]["dprice"], ) + else: + d_part = "" + else: + d_part = "" segment = (a_part, b_part, c_part, d_part) From 8ff6e2903c5391ee61fd3882c0233507e5b8fba0 Mon Sep 17 00:00:00 2001 From: didier Date: Sun, 2 May 2021 12:50:47 +0100 Subject: [PATCH 19/74] can I breath now mypy? --- .../technical_analysis/finhub_view.py | 28 ++++++------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/gamestonk_terminal/technical_analysis/finhub_view.py b/gamestonk_terminal/technical_analysis/finhub_view.py index 6c700b7b12d..85c588aba76 100644 --- a/gamestonk_terminal/technical_analysis/finhub_view.py +++ b/gamestonk_terminal/technical_analysis/finhub_view.py @@ -53,58 +53,46 @@ def plot_pattern_recognition(ticker: str, pattern: pd.DataFrame): l_segments = list() for i in pattern: + a_part = ("", "") if "aprice" in pattern[i]: if pattern[i]["aprice"] != 0 and not math.isnan(pattern[i]["aprice"]): a_part = ( datetime.utcfromtimestamp(pattern[i]["atime"]).strftime("%Y-%m-%d"), pattern[i]["aprice"], ) - else: - a_part = "" - else: - a_part = "" + b_part = ("", "") if "bprice" in pattern[i]: if pattern[i]["bprice"] != 0 and not math.isnan(pattern[i]["bprice"]): b_part = ( datetime.utcfromtimestamp(pattern[i]["btime"]).strftime("%Y-%m-%d"), pattern[i]["bprice"], ) - else: - b_part = "" - else: - b_part = "" + c_part = ("", "") if "cprice" in pattern[i]: if pattern[i]["cprice"] != 0 and not math.isnan(pattern[i]["cprice"]): c_part = ( datetime.utcfromtimestamp(pattern[i]["ctime"]).strftime("%Y-%m-%d"), pattern[i]["cprice"], ) - else: - c_part = "" - else: - c_part = "" + d_part = ("", "") if "dprice" in pattern[i]: if pattern[i]["dprice"] != 0 and not math.isnan(pattern[i]["dprice"]): d_part = ( datetime.utcfromtimestamp(pattern[i]["dtime"]).strftime("%Y-%m-%d"), pattern[i]["dprice"], ) - else: - d_part = "" - else: - d_part = "" segment = (a_part, b_part, c_part, d_part) l_segment = list(segment) - while "" in l_segment: - l_segment.remove("") - segment = tuple(l_segment) + while ("", "") in l_segment: + l_segment.remove(("", "")) + segm = tuple(l_segment) - l_segments.append(segment) + l_segments.append(segm) start_time = 999999999999 for i in pattern: From a8b5ee8a210888fc47b458b00e0c45960ac4f872 Mon Sep 17 00:00:00 2001 From: didier Date: Sun, 2 May 2021 12:53:19 +0100 Subject: [PATCH 20/74] imports order fixed --- gamestonk_terminal/technical_analysis/finhub_view.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/gamestonk_terminal/technical_analysis/finhub_view.py b/gamestonk_terminal/technical_analysis/finhub_view.py index 85c588aba76..67db2b88951 100644 --- a/gamestonk_terminal/technical_analysis/finhub_view.py +++ b/gamestonk_terminal/technical_analysis/finhub_view.py @@ -1,18 +1,17 @@ import argparse from typing import List +import math +from datetime import datetime import requests import yfinance as yf import mplfinance as mpf import pandas as pd -import math from gamestonk_terminal import config_terminal as cfg from gamestonk_terminal.helper_funcs import ( parse_known_args_and_warn, plot_autoscale, ) -from datetime import datetime - def get_pattern_recognition(ticker: str, resolution: str) -> pd.DataFrame: """Get pattern recognition data From 20e3e0f86d6b2fad69f17593f095aecad10ce83e Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Sun, 2 May 2021 13:05:48 +0100 Subject: [PATCH 21/74] Update README.md --- gamestonk_terminal/technical_analysis/README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/gamestonk_terminal/technical_analysis/README.md b/gamestonk_terminal/technical_analysis/README.md index 0e5c511d850..8ee5d86bf73 100644 --- a/gamestonk_terminal/technical_analysis/README.md +++ b/gamestonk_terminal/technical_analysis/README.md @@ -8,6 +8,8 @@ This menu aims to perform a technical analysis on a pre-loaded ticker chart, and - technical summary report [FinBrain API] * [recom](#recom) - recommendation based on Technical Indicators [Tradingview API] + * [pr](#pr) + - pattern recognition [Finhub] [OVERLAP](#OVERLAP) * [ema](#ema) @@ -79,6 +81,19 @@ Print tradingview recommendation based on technical indicators. [Source: https:/ Captura de ecrรฃ 2021-03-31, ร s 00 14 41 +## pr +``` +usage: pr [-r {1,5,15,30,60,D,W,M}] +``` +Display pattern recognition signals on the data. [Source: https://finnhub.io] + +* -r : Plot resolution to look for pattern signals. Default 'D'. + +![pr](https://user-images.githubusercontent.com/25267873/116812520-06338700-ab47-11eb-8db4-8f5e940507ed.png) + +Captura de ecrรฃ 2021-05-02, ร s 02 27 10 + + # OVERLAP ## ema From 83aa19e8c2f21c50a6b8e3860c9e9314823b6150 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Sun, 2 May 2021 13:07:09 +0100 Subject: [PATCH 22/74] Update README.md --- gamestonk_terminal/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/gamestonk_terminal/README.md b/gamestonk_terminal/README.md index 5034728b90c..f1c79fdbaef 100644 --- a/gamestonk_terminal/README.md +++ b/gamestonk_terminal/README.md @@ -183,6 +183,7 @@ Command | Description | Sources `view` | view historical data and trendlines| [Finviz](https://finviz.com/quote.ashx?t=tsla) `summary` | technical summary report| [FinBrain](https://finbrain.tech) `recom` | recommendation based on Technical Indicators| [Tradingview](https://uk.tradingview.com/widget/technical-analysis/) +`pr` | pattern recognition| [Finhub](https://finnhub.io) [overlap](https://github.com/twopirllc/pandas-ta/tree/master/pandas_ta/overlap) | `ema` | exponential moving average | [Wikipedia](https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average), [Investopedia](https://www.investopedia.com/terms/e/ema.asp) `sma` |simple moving average | [Wikipedia](https://en.wikipedia.org/wiki/Moving_average#Simple_moving_average_(boxcar_filter)), [Investopedia](https://www.investopedia.com/terms/s/sma.asp) From 94da5e4199326c673ec5213253b37703b924300c Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Sun, 2 May 2021 13:08:56 +0100 Subject: [PATCH 23/74] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4e558e73cf5..55fbedbf1f2 100644 --- a/README.md +++ b/README.md @@ -227,10 +227,11 @@ These are the ones where a key is necessary: * Tradier: https://developer.tradier.com/getting_started * Oanda API: https://developer.oanda.com * CoinMarketCap API: https://coinmarketcap.com/api/ + * Finhub API: https://finnhub.io When these are obtained, don't forget to update [config_terminal.py](/gamestonk_terminal/config_terminal.py). -Alternatively, you can also set them to the following environment variables: GT_API_KEY_ALPHAVANTAGE, GT_API_KEY_FINANCIALMODELINGPREP, GT_API_KEY_QUANDL, GT_API_REDDIT_CLIENT_ID, GT_API_REDDIT_CLIENT_SECRET, GT_API_REDDIT_USERNAME, GT_API_REDDIT_USER_AGENT, GT_API_REDDIT_PASSWORD, GT_API_TWITTER_KEY, GT_API_TWITTER_SECRET_KEY, GT_API_TWITTER_BEARER_TOKEN, GT_API_POLYGON_KEY, GT_FRED_API_KEY, GT_API_NEWS_TOKEN, GT_OANDA_TOKEN, GT_OANDA_ACCOUNT, GT_TRADIER_TOKEN, GT_CMC_API_KEY. +Alternatively, you can also set them to the following environment variables: GT_API_KEY_ALPHAVANTAGE, GT_API_KEY_FINANCIALMODELINGPREP, GT_API_KEY_QUANDL, GT_API_REDDIT_CLIENT_ID, GT_API_REDDIT_CLIENT_SECRET, GT_API_REDDIT_USERNAME, GT_API_REDDIT_USER_AGENT, GT_API_REDDIT_PASSWORD, GT_API_TWITTER_KEY, GT_API_TWITTER_SECRET_KEY, GT_API_TWITTER_BEARER_TOKEN, GT_API_POLYGON_KEY, GT_FRED_API_KEY, GT_API_NEWS_TOKEN, GT_OANDA_TOKEN, GT_OANDA_ACCOUNT, GT_TRADIER_TOKEN, GT_CMC_API_KEY, GT_CMC_API_KEY, API_FINNHUB_KEY. Example: ``` From e1151640f58756713eca01368dbdb539d1eadae4 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Sun, 2 May 2021 13:09:49 +0100 Subject: [PATCH 24/74] Update ROADMAP.md --- ROADMAP.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ROADMAP.md b/ROADMAP.md index f872efde669..37c69ea2cb1 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -78,9 +78,10 @@ * [x] Add technical summary report provided by FinBrain (@didier) - [PR #294](https://github.com/DidierRLopes/GamestonkTerminal/pull/294) * [x] Add recommendation based on technical indicators from Tradingview (@didier) - [PR #301](https://github.com/DidierRLopes/GamestonkTerminal/pull/301) * [x] Add view of stock historical price with trendlines (support, resistance) by Finviz (@didier) - [PR #317](https://github.com/DidierRLopes/GamestonkTerminal/pull/317) +* [x] Add auto-recognition of major TA patterns (@didier) - [PR #429](https://github.com/DidierRLopes/GamestonkTerminal/pull/429) **NEXT** -* [ ] Add auto-recognition of major TA patterns (@didier) + --- From 5ddb322ff2a771287fc9014e301c3f261616f013 Mon Sep 17 00:00:00 2001 From: didier Date: Sun, 2 May 2021 19:32:43 +0100 Subject: [PATCH 25/74] Economic calendar event impact --- gamestonk_terminal/config_terminal.py | 3 + gamestonk_terminal/econ/econ_controller.py | 7 ++ gamestonk_terminal/econ/finnhub_view.py | 136 +++++++++++++++++++++ gamestonk_terminal/main_helper.py | 2 +- 4 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 gamestonk_terminal/econ/finnhub_view.py diff --git a/gamestonk_terminal/config_terminal.py b/gamestonk_terminal/config_terminal.py index dd4092eb45d..f92788f94d5 100644 --- a/gamestonk_terminal/config_terminal.py +++ b/gamestonk_terminal/config_terminal.py @@ -59,3 +59,6 @@ PATH_TO_SELENIUM_DRIVER = None # Replace with "PATH" # https://coinmarketcap.com/api/ COINMARKETCAP_KEY = os.getenv("GT_CMC_API_KEY") or "REPLACE_ME" + +# https://finnhub.io +API_FINNHUB_KEY = os.getenv("GT_API_FINNHUB_KEY") or "REPLACE_ME" \ No newline at end of file diff --git a/gamestonk_terminal/econ/econ_controller.py b/gamestonk_terminal/econ/econ_controller.py index 2bdaf1e8727..9ec37eec7c7 100644 --- a/gamestonk_terminal/econ/econ_controller.py +++ b/gamestonk_terminal/econ/econ_controller.py @@ -9,6 +9,7 @@ from gamestonk_terminal import feature_flags as gtff from gamestonk_terminal.helper_funcs import get_flair from gamestonk_terminal.menu import session from gamestonk_terminal.econ import fred_view +from gamestonk_terminal.econ import finnhub_view class EconController: @@ -19,6 +20,7 @@ class EconController: "help", "q", "quit", + "events", "fred", "vixcls", "gdp", @@ -50,6 +52,7 @@ class EconController: print(" q quit this menu, and shows back to main menu") print(" quit quit to abandon program") print(" ") + print(" events economic events [Finnhub]") print( " fred display customized FRED data from https://fred.stlouisfed.org" ) @@ -95,6 +98,10 @@ class EconController: """Process Quit command - quit the program""" return True + def call_events(self, other_args: List[str]): + """Process events command""" + finnhub_view.economy_calendar_events(other_args) + def call_fred(self, other_args: List[str]): """Process fred command""" fred_view.display_fred(other_args, "") diff --git a/gamestonk_terminal/econ/finnhub_view.py b/gamestonk_terminal/econ/finnhub_view.py new file mode 100644 index 00000000000..8ec8ccdcef7 --- /dev/null +++ b/gamestonk_terminal/econ/finnhub_view.py @@ -0,0 +1,136 @@ +import argparse +from typing import List +import math +from datetime import datetime +import requests +import yfinance as yf +import mplfinance as mpf +import pandas as pd +from gamestonk_terminal import config_terminal as cfg +from gamestonk_terminal.helper_funcs import ( + parse_known_args_and_warn, + plot_autoscale, + check_positive, +) + + +def get_economy_calendar_events() -> pd.DataFrame: + """Get economic calendar events + + Returns + ------- + pd.DataFrame + Get dataframe with economic calendar events + """ + response = requests.get( + f"https://finnhub.io/api/v1/calendar/economic?token={cfg.API_FINNHUB_KEY}" + ) + if response.status_code == 200: + d_data = response.json() + if "economicCalendar" in d_data: + return pd.DataFrame(d_data["economicCalendar"]) + + return pd.DataFrame() + + +def economy_calendar_events(other_args: List[str]): + """Output economy calendar events + + Parameters + ---------- + other_args : List[str] + Command line arguments to be processed with argparse + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="events", + description=""" + Output economy calendar events. [Source: https://finnhub.io] + """, + ) + parser.add_argument( + "-c", + "--country", + action="store", + dest="country", + type=str, + default="US", + choices=["NZ", "AU", "ERL", "CA", "EU", "US", "JP", "CN", "GB", "CH"], + help="Country from where to get economic calendar events", + ) + parser.add_argument( + "-n", + "--num", + action="store", + dest="num", + type=check_positive, + default=10, + help="Number economic calendar events to display", + ) + parser.add_argument( + "-i", + "--impact", + action="store", + dest="impact", + type=str, + default="all", + choices=["low", "medium", "high", "all"], + help="Country from where to get economic calendar events", + ) + + try: + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-c") + + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + df_events = get_economy_calendar_events() + + if df_events.empty: + print("No latest economy calendar events found\n") + return + + df_econ_calendar = df_events[ + df_events["country"] == ns_parser.country + ].sort_values("time", ascending=False) + + if df_econ_calendar.empty: + print("No latet economy calendar events found in the specified country\n") + return + + if ns_parser.impact != "all": + df_econ_calendar = df_econ_calendar[ + df_econ_calendar["impact"] == ns_parser.impact + ] + + if df_econ_calendar.empty: + print( + "No latet economy calendar events found in the specified country with this impact\n" + ) + return + + df_econ_calendar = df_econ_calendar.fillna("---").head(n=ns_parser.num) + + d_econ_calendar_map = { + "actual": "Actual release", + "prev": "Previous release", + "country": "Country", + "unit": "Unit", + "estimate": "Estimate", + "event": "Event", + "impact": "Impact Level", + "time": "Release time", + } + + df_econ_calendar = df_econ_calendar[ + ["time", "event", "impact", "prev", "estimate", "actual", "unit"] + ].rename(columns=d_econ_calendar_map) + + print(df_econ_calendar.to_string(index=False)) + print("") + + except Exception as e: + print(e, "\n") diff --git a/gamestonk_terminal/main_helper.py b/gamestonk_terminal/main_helper.py index 824e14918b2..5723f7b3460 100644 --- a/gamestonk_terminal/main_helper.py +++ b/gamestonk_terminal/main_helper.py @@ -55,7 +55,7 @@ def print_help(s_ticker, s_start, s_interval, b_is_market_open): " > scr screener stocks, \t\t e.g. overview/performance, using preset filters" ) print(" > mill papermill menu, \t\t menu to generate notebook reports") - print(" > econ economic data, \t\t from: FRED, VIX") + print(" > econ economic data, \t\t e.g.: FRED, events") print( " > pa portfolio analysis, \t\t supports: robinhood, alpaca, ally " ) From 4132a1e4681b683d03b8fadf077d0cd9a38fe155 Mon Sep 17 00:00:00 2001 From: didier Date: Sun, 2 May 2021 19:34:28 +0100 Subject: [PATCH 26/74] improve description --- gamestonk_terminal/econ/econ_controller.py | 2 +- gamestonk_terminal/econ/finnhub_view.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/gamestonk_terminal/econ/econ_controller.py b/gamestonk_terminal/econ/econ_controller.py index 9ec37eec7c7..1b47f16d6b1 100644 --- a/gamestonk_terminal/econ/econ_controller.py +++ b/gamestonk_terminal/econ/econ_controller.py @@ -52,7 +52,7 @@ class EconController: print(" q quit this menu, and shows back to main menu") print(" quit quit to abandon program") print(" ") - print(" events economic events [Finnhub]") + print(" events economic impact events [Finnhub]") print( " fred display customized FRED data from https://fred.stlouisfed.org" ) diff --git a/gamestonk_terminal/econ/finnhub_view.py b/gamestonk_terminal/econ/finnhub_view.py index 8ec8ccdcef7..ca86eb448e9 100644 --- a/gamestonk_terminal/econ/finnhub_view.py +++ b/gamestonk_terminal/econ/finnhub_view.py @@ -34,7 +34,7 @@ def get_economy_calendar_events() -> pd.DataFrame: def economy_calendar_events(other_args: List[str]): - """Output economy calendar events + """Output economy calendar impact events Parameters ---------- @@ -45,7 +45,7 @@ def economy_calendar_events(other_args: List[str]): add_help=False, prog="events", description=""" - Output economy calendar events. [Source: https://finnhub.io] + Output economy impact calendar impact events. [Source: https://finnhub.io] """, ) parser.add_argument( @@ -56,7 +56,7 @@ def economy_calendar_events(other_args: List[str]): type=str, default="US", choices=["NZ", "AU", "ERL", "CA", "EU", "US", "JP", "CN", "GB", "CH"], - help="Country from where to get economic calendar events", + help="Country from where to get economy calendar impact events", ) parser.add_argument( "-n", @@ -65,7 +65,7 @@ def economy_calendar_events(other_args: List[str]): dest="num", type=check_positive, default=10, - help="Number economic calendar events to display", + help="Number economy calendar impact events to display", ) parser.add_argument( "-i", @@ -75,7 +75,7 @@ def economy_calendar_events(other_args: List[str]): type=str, default="all", choices=["low", "medium", "high", "all"], - help="Country from where to get economic calendar events", + help="Impact of the economy event", ) try: From 8ccc9c1a9c326504eb6921423cf60bcace68ca69 Mon Sep 17 00:00:00 2001 From: didier Date: Sun, 2 May 2021 19:40:20 +0100 Subject: [PATCH 27/74] fix black --- gamestonk_terminal/config_terminal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gamestonk_terminal/config_terminal.py b/gamestonk_terminal/config_terminal.py index f92788f94d5..e305115bf48 100644 --- a/gamestonk_terminal/config_terminal.py +++ b/gamestonk_terminal/config_terminal.py @@ -61,4 +61,4 @@ PATH_TO_SELENIUM_DRIVER = None # Replace with "PATH" COINMARKETCAP_KEY = os.getenv("GT_CMC_API_KEY") or "REPLACE_ME" # https://finnhub.io -API_FINNHUB_KEY = os.getenv("GT_API_FINNHUB_KEY") or "REPLACE_ME" \ No newline at end of file +API_FINNHUB_KEY = os.getenv("GT_API_FINNHUB_KEY") or "REPLACE_ME" From 999d6818db9deab6f8b54f8ae4d52c7b50aa1dbe Mon Sep 17 00:00:00 2001 From: didier Date: Sun, 2 May 2021 19:46:50 +0100 Subject: [PATCH 28/74] flake8 remove unused imports --- gamestonk_terminal/econ/finnhub_view.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/gamestonk_terminal/econ/finnhub_view.py b/gamestonk_terminal/econ/finnhub_view.py index ca86eb448e9..2f4d45d6c32 100644 --- a/gamestonk_terminal/econ/finnhub_view.py +++ b/gamestonk_terminal/econ/finnhub_view.py @@ -1,15 +1,10 @@ import argparse from typing import List -import math -from datetime import datetime import requests -import yfinance as yf -import mplfinance as mpf import pandas as pd from gamestonk_terminal import config_terminal as cfg from gamestonk_terminal.helper_funcs import ( parse_known_args_and_warn, - plot_autoscale, check_positive, ) From 32f13f8e80099c1bad233c4b6d678c76a40682ec Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Sun, 2 May 2021 20:01:21 +0100 Subject: [PATCH 29/74] Update README.md --- gamestonk_terminal/econ/README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/gamestonk_terminal/econ/README.md b/gamestonk_terminal/econ/README.md index 1e6bb26f4b9..8bd5054abed 100644 --- a/gamestonk_terminal/econ/README.md +++ b/gamestonk_terminal/econ/README.md @@ -1,7 +1,9 @@ -# ECONONMY +# ECONOMY This menu aims to assess economy data, and the usage of the following commands along with an example will be exploited below. +* [events](#events) + * economic impact events [Finnhub] * [fred](#fred) * display customized FRED from https://fred.stlouisfed.org * [vixcls](#vixcls) @@ -28,6 +30,20 @@ This menu aims to assess economy data, and the usage of the following commands a * Canada / U.S. Foreign Exchange Rate (CAD per 1 USD) +## events +```text +usage: events [-c {NZ,AU,ERL,CA,EU,US,JP,CN,GB,CH}] [-i {low,medium,high,all}] [-n NUM] +``` + +Output economy impact calendar impact events. [Source: https://finnhub.io] + +* -c : Country from where to get economy calendar impact events, between NZ,AU,ERL,CA,EU,US,JP,CN,GB,CH. Default: US. +* -i : Impact of the economy event, between low,medium,high,all. Default: all. +* -n : Number economy calendar impact events to display. Default: 10. + +Captura de ecrรฃ 2021-05-02, ร s 19 51 47 + + ## fred ```text usage: fred [-i SERIES_ID] [-s START_DATE] [-t] From d4fc5bb87211fd4195a3e739ed117945aec0e9bd Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Sun, 2 May 2021 20:05:15 +0100 Subject: [PATCH 30/74] Update README.md --- gamestonk_terminal/README.md | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/gamestonk_terminal/README.md b/gamestonk_terminal/README.md index 5034728b90c..df483d6ec66 100644 --- a/gamestonk_terminal/README.md +++ b/gamestonk_terminal/README.md @@ -328,19 +328,21 @@ Command|Description|Source   ## Economy [ยป](econ/README.md) -Command|Description ------- | -------- -`vixcls` | Volatility Index -`gdp` | Gross Domestic Product -`unrate` | Unemployment Rate -`dgs1` | 1-Year Treasury Constant Maturity Rate -`dgs5` | 5-Year Treasury Constant Maturity Rate -`dgs10` | 10-Year Treasury Constant Maturity Rate -`dgs30` | 30-Year Treasury Constant Maturity Rate -`mortgage30us` | 30-Year Fixed Rate Mortgage Average -`fedfunds` | Effective Federal Funds Rate -`aaa` | Moody's Seasoned AAA Corporate Bond Yield -`dexcaus` | Canada / U.S. Foreign Exchange Rate (CAD per 1 USD) +Command|Description|Source +------ | -------- | -------- +`events` | economic impact events | https://finnhub.io +`fred` | display customized FRED data | https://fred.stlouisfed.org +`vixcls` | Volatility Index | https://fred.stlouisfed.org +`gdp` | Gross Domestic Product | https://fred.stlouisfed.org +`unrate` | Unemployment Rate | https://fred.stlouisfed.org +`dgs1` | 1-Year Treasury Constant Maturity Rate | https://fred.stlouisfed.org +`dgs5` | 5-Year Treasury Constant Maturity Rate | https://fred.stlouisfed.org +`dgs10` | 10-Year Treasury Constant Maturity Rate | https://fred.stlouisfed.org +`dgs30` | 30-Year Treasury Constant Maturity Rate | https://fred.stlouisfed.org +`mortgage30us` | 30-Year Fixed Rate Mortgage Average | https://fred.stlouisfed.org +`fedfunds` | Effective Federal Funds Rate | https://fred.stlouisfed.org +`aaa` | Moody's Seasoned AAA Corporate Bond Yield | https://fred.stlouisfed.org +`dexcaus` | Canada / U.S. Foreign Exchange Rate (CAD per 1 USD) | https://fred.stlouisfed.org   From abd43a9d439014bb7940f10253261d52bb9a9b15 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Sun, 2 May 2021 20:06:35 +0100 Subject: [PATCH 31/74] Update ROADMAP.md --- ROADMAP.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ROADMAP.md b/ROADMAP.md index f872efde669..0b59abfba5b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -171,6 +171,7 @@ ___ ## Economy * [x] Implement Economic data (gpd, unemployment rate, ...) (@jmaslek) - [PR #167](https://github.com/DidierRLopes/GamestonkTerminal/pull/167) * [x] Refactor FRED to ECON menu and add VIX view (@jmaslek) - [PR #405](https://github.com/DidierRLopes/GamestonkTerminal/pull/405) +* [x] Output economic impact events calendar (@didier) - [PR #430](https://github.com/DidierRLopes/GamestonkTerminal/pull/430) **NEXT** From ae64ad88fa440e0b9711a78a732dadc9f7fb1000 Mon Sep 17 00:00:00 2001 From: Jamie Date: Sun, 2 May 2021 15:45:21 -0400 Subject: [PATCH 32/74] change key names and readme format --- README.md | 8 ++++---- ROADMAP.md | 2 +- gamestonk_terminal/config_terminal.py | 10 +++++----- gamestonk_terminal/cryptocurrency/binance_model.py | 12 ++++++------ .../cryptocurrency/coinmarketcap_view.py | 2 +- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index f692ff61ab4..63eb4f5379a 100644 --- a/README.md +++ b/README.md @@ -231,10 +231,10 @@ These are the ones where a key is necessary: When these are obtained, don't forget to update [config_terminal.py](/gamestonk_terminal/config_terminal.py). -Alternatively, you can also set them to the following environment variables: GT_API_KEY_ALPHAVANTAGE, GT_API_KEY_FINANCIALMODELINGPREP, -GT_API_KEY_QUANDL, GT_API_REDDIT_CLIENT_ID, GT_API_REDDIT_CLIENT_SECRET, GT_API_REDDIT_USERNAME, GT_API_REDDIT_USER_AGENT, GT_API_REDDIT_PASSWORD, -GT_API_TWITTER_KEY, GT_API_TWITTER_SECRET_KEY, GT_API_TWITTER_BEARER_TOKEN, GT_API_POLYGON_KEY, GT_FRED_API_KEY, -GT_API_NEWS_TOKEN, GT_OANDA_TOKEN, GT_OANDA_ACCOUNT, GT_TRADIER_TOKEN, GT_CMC_API_KEY, GT_BINANCE_API_KEY, GT_BINANCE_SECRET_KEY. +Alternatively, you can also set them to the following environment variables: GT_API_KEY_ALPHAVANTAGE, GT_API_KEY_FINANCIALMODELINGPREP, GT_API_KEY_QUANDL, GT_API_REDDIT_CLIENT_ID, GT_API_REDDIT_CLIENT_SECRET, GT_API_REDDIT_USERNAME, GT_API_REDDIT_USER_AGENT, GT_API_REDDIT_PASSWORD, GT_API_TWITTER_KEY, GT_API_TWITTER_SECRET_KEY, GT_API_TWITTER_BEARER_TOKEN, GT_API_POLYGON_KEY, GT_API_FRED_KEY, GT_API_NEWS_TOKEN, GT_API_TRADIER_TOKEN, GT_API_CMC_KEY, GT_API_BINANCE_KEY, GT_API_BINANCE_SECRET. + +The following environment variables are used for trading accounts: +GT_OANDA_TOKEN, GT_OANDA_ACCOUNT, GT_RH_USERNAME, GT_RH_PASSWORD Example: ``` diff --git a/ROADMAP.md b/ROADMAP.md index fa0af690d63..6853bfe803c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -197,11 +197,11 @@ ___ ## Cryptocurrencies * [x] Add Coingecko (@jmaslek) - [PR #283](#https://github.com/DidierRLopes/GamestonkTerminal/pull/283) * [x] view top coins from coinmarketcap (@jmaslek) - [PR #378](https://github.com/DidierRLopes/GamestonkTerminal/pull/378) +* [x] Add binance (@jmaslek) [PR #428](#https://github.com/DidierRLopes/GamestonkTerminal/pull/428) **NEXT** * [ ] Add [Coinpaprika](https://coinpaprika.com/api/) * [ ] Add crypto charts -* [x] Add binance (@jmaslek) * [ ] Add top altcoin lists --- diff --git a/gamestonk_terminal/config_terminal.py b/gamestonk_terminal/config_terminal.py index 77a2417965b..6a8350e9dc9 100644 --- a/gamestonk_terminal/config_terminal.py +++ b/gamestonk_terminal/config_terminal.py @@ -33,7 +33,7 @@ API_TWITTER_SECRET_KEY = os.getenv("GT_API_TWITTER_SECRET_KEY") or "REPLACE_ME" API_TWITTER_BEARER_TOKEN = os.getenv("GT_API_TWITTER_BEARER_TOKEN") or "REPLACE_ME" # https://fred.stlouisfed.org/docs/api/api_key.html -API_FRED_KEY = os.getenv("GT_FRED_API_KEY") or "REPLACE_ME" +API_FRED_KEY = os.getenv("GT_API_FRED_KEY") or "REPLACE_ME" # https://newsapi.org API_NEWS_TOKEN = os.getenv("GT_API_NEWS_TOKEN") or "REPLACE_ME" @@ -51,15 +51,15 @@ 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_TRADIER_TOKEN") or "REPLACE_ME" +TRADIER_TOKEN = os.getenv("GT_API_TRADIER_TOKEN") or "REPLACE_ME" # Selenium Webbrowser drivers can be found at https://selenium-python.readthedocs.io/installation.html WEBDRIVER_TO_USE = "chrome" PATH_TO_SELENIUM_DRIVER = None # Replace with "PATH" # https://coinmarketcap.com/api/ -COINMARKETCAP_KEY = os.getenv("GT_CMC_API_KEY") or "REPLACE_ME" +API_CMC_KEY = os.getenv("GT_API_CMC_KEY") or "REPLACE_ME" # https://www.binance.com/en/ -BINANCE_API_KEY = os.getenv("GT_BINANCE_API_KEY") or "REPLACE_ME" -BINANCE_SECRET_KEY = os.getenv("GT_BINANCE_SECRET_KEY") or "REPLACE_ME" +API_BINANCE_KEY = os.getenv("GT_API_BINANCE_KEY") or "REPLACE_ME" +API_BINANCE_SECRET = os.getenv("GT_API_BINANCE_SECRET") or "REPLACE_ME" diff --git a/gamestonk_terminal/cryptocurrency/binance_model.py b/gamestonk_terminal/cryptocurrency/binance_model.py index dfa1ff049e0..033a2b6499d 100644 --- a/gamestonk_terminal/cryptocurrency/binance_model.py +++ b/gamestonk_terminal/cryptocurrency/binance_model.py @@ -8,13 +8,13 @@ import numpy as np from binance.client import Client from binance.exceptions import BinanceAPIException from gamestonk_terminal.main_helper import parse_known_args_and_warn -from gamestonk_terminal.config_terminal import BINANCE_API_KEY, BINANCE_SECRET_KEY +from gamestonk_terminal.config_terminal import API_BINANCE_KEY, API_BINANCE_SECRET from gamestonk_terminal.cryptocurrency.binance_view import plot_order_book, plot_candles -def check_valid_binance_str(symbol: str): +def check_valid_binance_str(symbol: str) -> str : """Check if symbol is in defined binance""" - client = Client(BINANCE_API_KEY, BINANCE_SECRET_KEY) + client = Client(API_BINANCE_KEY, API_BINANCE_SECRET) try: client.get_avg_price(symbol=symbol.upper()) return symbol.upper() @@ -106,7 +106,7 @@ def order_book(coin: str, other_args: List[str]): ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return - client = Client(BINANCE_API_KEY, BINANCE_SECRET_KEY) + client = Client(API_BINANCE_KEY, API_BINANCE_SECRET) market_book = client.get_order_book(symbol=coin, limit=ns_parser.limit) bids = np.asarray(market_book["bids"], dtype=float) asks = np.asarray(market_book["asks"], dtype=float) @@ -133,7 +133,7 @@ def show_candles(coin: str, other_args: List[str]): ------- """ - client = Client(BINANCE_API_KEY, BINANCE_SECRET_KEY) + client = Client(API_BINANCE_KEY, API_BINANCE_SECRET) interval_map = { "1day": client.KLINE_INTERVAL_1DAY, @@ -235,7 +235,7 @@ def balance(coin: str): ------- """ - client = Client(BINANCE_API_KEY, BINANCE_SECRET_KEY) + client = Client(API_BINANCE_KEY, API_BINANCE_SECRET) try: current_balance = client.get_asset_balance(asset=coin) if current_balance is None: diff --git a/gamestonk_terminal/cryptocurrency/coinmarketcap_view.py b/gamestonk_terminal/cryptocurrency/coinmarketcap_view.py index a8820889d33..310cba96378 100644 --- a/gamestonk_terminal/cryptocurrency/coinmarketcap_view.py +++ b/gamestonk_terminal/cryptocurrency/coinmarketcap_view.py @@ -6,7 +6,7 @@ from typing import List import pandas as pd from tabulate import tabulate from coinmarketcapapi import CoinMarketCapAPI -from gamestonk_terminal.config_terminal import COINMARKETCAP_KEY +from gamestonk_terminal.config_terminal import API_CMC_KEY from gamestonk_terminal.helper_funcs import parse_known_args_and_warn sort_options = ["Symbol", "CMC_Rank", "LastPrice", "DayPctChange", "MarketCap"] From 7f7cec8451c5a424e9480ab00e0eb3e313d606ca Mon Sep 17 00:00:00 2001 From: Jamie Date: Sun, 2 May 2021 16:03:15 -0400 Subject: [PATCH 33/74] formatting fixes round 1 --- gamestonk_terminal/cryptocurrency/README.md | 12 ++++----- .../cryptocurrency/binance_model.py | 23 ++++++++-------- .../cryptocurrency/crypto_controller.py | 27 +++++++++---------- 3 files changed, 30 insertions(+), 32 deletions(-) diff --git a/gamestonk_terminal/cryptocurrency/README.md b/gamestonk_terminal/cryptocurrency/README.md index 882e2756a51..12dfbf737b4 100644 --- a/gamestonk_terminal/cryptocurrency/README.md +++ b/gamestonk_terminal/cryptocurrency/README.md @@ -10,8 +10,8 @@ This menu aims to explore crypto world, and the usage of the following commands * show the top 7 coins from coingecko * [top](#top) * view top coins from coinmarketcap [coinmarketcap.com] -* [add](#add) - * add a coin/currency pair for Binance +* [select](#select) + * select a coin/currency pair for Binance * [book](#book) * get and show market (order) book @@ -68,17 +68,17 @@ This command displays the top n cryptocurrencies from coinmarketcap.com. crypto -## add -Add a coin/currency to the current object. Note that in binance, the exchange "ticker" is usually COINCURR, such as BTCEUR +## select +Select a coin/currency to the current object. Note that in binance, the exchange "ticker" is usually COINCURR, such as BTCEUR for BTC to EURO. There is no USD, but it uses a coin tethered to the USD (USDT), which is the default. Some symbols are a combination of coins, as `ETHBTC` is a valid symbol ```` -usage: add [-c --coin] [-q --quote] +usage: select [-c --coin] [-q --quote] ```` * -c/--coin Coin to load. If not specified, BTC will be loaded to prevent errors later. * -q/--quote Quote currency. Defaults to `USDT` (which is 1-to-1 with USD) -Note that the usage `load btc` will also work, as it will add `-c` if not detected. +Note that the usage `select btc` will also work, as it will add `-c` if not detected. ## book Gets and shows the order book for the given coin/currency. Shows the cumulative amount of orders. diff --git a/gamestonk_terminal/cryptocurrency/binance_model.py b/gamestonk_terminal/cryptocurrency/binance_model.py index 033a2b6499d..29f482e1ca6 100644 --- a/gamestonk_terminal/cryptocurrency/binance_model.py +++ b/gamestonk_terminal/cryptocurrency/binance_model.py @@ -25,11 +25,10 @@ def check_valid_binance_str(symbol: str) -> str : # pylint: disable=inconsistent-return-statements - - -def add_binance_coin(other_args: List[str]): +def select_binance_coin(other_args: List[str]): """ Define current_coin from binance + Parameters ---------- other_args: List[str] @@ -41,7 +40,7 @@ def add_binance_coin(other_args: List[str]): Coin that is defined on binance """ parser = argparse.ArgumentParser( - prog="add_coin", + prog="select", add_help=False, description="Define the coin to be used from binance", ) @@ -66,17 +65,18 @@ def add_binance_coin(other_args: List[str]): coin = ns_parser.coin + ns_parser.quote if check_valid_binance_str(coin): + print(f"{ns_parser.coin.upper()} loaded vs {ns_parser.quote.upper()}") return ns_parser.coin.upper(), ns_parser.quote.upper() except Exception as e: - print(e) - print("") + print(e, "\n") return None, None def order_book(coin: str, other_args: List[str]): """ Get order book for currency + Parameters ---------- coin: str @@ -115,13 +115,13 @@ def order_book(coin: str, other_args: List[str]): plot_order_book(bids, asks, coin) except Exception as e: - print(e) - print("") + print(e, "\n") def show_candles(coin: str, other_args: List[str]): """ Get klines/candles for coin + Parameters ---------- coin: str @@ -219,13 +219,13 @@ def show_candles(coin: str, other_args: List[str]): ) except Exception as e: - print(e) - print("") + print(e, "\n") def balance(coin: str): """ Get account holdings for asset + Parameters ---------- coin: str @@ -248,6 +248,5 @@ def balance(coin: str): print("") return except Exception as e: - print(e) - print("") + print(e, "\n") return diff --git a/gamestonk_terminal/cryptocurrency/crypto_controller.py b/gamestonk_terminal/cryptocurrency/crypto_controller.py index c58da931f3f..7789b0b170a 100644 --- a/gamestonk_terminal/cryptocurrency/crypto_controller.py +++ b/gamestonk_terminal/cryptocurrency/crypto_controller.py @@ -29,7 +29,7 @@ class CryptoController: "trades", "candle", "balance", - "add", + "select", ] def __init__(self): @@ -59,7 +59,7 @@ class CryptoController: print(" top view top coins from coinmarketcap") print("") print("Binance:") - print(" add add coin as current") + print(" select select coin/currency to use") print(" book show order book") print(" candle get klines/candles and plot") print(" balance show coin balance") @@ -94,11 +94,11 @@ class CryptoController: return True def call_load(self, other_args): - """Load command - get df from gecko""" + """Process load command""" self.current_coin, self.current_df = pycoingecko_view.load(other_args) def call_view(self, other_args): - """View command - plot loaded coin""" + """Process view command""" if self.current_coin: pycoingecko_view.view(self.current_coin, self.current_df, other_args) @@ -107,39 +107,38 @@ class CryptoController: print("") def call_trend(self, _): - """Trend command - show top 7 gecko coins""" + """Process trend command""" pycoingecko_view.trend() def call_top(self, other_args): - """Top command - get top n coins from coinmarketcap""" + """Process top command""" cmc_view.get_cmc_top_n(other_args) def call_book(self, other_args): - """Book command - get order book from binance""" + """Process book command""" binance_model.order_book(self.current_coin + self.current_currency, other_args) def call_candle(self, other_args): - """Candle command - show candle chart from binance""" + """Process candle command""" binance_model.show_candles( self.current_coin + self.current_currency, other_args ) def call_balance(self, _): - """Balance command - check current holdings of coin in binance""" + """Process balance command""" binance_model.balance(self.current_coin) - def call_add(self, other_args): - """Add command - set current coin for binance operations""" - self.current_coin, self.current_currency = binance_model.add_binance_coin( + def call_select(self, other_args): + """Process select command""" + self.current_coin, self.current_currency = binance_model.select_binance_coin( other_args ) - print(f"{self.current_coin} loaded vs {self.current_currency}") print("") def menu(): crypto_controller = CryptoController() - crypto_controller.print_help(crypto_controller) + crypto_controller.print_help(None) plt.close("all") while True: # Get input command from user From 162a611973c3dced7edd2e69c8e4b97adb5d75ae Mon Sep 17 00:00:00 2001 From: didier Date: Sun, 2 May 2021 22:17:03 +0100 Subject: [PATCH 34/74] fix finnhub naming --- gamestonk_terminal/README.md | 4 ++-- gamestonk_terminal/technical_analysis/README.md | 2 +- .../technical_analysis/{finhub_view.py => finnhub_view.py} | 0 gamestonk_terminal/technical_analysis/ta_controller.py | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) rename gamestonk_terminal/technical_analysis/{finhub_view.py => finnhub_view.py} (100%) diff --git a/gamestonk_terminal/README.md b/gamestonk_terminal/README.md index f1c79fdbaef..e314b341ba2 100644 --- a/gamestonk_terminal/README.md +++ b/gamestonk_terminal/README.md @@ -183,7 +183,7 @@ Command | Description | Sources `view` | view historical data and trendlines| [Finviz](https://finviz.com/quote.ashx?t=tsla) `summary` | technical summary report| [FinBrain](https://finbrain.tech) `recom` | recommendation based on Technical Indicators| [Tradingview](https://uk.tradingview.com/widget/technical-analysis/) -`pr` | pattern recognition| [Finhub](https://finnhub.io) +`pr` | pattern recognition| [Finnhub](https://finnhub.io) [overlap](https://github.com/twopirllc/pandas-ta/tree/master/pandas_ta/overlap) | `ema` | exponential moving average | [Wikipedia](https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average), [Investopedia](https://www.investopedia.com/terms/e/ema.asp) `sma` |simple moving average | [Wikipedia](https://en.wikipedia.org/wiki/Moving_average#Simple_moving_average_(boxcar_filter)), [Investopedia](https://www.investopedia.com/terms/s/sma.asp) @@ -398,7 +398,7 @@ positionbook |display the positionbook if Oanda provides one for the forex instr Command|Description ------ | -------- `ema` | buy when price exceeds EMA(l) -`ema_cross` | buy when EMA(short) > EMA(long) +`ema_cross` | buy when EMA(short) > EMA(long) `rsi` | buy when RSI < low and sell when RSI > high diff --git a/gamestonk_terminal/technical_analysis/README.md b/gamestonk_terminal/technical_analysis/README.md index 8ee5d86bf73..aa5ca8a0fe6 100644 --- a/gamestonk_terminal/technical_analysis/README.md +++ b/gamestonk_terminal/technical_analysis/README.md @@ -9,7 +9,7 @@ This menu aims to perform a technical analysis on a pre-loaded ticker chart, and * [recom](#recom) - recommendation based on Technical Indicators [Tradingview API] * [pr](#pr) - - pattern recognition [Finhub] + - pattern recognition [Finnhub] [OVERLAP](#OVERLAP) * [ema](#ema) diff --git a/gamestonk_terminal/technical_analysis/finhub_view.py b/gamestonk_terminal/technical_analysis/finnhub_view.py similarity index 100% rename from gamestonk_terminal/technical_analysis/finhub_view.py rename to gamestonk_terminal/technical_analysis/finnhub_view.py diff --git a/gamestonk_terminal/technical_analysis/ta_controller.py b/gamestonk_terminal/technical_analysis/ta_controller.py index 685d4b104fa..2d48fced7d9 100644 --- a/gamestonk_terminal/technical_analysis/ta_controller.py +++ b/gamestonk_terminal/technical_analysis/ta_controller.py @@ -20,7 +20,7 @@ from gamestonk_terminal.technical_analysis import volume as ta_volume from gamestonk_terminal.technical_analysis import finbrain_view from gamestonk_terminal.technical_analysis import tradingview_view from gamestonk_terminal.technical_analysis import finviz_view -from gamestonk_terminal.technical_analysis import finhub_view +from gamestonk_terminal.technical_analysis import finnhub_view class TechnicalAnalysisController: @@ -90,7 +90,7 @@ class TechnicalAnalysisController: print( " recom recommendation based on Technical Indicators [Tradingview API]" ) - print(" pr pattern recognition [Finhub]") + print(" pr pattern recognition [Finnhub]") print("") print("overlap:") print(" ema exponential moving average") @@ -163,7 +163,7 @@ class TechnicalAnalysisController: def call_pr(self, other_args: List[str]): """Process pr command""" - finhub_view.pattern_recognition_view(other_args, self.ticker) + finnhub_view.pattern_recognition_view(other_args, self.ticker) # OVERLAP def call_ema(self, other_args: List[str]): From 7d6426c4834205e666b5b3dcd0cf4048d0e82607 Mon Sep 17 00:00:00 2001 From: Jamie Date: Sun, 2 May 2021 17:25:45 -0400 Subject: [PATCH 35/74] formatting fixes round 2 --- gamestonk_terminal/README.md | 9 ++++++--- gamestonk_terminal/cryptocurrency/README.md | 19 ++++++++++++------- .../cryptocurrency/crypto_controller.py | 9 ++++----- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/gamestonk_terminal/README.md b/gamestonk_terminal/README.md index 513e0a375a6..cc52c69aef6 100644 --- a/gamestonk_terminal/README.md +++ b/gamestonk_terminal/README.md @@ -274,10 +274,13 @@ Command|Description Command|Description ------ | ------------ -`load`| load cryptocurrency data from coingecko -`view`| view loaded cryptocurrency data -`trend`| view top 7 coins from coingecko +[coingecko](#https://www.coingecko.com/en)| +`load`| load cryptocurrency data +`view`| view loaded cryptocurrency data +`trend`| view top 7 coins +[coinmarketcap](#http://coinmarketcap.com)| `top` | view top coins from coinmarketcap +[binance](#http://binance.us)| `add` | add binance ticker `book`| get and show order book for symbol `candle`| show candlechart for symbol diff --git a/gamestonk_terminal/cryptocurrency/README.md b/gamestonk_terminal/cryptocurrency/README.md index 12dfbf737b4..33a4aecaa28 100644 --- a/gamestonk_terminal/cryptocurrency/README.md +++ b/gamestonk_terminal/cryptocurrency/README.md @@ -48,10 +48,10 @@ Plot the loaded crypto data. ![crypto_view](https://user-images.githubusercontent.com/25267873/115787452-20889a80-a3ba-11eb-9216-f7fd1ffc98cf.png) ## trend -Print the top 7 trending coins from coingecko ```` usage: trend ```` +Print the top 7 trending coins from coingecko ## top @@ -69,26 +69,29 @@ This command displays the top n cryptocurrencies from coinmarketcap.com. crypto ## select -Select a coin/currency to the current object. Note that in binance, the exchange "ticker" is usually COINCURR, such as BTCEUR -for BTC to EURO. There is no USD, but it uses a coin tethered to the USD (USDT), which is the default. Some symbols -are a combination of coins, as `ETHBTC` is a valid symbol + ```` usage: select [-c --coin] [-q --quote] ```` * -c/--coin Coin to load. If not specified, BTC will be loaded to prevent errors later. * -q/--quote Quote currency. Defaults to `USDT` (which is 1-to-1 with USD) +Select a coin/currency to the current object. Note that in binance, the exchange "ticker" is usually COINCURR, such as BTCEUR +for BTC to EURO. There is no USD, but it uses a coin tethered to the USD (USDT), which is the default. Some symbols +are a combination of coins, as `ETHBTC` is a valid symbol + + Note that the usage `select btc` will also work, as it will add `-c` if not detected. ## book -Gets and shows the order book for the given coin/currency. Shows the cumulative amount of orders. ```` usage: book -l/--limit ```` * -l/--limit Number of orders to get on each side. One of [5,10,20,50,100,500,1000,5000]. Defaults to 100. +Gets and shows the order book for the given coin/currency. Shows the cumulative amount of orders. + ## candle -Show candle chart for loaded coin/currency. (Terminal will quit if coin is not defined). ```` usage: candle [-i --interval] [-l --limit] ```` @@ -97,10 +100,12 @@ usage: candle [-i --interval] [-l --limit] * -l/--limit. Number of candles to get. Defaults to 100. Example output (using 3day interval and 50 candles) +Show candle chart for loaded coin/currency. (Terminal will quit if coin is not defined). + ![im](https://user-images.githubusercontent.com/18151143/116797645-f455d380-aab5-11eb-8dbb-df257425302d.png) ## balance -Shows the current holding balance in your account. ```` usage: balance ```` +Shows the current holding balance in your account. diff --git a/gamestonk_terminal/cryptocurrency/crypto_controller.py b/gamestonk_terminal/cryptocurrency/crypto_controller.py index 7789b0b170a..60a9359631c 100644 --- a/gamestonk_terminal/cryptocurrency/crypto_controller.py +++ b/gamestonk_terminal/cryptocurrency/crypto_controller.py @@ -41,14 +41,13 @@ class CryptoController: self.current_currency = None self.current_df = pd.DataFrame() - @staticmethod - def print_help(cont): + def print_help(self): """Print help""" print("\nCryptocurrency:") print(" help show this menu again") print(" q quit this menu, and shows back to main menu") print(" quit quit to abandon program") - print(f"\nCurrent Coin: {cont.current_coin}") + print(f"\nCurrent Coin: {self.current_coin}") print("") print("Coingecko:") print(" load load cryptocurrency data") @@ -83,7 +82,7 @@ class CryptoController: def call_help(self, _): """Process Help command""" - self.print_help(self) + self.print_help() def call_q(self, _): """Process Q command - quit the menu""" @@ -138,7 +137,7 @@ class CryptoController: def menu(): crypto_controller = CryptoController() - crypto_controller.print_help(None) + crypto_controller.print_help() plt.close("all") while True: # Get input command from user From 54fafab4a360626e3f5e5b0d40cf12dc3b7a06e2 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Sun, 2 May 2021 23:02:30 +0100 Subject: [PATCH 36/74] Update README.md --- README.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 55fbedbf1f2..f204eb32be6 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,8 @@ As a modern Python-based environment, GamestonkTerminal opens access to numerous ## Getting Started ### Install +If you'd like to see a video recording of the installation process, @JohnnyDankseed has made one available [here](https://www.twitch.tv/videos/1008414642). + This project was originally written and tested with Python 3.6.8. It should now support Python 3.6, 3.7, and 3.8. Our current recommendation is to use this project with Anaconda's Python distribution - either full [__Anaconda3 Latest__](https://repo.anaconda.com/archive/) or [__Miniconda3 Latest__](https://repo.anaconda.com/archive/). Several features in this project utilize Machine Learning. Machine Learning Python dependencies are optional. If you decided to add Machine Learning features at a later point, you will likely have better user experience with Anaconda's Python distribution. @@ -340,15 +342,18 @@ Trading in financial instruments involves high risks including the risk of losin [James Maslek](https://www.linkedin.com/in/james-maslek-b6810186/) - jmaslek11@gmail.com -Feel free to share loss porn, memes or any questions: https://discord.gg/Up2QGbMKHY +Feel free to share loss porn, memes or any questions at: +* Discord: https://discord.gg/Up2QGbMKHY +* Twitter: [@gamestonkt](https://twitter.com/gamestonkt) ## Acknowledgments -* [VICE article](https://www.vice.com/en/article/qjp9vp/gamestonk-terminal-is-a-diy-meme-stock-version-of-bloomberg-terminal) -* [Daily Fintech article](https://dailyfintech.com/2021/02/25/never-underestimate-bloomberg-but-here-are-5-reasons-why-the-gamestonk-terminal-is-a-contender/) -* [HackerNews](https://news.ycombinator.com/item?id=26258773) -* [Reddit r/algotrading](https://www.reddit.com/r/algotrading/comments/m4uvza/gamestonk_terminal_the_next_best_thing_after/) -* [Reddit r/Python](https://www.reddit.com/r/Python/comments/m515yk/gamestonk_terminal_the_equivalent_to_an/) +* [VICE article - Gamestonk Terminal Is a DIY, Meme Stock Version of Bloomberg Terminal](https://www.vice.com/en/article/qjp9vp/gamestonk-terminal-is-a-diy-meme-stock-version-of-bloomberg-terminal) +* [Daily Fintech article - Never underestimate Bloomberg, but here are 5 reasons why the Gamestonk Terminal is a contender](https://dailyfintech.com/2021/02/25/never-underestimate-bloomberg-but-here-are-5-reasons-why-the-gamestonk-terminal-is-a-contender/) +* [HackerNews - Show HN: Canโ€™t afford Bloomberg Terminal? No prob, I built the next best thing ](https://news.ycombinator.com/item?id=26258773) +* [Reddit r/algotrading - Gamestonk Terminal: The next best thing after Bloomberg Terminal.](https://www.reddit.com/r/algotrading/comments/m4uvza/gamestonk_terminal_the_next_best_thing_after/) +* [Reddit r/Python - Gamestonk Terminal: The equivalent to an open-source python Bloomberg Terminal.](https://www.reddit.com/r/Python/comments/m515yk/gamestonk_terminal_the_equivalent_to_an/) +* [Reddit r/Superstonk - Move over Bloomberg Terminal, here comes Gamestonk Terminal](https://www.reddit.com/r/Superstonk/comments/mx2cjh/move_over_bloomberg_terminal_here_comes_gamestonk/) From 602b75691217b216d4ac822d9e74c936904b678e Mon Sep 17 00:00:00 2001 From: Jamie Date: Sun, 2 May 2021 18:24:14 -0400 Subject: [PATCH 37/74] formatting fixes round 3 --- gamestonk_terminal/README.md | 6 ++--- gamestonk_terminal/cryptocurrency/README.md | 29 ++++++++++++--------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/gamestonk_terminal/README.md b/gamestonk_terminal/README.md index cc52c69aef6..51abdbef252 100644 --- a/gamestonk_terminal/README.md +++ b/gamestonk_terminal/README.md @@ -282,9 +282,9 @@ Command|Description `top` | view top coins from coinmarketcap [binance](#http://binance.us)| `add` | add binance ticker -`book`| get and show order book for symbol -`candle`| show candlechart for symbol -`balance`| look at current binance holdings for loaded coin +`book`| show order book +`candle`| get klines/candles and plot +`balance`| show coin balance   diff --git a/gamestonk_terminal/cryptocurrency/README.md b/gamestonk_terminal/cryptocurrency/README.md index 33a4aecaa28..1a4d228b49f 100644 --- a/gamestonk_terminal/cryptocurrency/README.md +++ b/gamestonk_terminal/cryptocurrency/README.md @@ -2,26 +2,29 @@ This menu aims to explore crypto world, and the usage of the following commands along with an example will be exploited below. +[COINGECKO](#COINGECKO) * [load](#load) * load a given coin vs a given currency [CoinGecko] * [view](#view) * plot the loaded crypto data * [trend](#trend) - * show the top 7 coins from coingecko + * show top 7 trending coins + +[COINMARKETCAP](#COINMARKETCAP) * [top](#top) * view top coins from coinmarketcap [coinmarketcap.com] -* [select](#select) - * select a coin/currency pair for Binance - -* [book](#book) - * get and show market (order) book - -* [candle](#candle) - * show candle plot of selected coin/currency - -* [balance](#balance) - * show current holdings in your account for given coin +[BINANCE](#BINANCE) +* [select](#select) + * select coin/currency to use +* [book](#book) + * show order book +* [candle](#candle) + * get klines/candles and plot +* [balance](#balance) + * show coin balance + +# COINGECKO ## load ```` @@ -53,6 +56,7 @@ usage: trend ```` Print the top 7 trending coins from coingecko +# COINMARKETCAP ## top @@ -68,6 +72,7 @@ This command displays the top n cryptocurrencies from coinmarketcap.com. crypto +# BINANCE ## select ```` From d455c72fe4d5553dd0a35c4a8bfa61207220f07c Mon Sep 17 00:00:00 2001 From: Jamie Date: Sun, 2 May 2021 18:56:31 -0400 Subject: [PATCH 38/74] formatting fixes round 3 --- gamestonk_terminal/cryptocurrency/binance_model.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gamestonk_terminal/cryptocurrency/binance_model.py b/gamestonk_terminal/cryptocurrency/binance_model.py index 29f482e1ca6..1fbc1bedfa5 100644 --- a/gamestonk_terminal/cryptocurrency/binance_model.py +++ b/gamestonk_terminal/cryptocurrency/binance_model.py @@ -242,6 +242,10 @@ def balance(coin: str): print("Check loaded coin") return print("") + amounts = [current_balance["free"], current_balance["locked"]] + df.columns = ["Amount"] + df.index = ["Free", "Locked"] + df["Percent"] = df.div(df.sum(axis=0), axis=1).round(3) print( f"You currently have {current_balance['free']} free and {current_balance['locked']} locked" ) From 8719bc890fe2c03694a0e2c1e5a79af89fb1f7fa Mon Sep 17 00:00:00 2001 From: Jamie Date: Sun, 2 May 2021 19:06:10 -0400 Subject: [PATCH 39/74] formatting fixes round 4 --- gamestonk_terminal/cryptocurrency/binance_model.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/gamestonk_terminal/cryptocurrency/binance_model.py b/gamestonk_terminal/cryptocurrency/binance_model.py index 1fbc1bedfa5..e7938b56371 100644 --- a/gamestonk_terminal/cryptocurrency/binance_model.py +++ b/gamestonk_terminal/cryptocurrency/binance_model.py @@ -7,6 +7,7 @@ import pandas as pd import numpy as np from binance.client import Client from binance.exceptions import BinanceAPIException +form tabulate import tabulate from gamestonk_terminal.main_helper import parse_known_args_and_warn from gamestonk_terminal.config_terminal import API_BINANCE_KEY, API_BINANCE_SECRET from gamestonk_terminal.cryptocurrency.binance_view import plot_order_book, plot_candles @@ -242,13 +243,13 @@ def balance(coin: str): print("Check loaded coin") return print("") - amounts = [current_balance["free"], current_balance["locked"]] + amounts = [float(current_balance["free"]), float(current_balance["locked"])] + total = amounts.sum() df.columns = ["Amount"] df.index = ["Free", "Locked"] df["Percent"] = df.div(df.sum(axis=0), axis=1).round(3) - print( - f"You currently have {current_balance['free']} free and {current_balance['locked']} locked" - ) + print(f"You currently have {total} coins and the breakdown is:") + print(tabulate(df, showindex=True, tablefmt="fancy_grid")) print("") return except Exception as e: From 3df266ff94cbeaf55cdffcf054e8ee6e856447d8 Mon Sep 17 00:00:00 2001 From: Jamie Date: Sun, 2 May 2021 19:27:57 -0400 Subject: [PATCH 40/74] formatting fixes round 5 --- .../cryptocurrency/binance_model.py | 6 +-- poetry.lock | 48 +++++++++---------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/gamestonk_terminal/cryptocurrency/binance_model.py b/gamestonk_terminal/cryptocurrency/binance_model.py index e7938b56371..761d26b6556 100644 --- a/gamestonk_terminal/cryptocurrency/binance_model.py +++ b/gamestonk_terminal/cryptocurrency/binance_model.py @@ -7,7 +7,7 @@ import pandas as pd import numpy as np from binance.client import Client from binance.exceptions import BinanceAPIException -form tabulate import tabulate +from tabulate import tabulate from gamestonk_terminal.main_helper import parse_known_args_and_warn from gamestonk_terminal.config_terminal import API_BINANCE_KEY, API_BINANCE_SECRET from gamestonk_terminal.cryptocurrency.binance_view import plot_order_book, plot_candles @@ -244,12 +244,12 @@ def balance(coin: str): return print("") amounts = [float(current_balance["free"]), float(current_balance["locked"])] - total = amounts.sum() + total = np.sum(amounts) df.columns = ["Amount"] df.index = ["Free", "Locked"] df["Percent"] = df.div(df.sum(axis=0), axis=1).round(3) print(f"You currently have {total} coins and the breakdown is:") - print(tabulate(df, showindex=True, tablefmt="fancy_grid")) + print(tabulate(df, headers = df.columns, showindex=True, tablefmt="fancy_grid")) print("") return except Exception as e: diff --git a/poetry.lock b/poetry.lock index 38f2f7879e1..6b62906f552 100644 --- a/poetry.lock +++ b/poetry.lock @@ -729,7 +729,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "gdown" -version = "3.12.2" +version = "3.13.0" description = "Google Drive direct download of big files." category = "main" optional = true @@ -737,7 +737,7 @@ python-versions = "*" [package.dependencies] filelock = "*" -requests = {version = "*", extras = ["socks"]} +requests = {version = ">=2.12.0", extras = ["socks"]} six = "*" tqdm = "*" @@ -2026,7 +2026,7 @@ python-versions = "*" [[package]] name = "pyobjc-core" -version = "7.1" +version = "7.2" description = "Python<->ObjC Interoperability Module" category = "main" optional = false @@ -2034,14 +2034,14 @@ python-versions = ">=3.6" [[package]] name = "pyobjc-framework-cocoa" -version = "7.1" +version = "7.2" description = "Wrappers for the Cocoa frameworks on macOS" category = "main" optional = false python-versions = ">=3.6" [package.dependencies] -pyobjc-core = ">=7.1" +pyobjc-core = ">=7.2" [[package]] name = "pyotp" @@ -2206,7 +2206,7 @@ python-versions = "*" [[package]] name = "pyupgrade" -version = "2.13.0" +version = "2.14.0" description = "A tool to automatically upgrade syntax for newer versions." category = "dev" optional = false @@ -2265,7 +2265,7 @@ scipy = ">=0.13.2" [[package]] name = "qtconsole" -version = "5.0.3" +version = "5.1.0" description = "Jupyter Qt console" category = "main" optional = false @@ -3514,7 +3514,7 @@ gast = [ {file = "gast-0.3.3.tar.gz", hash = "sha256:b881ef288a49aa81440d2c5eb8aeefd4c2bb8993d5f50edae7413a85bfdb3b57"}, ] gdown = [ - {file = "gdown-3.12.2.tar.gz", hash = "sha256:4b3a1301e57bfd8dce939bf25ef8fbb4b23967fd0f878eede328bdcc41386bac"}, + {file = "gdown-3.13.0.tar.gz", hash = "sha256:d5f9389539673875712beba4936c4ace95d24324953c6f0408a858c534c0bf21"}, ] gensim = [ {file = "gensim-3.8.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:61eed1d6b5fbe6dda0586ea447ebc2dc7890a7f70c2ed953d5abc3fe3cfb94bb"}, @@ -4391,20 +4391,20 @@ pymeeus = [ {file = "PyMeeus-0.5.11.tar.gz", hash = "sha256:bb9d670818d8b0594317b48a7dadea02a0594e5344263bf2054e1a011c8fed55"}, ] pyobjc-core = [ - {file = "pyobjc-core-7.1.tar.gz", hash = "sha256:a0616d5d816b4471f8f782c3a9a8923d2cc85014d88ad4f7fec694be9e6ea349"}, - {file = "pyobjc_core-7.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9fb45c9916f2a03ecd6b9ecde4c35d1d0f1a590ae2ea2372f9d9a360226ac1d"}, - {file = "pyobjc_core-7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fff8e87358c6195a2937004f279050cce3d4c02cd77acd73c5ad367307def855"}, - {file = "pyobjc_core-7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:afb38efd3f2960eb49eb78552d465cfd025a9d6efa06cd4cd8694dafbe7c6e06"}, - {file = "pyobjc_core-7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7cb329c4119044fe83bcb3c5d4794d636c706ff0cb7c1c77d36ef5c373100082"}, - {file = "pyobjc_core-7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7913d7b20217c294900537faf58e5cc15942ed7af277bf05db25667d18255114"}, + {file = "pyobjc-core-7.2.tar.gz", hash = "sha256:9e9ec482d80ea030cdb1613d05a247f31eedabe6666d884d42dd890cc5fb0e05"}, + {file = "pyobjc_core-7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:94b4d9de9d228db52dd35012096d63bdf8c1ace58ea3be1d5f6f39313cd502f2"}, + {file = "pyobjc_core-7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:971cbd7189ae1aa03ef0d16124aa5bcd053779e0e6b6011a41c3dbd5b4ea7e88"}, + {file = "pyobjc_core-7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9d93b20394008373d6d2856d49aaff26f4b97ff42d924a14516c8a82313ec8c0"}, + {file = "pyobjc_core-7.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:860183540d1be792c26426018139ac8ba75e85f675c59ba080ccdc52d8e74c7a"}, + {file = "pyobjc_core-7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ffe61d3c2a404354daf2d895e34e38c5044453353581b3c396bf5365de26250c"}, ] pyobjc-framework-cocoa = [ - {file = "pyobjc-framework-Cocoa-7.1.tar.gz", hash = "sha256:67966152b3d38a0225176fceca2e9f56d849c8e7445548da09a00cb13155ec3e"}, - {file = "pyobjc_framework_Cocoa-7.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:bef77eafaac5eaf1d91d479d5483fd02216caa3edc27e8f5adc9af0b3fecdac3"}, - {file = "pyobjc_framework_Cocoa-7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2ea3582c456827dc20e648c905fdbcf8d3dfae89434f981e9b761cd07262049"}, - {file = "pyobjc_framework_Cocoa-7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a4050f2d776f40c2409a151c6f7896420e936934b3bdbfabedf91509637ed9b"}, - {file = "pyobjc_framework_Cocoa-7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f68f022f1f6d5985c418e10c6608c562fcf4bfe3714ec64fd10ce3dc6221bd4"}, - {file = "pyobjc_framework_Cocoa-7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ecfefd4c48dae42275c18679c69f6f2fff970e711097515a0a8732fc10194018"}, + {file = "pyobjc-framework-Cocoa-7.2.tar.gz", hash = "sha256:c8b23f03dc3f4436d36c0fd006a8a084835c4f6015187df7c3aa5de8ecd5c653"}, + {file = "pyobjc_framework_Cocoa-7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8e5dd5daa0096755937ec24c345a4b07c3fa131a457f99e0fdeeb01979178ec7"}, + {file = "pyobjc_framework_Cocoa-7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:828d183947fc7746953fd0c9b1092cc423745ba0b49719e7b7d1e1614aaa20ec"}, + {file = "pyobjc_framework_Cocoa-7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e4c6d7baa0c2ab5ea5efb8836ad0b3b3976cffcfc6195c1f195e826c6eb5744"}, + {file = "pyobjc_framework_Cocoa-7.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c9a9d1d49cc5a810773c88d6de821e60c8cc41d01113cf1b9e7662938f5f7d66"}, + {file = "pyobjc_framework_Cocoa-7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:506c2cd09f421eac92b9008a0142174c3d1d70ecd4b0e3fa2b924767995fd14e"}, ] pyotp = [ {file = "pyotp-2.6.0-py2.py3-none-any.whl", hash = "sha256:9d144de0f8a601d6869abe1409f4a3f75f097c37b50a36a3bf165810a6e23f28"}, @@ -4482,8 +4482,8 @@ pytz = [ {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, ] pyupgrade = [ - {file = "pyupgrade-2.13.0-py2.py3-none-any.whl", hash = "sha256:1cb5a15573fc63824b952bf7dde5b96f9672a1179ec779ac2d159a845da743c8"}, - {file = "pyupgrade-2.13.0.tar.gz", hash = "sha256:dc6b580b7bc82f357acb354bbba03d8d69da51425ecd5993a2651059bca80518"}, + {file = "pyupgrade-2.14.0-py2.py3-none-any.whl", hash = "sha256:19bd23babd4c09171be66d393d83a53e5ae8d54c6534001c4b7ad92c4d560d69"}, + {file = "pyupgrade-2.14.0.tar.gz", hash = "sha256:34f5dfa57d8007f6cd455a0a423e06e96753b07e4711bac63e3d5c4f3af97cea"}, ] pywin32 = [ {file = "pywin32-300-cp35-cp35m-win32.whl", hash = "sha256:1c204a81daed2089e55d11eefa4826c05e604d27fe2be40b6bf8db7b6a39da63"}, @@ -4588,8 +4588,8 @@ qdldl = [ {file = "qdldl-0.1.5.post0.tar.gz", hash = "sha256:c392c7427651d8b226423c7aba4a0f2338a1f38a4bbdabac6bc4afd8bc934f06"}, ] qtconsole = [ - {file = "qtconsole-5.0.3-py3-none-any.whl", hash = "sha256:4a38053993ca2da058f76f8d75b3d8906efbf9183de516f92f222ac8e37d9614"}, - {file = "qtconsole-5.0.3.tar.gz", hash = "sha256:c091a35607d2a2432e004c4a112d241ce908086570cf68594176dd52ccaa212d"}, + {file = "qtconsole-5.1.0-py3-none-any.whl", hash = "sha256:3a2adecc43ff201a08972fb2179df22e7b3a08d71b9ed680f46ad1bfd4fb9132"}, + {file = "qtconsole-5.1.0.tar.gz", hash = "sha256:12c734494901658787339dea9bbd82f3dc0d5e394071377a1c77b4a0954d7d8b"}, ] qtpy = [ {file = "QtPy-1.9.0-py2.py3-none-any.whl", hash = "sha256:fa0b8363b363e89b2a6f49eddc162a04c0699ae95e109a6be3bb145a913190ea"}, From 1f99f30abecd9341279f7f9f327bf048059c6c88 Mon Sep 17 00:00:00 2001 From: Jamie Date: Sun, 2 May 2021 19:41:01 -0400 Subject: [PATCH 41/74] linting --- gamestonk_terminal/cryptocurrency/binance_model.py | 1 + gamestonk_terminal/cryptocurrency/coinmarketcap_view.py | 2 +- gamestonk_terminal/cryptocurrency/pycoingecko_view.py | 3 +-- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gamestonk_terminal/cryptocurrency/binance_model.py b/gamestonk_terminal/cryptocurrency/binance_model.py index e44a08e6e6c..d6239b85b32 100644 --- a/gamestonk_terminal/cryptocurrency/binance_model.py +++ b/gamestonk_terminal/cryptocurrency/binance_model.py @@ -245,6 +245,7 @@ def balance(coin: str): print("") amounts = [float(current_balance["free"]), float(current_balance["locked"])] total = np.sum(amounts) + df = pd.DataFrame(amounts) df.columns = ["Amount"] df.index = ["Free", "Locked"] df["Percent"] = df.div(df.sum(axis=0), axis=1).round(3) diff --git a/gamestonk_terminal/cryptocurrency/coinmarketcap_view.py b/gamestonk_terminal/cryptocurrency/coinmarketcap_view.py index 310cba96378..f09b4cd41bd 100644 --- a/gamestonk_terminal/cryptocurrency/coinmarketcap_view.py +++ b/gamestonk_terminal/cryptocurrency/coinmarketcap_view.py @@ -62,7 +62,7 @@ def get_cmc_top_n(other_args: List[str]): if not ns_parser: return - cmc = CoinMarketCapAPI(COINMARKETCAP_KEY) + cmc = CoinMarketCapAPI(API_CMC_KEY) ratings = cmc.cryptocurrency_listings_latest().data symbol, rank, price, pchange1d, mkt_cap = [], [], [], [], [] diff --git a/gamestonk_terminal/cryptocurrency/pycoingecko_view.py b/gamestonk_terminal/cryptocurrency/pycoingecko_view.py index 4402703165d..926a2c3726c 100644 --- a/gamestonk_terminal/cryptocurrency/pycoingecko_view.py +++ b/gamestonk_terminal/cryptocurrency/pycoingecko_view.py @@ -24,8 +24,6 @@ for single_coin in coins: coin_ids.append(single_coin["id"]) # pylint: disable=inconsistent-return-statements - - def load(other_args: List[str]): """Load selected Cryptocurrency @@ -150,6 +148,7 @@ def view(coin: str, prices: pd.DataFrame, other_args: List[str]): def trend(): """Prints top 7 coins from pycoingecko""" + cg = CoinGeckoAPI() trending = cg.get_search_trending()["coins"] name, symbol, price, rank = [], [], [], [] From f33f878fa31c886c55aab2f7d5a087ce69996366 Mon Sep 17 00:00:00 2001 From: Jamie Date: Sun, 2 May 2021 19:43:58 -0400 Subject: [PATCH 42/74] flake --- gamestonk_terminal/cryptocurrency/pycoingecko_view.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gamestonk_terminal/cryptocurrency/pycoingecko_view.py b/gamestonk_terminal/cryptocurrency/pycoingecko_view.py index 926a2c3726c..c4f6fca5f3c 100644 --- a/gamestonk_terminal/cryptocurrency/pycoingecko_view.py +++ b/gamestonk_terminal/cryptocurrency/pycoingecko_view.py @@ -24,6 +24,8 @@ for single_coin in coins: coin_ids.append(single_coin["id"]) # pylint: disable=inconsistent-return-statements + + def load(other_args: List[str]): """Load selected Cryptocurrency From 40368333b9a17d8ec2a74c62f97096817a5bd108 Mon Sep 17 00:00:00 2001 From: Jamie Date: Sun, 2 May 2021 19:49:11 -0400 Subject: [PATCH 43/74] remerge finnhub info --- README.md | 3 ++- gamestonk_terminal/config_terminal.py | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 63eb4f5379a..f043100cdfa 100644 --- a/README.md +++ b/README.md @@ -227,11 +227,12 @@ These are the ones where a key is necessary: * Tradier: https://developer.tradier.com/getting_started * Oanda API: https://developer.oanda.com * CoinMarketCap API: https://coinmarketcap.com/api/ + * Finhub API: https://finnhub.io * Binance: https://binance.us When these are obtained, don't forget to update [config_terminal.py](/gamestonk_terminal/config_terminal.py). -Alternatively, you can also set them to the following environment variables: GT_API_KEY_ALPHAVANTAGE, GT_API_KEY_FINANCIALMODELINGPREP, GT_API_KEY_QUANDL, GT_API_REDDIT_CLIENT_ID, GT_API_REDDIT_CLIENT_SECRET, GT_API_REDDIT_USERNAME, GT_API_REDDIT_USER_AGENT, GT_API_REDDIT_PASSWORD, GT_API_TWITTER_KEY, GT_API_TWITTER_SECRET_KEY, GT_API_TWITTER_BEARER_TOKEN, GT_API_POLYGON_KEY, GT_API_FRED_KEY, GT_API_NEWS_TOKEN, GT_API_TRADIER_TOKEN, GT_API_CMC_KEY, GT_API_BINANCE_KEY, GT_API_BINANCE_SECRET. +Alternatively, you can also set them to the following environment variables: GT_API_KEY_ALPHAVANTAGE, GT_API_KEY_FINANCIALMODELINGPREP, GT_API_KEY_QUANDL, GT_API_REDDIT_CLIENT_ID, GT_API_REDDIT_CLIENT_SECRET, GT_API_REDDIT_USERNAME, GT_API_REDDIT_USER_AGENT, GT_API_REDDIT_PASSWORD, GT_API_TWITTER_KEY, GT_API_TWITTER_SECRET_KEY, GT_API_TWITTER_BEARER_TOKEN, GT_API_POLYGON_KEY, GT_API_FRED_KEY, GT_API_NEWS_TOKEN, GT_API_TRADIER_TOKEN, GT_API_CMC_KEY, API_FINNHUB_KEY, GT_API_BINANCE_KEY, GT_API_BINANCE_SECRET. The following environment variables are used for trading accounts: GT_OANDA_TOKEN, GT_OANDA_ACCOUNT, GT_RH_USERNAME, GT_RH_PASSWORD diff --git a/gamestonk_terminal/config_terminal.py b/gamestonk_terminal/config_terminal.py index 6a8350e9dc9..3685f88e7dd 100644 --- a/gamestonk_terminal/config_terminal.py +++ b/gamestonk_terminal/config_terminal.py @@ -63,3 +63,6 @@ API_CMC_KEY = os.getenv("GT_API_CMC_KEY") or "REPLACE_ME" # https://www.binance.com/en/ API_BINANCE_KEY = os.getenv("GT_API_BINANCE_KEY") or "REPLACE_ME" API_BINANCE_SECRET = os.getenv("GT_API_BINANCE_SECRET") or "REPLACE_ME" + +# https://finnhub.io +API_FINNHUB_KEY = os.getenv("GT_API_FINNHUB_KEY") or "REPLACE_ME" From e547b0e11c8b950d17f096a91c83cfe7cfb9aa0c Mon Sep 17 00:00:00 2001 From: didier Date: Mon, 3 May 2021 10:58:33 +0100 Subject: [PATCH 44/74] Add ratings over time to due diligence menu --- gamestonk_terminal/config_terminal.py | 2 +- gamestonk_terminal/due_diligence/dd_controller.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/gamestonk_terminal/config_terminal.py b/gamestonk_terminal/config_terminal.py index 3dfa270ae34..babd6245ce8 100644 --- a/gamestonk_terminal/config_terminal.py +++ b/gamestonk_terminal/config_terminal.py @@ -61,4 +61,4 @@ PATH_TO_SELENIUM_DRIVER = None # Replace with "PATH" COINMARKETCAP_KEY = os.getenv("GT_CMC_API_KEY") or "REPLACE_ME" # https://finnhub.io -API_FINNHUB_KEY = os.getenv("GT_API_FINNHUB_KEY") or "REPLACE_ME" +API_FINNHUB_KEY = os.getenv("GT_API_FINNHUB_KEY") or "c23k26qad3ieeb1lftug" diff --git a/gamestonk_terminal/due_diligence/dd_controller.py b/gamestonk_terminal/due_diligence/dd_controller.py index 27efe9eb880..f24ddcad017 100644 --- a/gamestonk_terminal/due_diligence/dd_controller.py +++ b/gamestonk_terminal/due_diligence/dd_controller.py @@ -15,6 +15,7 @@ from gamestonk_terminal.due_diligence import reddit_view as r_view from gamestonk_terminal.due_diligence import news_view from gamestonk_terminal.due_diligence import finra_view from gamestonk_terminal.due_diligence import sec_view +from gamestonk_terminal.due_diligence import finnhub_view from gamestonk_terminal import feature_flags as gtff from gamestonk_terminal.helper_funcs import get_flair from gamestonk_terminal.menu import session @@ -32,6 +33,7 @@ class DueDiligenceController: "short", "rating", "pt", + "rot", "est", "ins", "insider", @@ -91,6 +93,9 @@ class DueDiligenceController: " rating rating of the company from strong sell to strong buy [FMP]" ) print(" pt price targets over time [Business Insider]") + print( + " rot rating over timefrom strong sell to strong buy [Finnhub]" + ) print( " est quarter and year analysts earnings estimates [Business Insider]" ) @@ -155,6 +160,10 @@ class DueDiligenceController: other_args, self.stock, self.ticker, self.start, self.interval ) + def call_rot(self, other_args: List[str]): + """Process rot command""" + finnhub_view.rating_over_time(other_args, self.ticker) + def call_est(self, other_args: List[str]): """Process est command""" bi_view.estimates(other_args, self.ticker) From 22d57638c41c8a3d6eac4f739eb5f603a860c309 Mon Sep 17 00:00:00 2001 From: didier Date: Mon, 3 May 2021 11:00:35 +0100 Subject: [PATCH 45/74] forgot to commit main file added lol --- .../due_diligence/finnhub_view.py | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 gamestonk_terminal/due_diligence/finnhub_view.py diff --git a/gamestonk_terminal/due_diligence/finnhub_view.py b/gamestonk_terminal/due_diligence/finnhub_view.py new file mode 100644 index 00000000000..112032ea462 --- /dev/null +++ b/gamestonk_terminal/due_diligence/finnhub_view.py @@ -0,0 +1,108 @@ +import argparse +from typing import List +import requests +import pandas as pd +from matplotlib import pyplot as plt +from pandas.plotting import register_matplotlib_converters +from gamestonk_terminal import config_terminal as cfg +from gamestonk_terminal.helper_funcs import ( + parse_known_args_and_warn, + plot_autoscale, +) +from gamestonk_terminal.config_plot import PLOT_DPI +from gamestonk_terminal import feature_flags as gtff + +register_matplotlib_converters() + + +def get_rating_over_time(ticker: str) -> pd.DataFrame: + """Get rating over time + + Parameters + ---------- + ticker : str + Ticker to get ratings from + + Returns + ------- + pd.DataFrame + Get dataframe with economic calendar events + """ + response = requests.get( + f"https://finnhub.io/api/v1/stock/recommendation?symbol={ticker}&token={cfg.API_FINNHUB_KEY}" + ) + if response.status_code == 200: + return pd.DataFrame(response.json()) + + return pd.DataFrame() + + +def plot_rating_over_time(rot: pd.DataFrame, ticker: str): + """Plot rating over time + + Parameters + ---------- + rot : pd.DataFrame + Rating over time + ticker : str + Ticker associated with ratings + + """ + plt.figure(figsize=plot_autoscale(), dpi=PLOT_DPI) + + rot.sort_values("period", inplace=True) + plt.plot(pd.to_datetime(rot["period"]), rot["strongBuy"], c="green", lw=3) + plt.plot(pd.to_datetime(rot["period"]), rot["buy"], c="lightgreen", lw=3) + plt.plot(pd.to_datetime(rot["period"]), rot["hold"], c="grey", lw=3) + plt.plot(pd.to_datetime(rot["period"]), rot["sell"], c="pink", lw=3) + plt.plot(pd.to_datetime(rot["period"]), rot["strongSell"], c="red", lw=3) + plt.xlim( + pd.to_datetime(rot["period"].values[0]), + pd.to_datetime(rot["period"].values[-1]), + ) + plt.grid() + plt.title(f"{ticker}'s ratings over time") + plt.xlabel("Time") + plt.ylabel("Rating") + plt.legend(["Strong Buy", "Buy", "Hold", "Sell", "Strong Sell"]) + + if gtff.USE_ION: + plt.ion() + + plt.show() + + +def rating_over_time(other_args: List[str], ticker: str): + """Rating over time + + Parameters + ---------- + other_args : List[str] + Command line arguments to be processed with argparse + ticker : str + Ticker to get ratings from + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="rot", + description=""" + Rating over time. [Source: https://finnhub.io] + """, + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + df_rot = get_rating_over_time(ticker) + + if df_rot.empty: + print("No ratings over time found\n") + return + + plot_rating_over_time(df_rot, ticker) + print("") + + except Exception as e: + print(e, "\n") From 99ef4d9b12715a96cc0c21174ff6226d7e5e726f Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Mon, 3 May 2021 11:14:28 +0100 Subject: [PATCH 46/74] Update README.md --- gamestonk_terminal/due_diligence/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/gamestonk_terminal/due_diligence/README.md b/gamestonk_terminal/due_diligence/README.md index cb8854b78e5..7a88574392a 100644 --- a/gamestonk_terminal/due_diligence/README.md +++ b/gamestonk_terminal/due_diligence/README.md @@ -12,6 +12,8 @@ This menu aims to help in due-diligence of a pre-loaded stock, and the usage of * rating of the company from strong sell to strong buy [FMP] * [pt](#pt) * price targets over time [Business Insider] +* [rot](#rot) + * ratings over time [Finnhub] * [est](#est) * quarter and year analysts earnings estimates [Business Insider] * [ins](#ins) @@ -89,6 +91,18 @@ Prints price target from analysts. [Source: Business Insider] pt2 + +## rot + +```text +usage: rot +``` + +Plots ratings over time. [Source: Finnhub] + +![rot](https://user-images.githubusercontent.com/25267873/116864981-ad282980-ac00-11eb-9e86-7163782a8ef6.png) + + ## est ```text From f18567c6604eaa58b967af580c621fda7bb46326 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Mon, 3 May 2021 11:15:33 +0100 Subject: [PATCH 47/74] Update README.md --- gamestonk_terminal/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/gamestonk_terminal/README.md b/gamestonk_terminal/README.md index 5ac456b0be8..8b43ff8b72f 100644 --- a/gamestonk_terminal/README.md +++ b/gamestonk_terminal/README.md @@ -212,6 +212,7 @@ Command|Description|Source `analyst` |analyst prices and ratings of the company |[Finviz](https://finviz.com/) `rating` |rating of the company from strong sell to strong buy | [FMP](https://financialmodelingprep.com/) `pt` |price targets over time |[Business Insider](https://www.businessinsider.com/) +`rot` |ratings over time |[Finnhub](https://finnhub.io) `est` |quarter and year analysts earnings estimates |[Business Insider](https://www.businessinsider.com/) `ins` |insider activity over time |[Business Insider](https://www.businessinsider.com/) `insider` |insider trading of the company |[Finviz](https://finviz.com/) From 4e471621eb45bc95832f14636851adef29217fe8 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Mon, 3 May 2021 11:17:48 +0100 Subject: [PATCH 48/74] Update ROADMAP.md --- ROADMAP.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index e2ddc7cf15d..277e09931fd 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -87,7 +87,8 @@ ## Due Diligence * [x] Add dark pools (ATS) vs Non-ATS data over time (@didier) - [PR #363](https://github.com/DidierRLopes/GamestonkTerminal/pull/363) -* [x] Add failure to deliver command (@hinxx) - [PR #366](#https://github.com/DidierRLopes/GamestonkTerminal/pull/366) +* [x] Add failure to deliver command (@hinxx) - [PR #366](https://github.com/DidierRLopes/GamestonkTerminal/pull/366) +* [x] Ratings over time (@didier) - [PR #431](https://github.com/DidierRLopes/GamestonkTerminal/pull/431) **NEXT** @@ -197,7 +198,7 @@ ___ --- ## Cryptocurrencies -* [x] Add Coingecko (@jmaslek) - [PR #283](#https://github.com/DidierRLopes/GamestonkTerminal/pull/283) +* [x] Add Coingecko (@jmaslek) - [PR #283](https://github.com/DidierRLopes/GamestonkTerminal/pull/283) * [x] view top coins from coinmarketcap (@jmaslek) - [PR #378](https://github.com/DidierRLopes/GamestonkTerminal/pull/378) **NEXT** From 8972ed5e44bd7baab4573fe504db8b9f9610a2d8 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Mon, 3 May 2021 11:42:05 +0100 Subject: [PATCH 49/74] Update config_terminal.py --- gamestonk_terminal/config_terminal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gamestonk_terminal/config_terminal.py b/gamestonk_terminal/config_terminal.py index babd6245ce8..e305115bf48 100644 --- a/gamestonk_terminal/config_terminal.py +++ b/gamestonk_terminal/config_terminal.py @@ -33,7 +33,7 @@ API_TWITTER_SECRET_KEY = os.getenv("GT_API_TWITTER_SECRET_KEY") or "REPLACE_ME" API_TWITTER_BEARER_TOKEN = os.getenv("GT_API_TWITTER_BEARER_TOKEN") or "REPLACE_ME" # https://fred.stlouisfed.org/docs/api/api_key.html -API_FRED_KEY = os.getenv("GT_FRED_API_KEY") or "b98e15f1fde39212bce9213f2e3e5afb" +API_FRED_KEY = os.getenv("GT_FRED_API_KEY") or "REPLACE_ME" # https://newsapi.org API_NEWS_TOKEN = os.getenv("GT_API_NEWS_TOKEN") or "REPLACE_ME" @@ -61,4 +61,4 @@ PATH_TO_SELENIUM_DRIVER = None # Replace with "PATH" COINMARKETCAP_KEY = os.getenv("GT_CMC_API_KEY") or "REPLACE_ME" # https://finnhub.io -API_FINNHUB_KEY = os.getenv("GT_API_FINNHUB_KEY") or "c23k26qad3ieeb1lftug" +API_FINNHUB_KEY = os.getenv("GT_API_FINNHUB_KEY") or "REPLACE_ME" From 1fd075e12970a445c0a1aca825aa582c5c66412c Mon Sep 17 00:00:00 2001 From: didier Date: Mon, 3 May 2021 12:39:24 +0100 Subject: [PATCH 50/74] Add IPOs calendar to discovery menu --- .../discovery/disc_controller.py | 7 + gamestonk_terminal/discovery/finnhub_view.py | 122 ++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 gamestonk_terminal/discovery/finnhub_view.py diff --git a/gamestonk_terminal/discovery/disc_controller.py b/gamestonk_terminal/discovery/disc_controller.py index 855f2eeb0d4..07731915805 100644 --- a/gamestonk_terminal/discovery/disc_controller.py +++ b/gamestonk_terminal/discovery/disc_controller.py @@ -22,6 +22,7 @@ from gamestonk_terminal.discovery import ( yahoo_finance_view, marketbeat_view, finra_ats_view, + finnhub_view, ) @@ -33,6 +34,7 @@ class DiscoveryController: "help", "q", "quit", + "ipo", "map", "rtp_sectors", "gainers", @@ -72,6 +74,7 @@ class DiscoveryController: print(" q quit this menu, and shows back to main menu") print(" quit quit to abandon program") print("") + print(" ipo past and future IPOs [Finnhub]") print(" map S&P500 index stocks map [Finviz]") print(" rtp_sectors real-time performance sectors [Alpha Vantage]") print(" gainers show latest top gainers [Yahoo Finance]") @@ -135,6 +138,10 @@ class DiscoveryController: """Process Quit command - quit the program""" return True + def call_ipo(self, other_args: List[str]): + """Process ipo command""" + finnhub_view.ipo_calendar(other_args) + def call_map(self, other_args: List[str]): """Process map command""" finviz_view.map_sp500_view(other_args) diff --git a/gamestonk_terminal/discovery/finnhub_view.py b/gamestonk_terminal/discovery/finnhub_view.py new file mode 100644 index 00000000000..f1220eb70ca --- /dev/null +++ b/gamestonk_terminal/discovery/finnhub_view.py @@ -0,0 +1,122 @@ +import argparse +from typing import List +import requests +import pandas as pd +from datetime import datetime, timedelta +from gamestonk_terminal import config_terminal as cfg +from gamestonk_terminal.helper_funcs import ( + parse_known_args_and_warn, + check_non_negative, +) + + +def get_ipo_calendar(from_date: str, to_date: str) -> pd.DataFrame: + """Get IPO calendar + + Parameters + ---------- + from_date : str + from date (%Y-%m-%d) to get IPO calendar + to_date : str + to date (%Y-%m-%d) to get IPO calendar + + Returns + ------- + pd.DataFrame + Get dataframe with economic calendar events + """ + response = requests.get( + f"https://finnhub.io/api/v1/calendar/ipo?from={from_date}&to={to_date}&token={cfg.API_FINNHUB_KEY}" + ) + if response.status_code == 200: + d_data = response.json() + if "ipoCalendar" in d_data: + d_refactor_columns = { + "numberOfShares": "Number of Shares", + "totalSharesValue": "Total Shares Value", + "date": "Date", + "exchange": "Exchange", + "name": "Name", + "price": "Price", + "status": "Status", + } + return pd.DataFrame(d_data["ipoCalendar"]).rename( + columns=d_refactor_columns + ) + + return pd.DataFrame() + + +def ipo_calendar(other_args: List[str]): + """Past and future IPOs calendar [Finnhub] + + Parameters + ---------- + other_args : List[str] + Command line arguments to be processed with argparse + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="ipo", + description=""" + Past and future IPOs. [Source: https://finnhub.io] + """, + ) + parser.add_argument( + "-p", + "--past", + action="store", + dest="past_days", + type=check_non_negative, + default=0, + help="Number of past days to look for IPOs.", + ) + parser.add_argument( + "-f", + "--future", + action="store", + dest="future_days", + type=check_non_negative, + default=10, + help="Number of future days to look for IPOs.", + ) + + try: + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-f") + + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + today = datetime.now() + + if ns_parser.future_days > 0: + future_date = today + timedelta(days=ns_parser.future_days) + + df_future_ipo = get_ipo_calendar( + today.strftime("%Y-%m-%d"), future_date.strftime("%Y-%m-%d") + ).rename(columns={"Date": "Future"}) + + if df_future_ipo.empty: + print(f"No IPOs found for the next {ns_parser.future_days} days") + else: + print(df_future_ipo.to_string(index=False)) + print("") + + if ns_parser.past_days > 0: + past_date = today - timedelta(days=ns_parser.past_days) + + df_past_ipo = get_ipo_calendar( + past_date.strftime("%Y-%m-%d"), today.strftime("%Y-%m-%d") + ).rename(columns={"Date": "Past"}) + + if df_past_ipo.empty: + print(f"No IPOs found since the last {ns_parser.past_days} days") + else: + print(df_past_ipo.to_string(index=False)) + print("") + + except Exception as e: + print(e, "\n") From 614dcd9a7a6ed7de8c0e6ffc3957ec7cb0d3c305 Mon Sep 17 00:00:00 2001 From: didier Date: Mon, 3 May 2021 12:49:41 +0100 Subject: [PATCH 51/74] imports order --- gamestonk_terminal/discovery/finnhub_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gamestonk_terminal/discovery/finnhub_view.py b/gamestonk_terminal/discovery/finnhub_view.py index f1220eb70ca..8fa058b54c3 100644 --- a/gamestonk_terminal/discovery/finnhub_view.py +++ b/gamestonk_terminal/discovery/finnhub_view.py @@ -1,8 +1,8 @@ import argparse from typing import List +from datetime import datetime, timedelta import requests import pandas as pd -from datetime import datetime, timedelta from gamestonk_terminal import config_terminal as cfg from gamestonk_terminal.helper_funcs import ( parse_known_args_and_warn, From 3494e232bce72e6d6b7b191d39e73438177dd8c6 Mon Sep 17 00:00:00 2001 From: Jamie Date: Mon, 3 May 2021 07:55:00 -0400 Subject: [PATCH 52/74] round 2 changes part 1 --- README.md | 26 ++++++++++--------- gamestonk_terminal/README.md | 2 +- gamestonk_terminal/cryptocurrency/README.md | 14 +++++----- .../cryptocurrency/binance_model.py | 12 ++++----- .../cryptocurrency/coinmarketcap_view.py | 4 +-- 5 files changed, 30 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index f043100cdfa..f35535093b3 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,8 @@ As a modern Python-based environment, GamestonkTerminal opens access to numerous ## Getting Started ### Install +If you'd like to see a video recording of the installation process, @JohnnyDankseed has made one available [here](https://www.twitch.tv/videos/1008414642). + This project was originally written and tested with Python 3.6.8. It should now support Python 3.6, 3.7, and 3.8. Our current recommendation is to use this project with Anaconda's Python distribution - either full [__Anaconda3 Latest__](https://repo.anaconda.com/archive/) or [__Miniconda3 Latest__](https://repo.anaconda.com/archive/). Several features in this project utilize Machine Learning. Machine Learning Python dependencies are optional. If you decided to add Machine Learning features at a later point, you will likely have better user experience with Anaconda's Python distribution. @@ -227,15 +229,12 @@ These are the ones where a key is necessary: * Tradier: https://developer.tradier.com/getting_started * Oanda API: https://developer.oanda.com * CoinMarketCap API: https://coinmarketcap.com/api/ - * Finhub API: https://finnhub.io - * Binance: https://binance.us + * Finhub API: https://finnhub.io + * Binance: https://binance.us (US) / https://binance.com (Outside US) When these are obtained, don't forget to update [config_terminal.py](/gamestonk_terminal/config_terminal.py). -Alternatively, you can also set them to the following environment variables: GT_API_KEY_ALPHAVANTAGE, GT_API_KEY_FINANCIALMODELINGPREP, GT_API_KEY_QUANDL, GT_API_REDDIT_CLIENT_ID, GT_API_REDDIT_CLIENT_SECRET, GT_API_REDDIT_USERNAME, GT_API_REDDIT_USER_AGENT, GT_API_REDDIT_PASSWORD, GT_API_TWITTER_KEY, GT_API_TWITTER_SECRET_KEY, GT_API_TWITTER_BEARER_TOKEN, GT_API_POLYGON_KEY, GT_API_FRED_KEY, GT_API_NEWS_TOKEN, GT_API_TRADIER_TOKEN, GT_API_CMC_KEY, API_FINNHUB_KEY, GT_API_BINANCE_KEY, GT_API_BINANCE_SECRET. - -The following environment variables are used for trading accounts: -GT_OANDA_TOKEN, GT_OANDA_ACCOUNT, GT_RH_USERNAME, GT_RH_PASSWORD +Alternatively, you can also set them to the following environment variables: GT_API_KEY_ALPHAVANTAGE, GT_API_KEY_FINANCIALMODELINGPREP, GT_API_KEY_QUANDL, GT_API_REDDIT_CLIENT_ID, GT_API_REDDIT_CLIENT_SECRET, GT_API_REDDIT_USERNAME, GT_API_REDDIT_USER_AGENT, GT_API_REDDIT_PASSWORD, GT_API_TWITTER_KEY, GT_API_TWITTER_SECRET_KEY, GT_API_TWITTER_BEARER_TOKEN, GT_API_POLYGON_KEY, GT_FRED_API_KEY, GT_API_NEWS_TOKEN, GT_OANDA_TOKEN, GT_OANDA_ACCOUNT, GT_TRADIER_TOKEN, GT_CMC_API_KEY, GT_CMC_API_KEY, API_FINNHUB_KEY, GT_API_BINANCE_KEY, GT_API_BINANCE_SECRET. Example: ``` @@ -344,15 +343,18 @@ Trading in financial instruments involves high risks including the risk of losin [James Maslek](https://www.linkedin.com/in/james-maslek-b6810186/) - jmaslek11@gmail.com -Feel free to share loss porn, memes or any questions: https://discord.gg/Up2QGbMKHY +Feel free to share loss porn, memes or any questions at: +* Discord: https://discord.gg/Up2QGbMKHY +* Twitter: [@gamestonkt](https://twitter.com/gamestonkt) ## Acknowledgments -* [VICE article](https://www.vice.com/en/article/qjp9vp/gamestonk-terminal-is-a-diy-meme-stock-version-of-bloomberg-terminal) -* [Daily Fintech article](https://dailyfintech.com/2021/02/25/never-underestimate-bloomberg-but-here-are-5-reasons-why-the-gamestonk-terminal-is-a-contender/) -* [HackerNews](https://news.ycombinator.com/item?id=26258773) -* [Reddit r/algotrading](https://www.reddit.com/r/algotrading/comments/m4uvza/gamestonk_terminal_the_next_best_thing_after/) -* [Reddit r/Python](https://www.reddit.com/r/Python/comments/m515yk/gamestonk_terminal_the_equivalent_to_an/) +* [VICE article - Gamestonk Terminal Is a DIY, Meme Stock Version of Bloomberg Terminal](https://www.vice.com/en/article/qjp9vp/gamestonk-terminal-is-a-diy-meme-stock-version-of-bloomberg-terminal) +* [Daily Fintech article - Never underestimate Bloomberg, but here are 5 reasons why the Gamestonk Terminal is a contender](https://dailyfintech.com/2021/02/25/never-underestimate-bloomberg-but-here-are-5-reasons-why-the-gamestonk-terminal-is-a-contender/) +* [HackerNews - Show HN: Canโ€™t afford Bloomberg Terminal? No prob, I built the next best thing ](https://news.ycombinator.com/item?id=26258773) +* [Reddit r/algotrading - Gamestonk Terminal: The next best thing after Bloomberg Terminal.](https://www.reddit.com/r/algotrading/comments/m4uvza/gamestonk_terminal_the_next_best_thing_after/) +* [Reddit r/Python - Gamestonk Terminal: The equivalent to an open-source python Bloomberg Terminal.](https://www.reddit.com/r/Python/comments/m515yk/gamestonk_terminal_the_equivalent_to_an/) +* [Reddit r/Superstonk - Move over Bloomberg Terminal, here comes Gamestonk Terminal](https://www.reddit.com/r/Superstonk/comments/mx2cjh/move_over_bloomberg_terminal_here_comes_gamestonk/) diff --git a/gamestonk_terminal/README.md b/gamestonk_terminal/README.md index 9e2cbaffa8b..32a42a6e67a 100644 --- a/gamestonk_terminal/README.md +++ b/gamestonk_terminal/README.md @@ -282,7 +282,7 @@ Command|Description [coinmarketcap](#http://coinmarketcap.com)| `top` | view top coins from coinmarketcap [binance](#http://binance.us)| -`add` | add binance ticker +`select` | Select a coin/currency `book`| show order book `candle`| get klines/candles and plot `balance`| show coin balance diff --git a/gamestonk_terminal/cryptocurrency/README.md b/gamestonk_terminal/cryptocurrency/README.md index 1a4d228b49f..7b2dbabe1db 100644 --- a/gamestonk_terminal/cryptocurrency/README.md +++ b/gamestonk_terminal/cryptocurrency/README.md @@ -78,34 +78,34 @@ This command displays the top n cryptocurrencies from coinmarketcap.com. ```` usage: select [-c --coin] [-q --quote] ```` -* -c/--coin Coin to load. If not specified, BTC will be loaded to prevent errors later. -* -q/--quote Quote currency. Defaults to `USDT` (which is 1-to-1 with USD) - Select a coin/currency to the current object. Note that in binance, the exchange "ticker" is usually COINCURR, such as BTCEUR for BTC to EURO. There is no USD, but it uses a coin tethered to the USD (USDT), which is the default. Some symbols are a combination of coins, as `ETHBTC` is a valid symbol +* -c/--coin Coin to load. If not specified, BTC will be loaded to prevent errors later. +* -q/--quote Quote currency. Defaults to `USDT` (which is 1-to-1 with USD) -Note that the usage `select btc` will also work, as it will add `-c` if not detected. +*Note that the usage `select btc` will also work, as it will add `-c` if not detected. ## book ```` usage: book -l/--limit ```` -* -l/--limit Number of orders to get on each side. One of [5,10,20,50,100,500,1000,5000]. Defaults to 100. - Gets and shows the order book for the given coin/currency. Shows the cumulative amount of orders. +* -l/--limit Number of orders to get on each side. One of [5,10,20,50,100,500,1000,5000]. Defaults to 100. + ## candle ```` usage: candle [-i --interval] [-l --limit] ```` +Show candle chart for loaded coin/currency. (Terminal will quit if coin is not defined). + * -i/--interval. Interval for candles. One of [1day,3day,1hour,2hour,4hour,6hour,8hour,12hour,1week,1min,3min,5min,15min,30min,1month]. Defaults to 1day. * -l/--limit. Number of candles to get. Defaults to 100. Example output (using 3day interval and 50 candles) -Show candle chart for loaded coin/currency. (Terminal will quit if coin is not defined). ![im](https://user-images.githubusercontent.com/18151143/116797645-f455d380-aab5-11eb-8dbb-df257425302d.png) diff --git a/gamestonk_terminal/cryptocurrency/binance_model.py b/gamestonk_terminal/cryptocurrency/binance_model.py index d6239b85b32..03b5589214c 100644 --- a/gamestonk_terminal/cryptocurrency/binance_model.py +++ b/gamestonk_terminal/cryptocurrency/binance_model.py @@ -9,13 +9,13 @@ from binance.client import Client from binance.exceptions import BinanceAPIException from tabulate import tabulate from gamestonk_terminal.main_helper import parse_known_args_and_warn -from gamestonk_terminal.config_terminal import API_BINANCE_KEY, API_BINANCE_SECRET +import gamestonk_terminal.config_terminal as cfg from gamestonk_terminal.cryptocurrency.binance_view import plot_order_book, plot_candles def check_valid_binance_str(symbol: str) -> str: """Check if symbol is in defined binance""" - client = Client(API_BINANCE_KEY, API_BINANCE_SECRET) + client = Client(cfg.API_BINANCE_KEY, cfg.API_BINANCE_SECRET) try: client.get_avg_price(symbol=symbol.upper()) return symbol.upper() @@ -107,7 +107,7 @@ def order_book(coin: str, other_args: List[str]): ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return - client = Client(API_BINANCE_KEY, API_BINANCE_SECRET) + client = Client(cfg.API_BINANCE_KEY, cfg.API_BINANCE_SECRET) market_book = client.get_order_book(symbol=coin, limit=ns_parser.limit) bids = np.asarray(market_book["bids"], dtype=float) asks = np.asarray(market_book["asks"], dtype=float) @@ -134,7 +134,7 @@ def show_candles(coin: str, other_args: List[str]): ------- """ - client = Client(API_BINANCE_KEY, API_BINANCE_SECRET) + client = Client(cfg.API_BINANCE_KEY, cfg.API_BINANCE_SECRET) interval_map = { "1day": client.KLINE_INTERVAL_1DAY, @@ -236,7 +236,7 @@ def balance(coin: str): ------- """ - client = Client(API_BINANCE_KEY, API_BINANCE_SECRET) + client = Client(cfg.API_BINANCE_KEY, cfg.API_BINANCE_SECRET) try: current_balance = client.get_asset_balance(asset=coin) if current_balance is None: @@ -245,7 +245,7 @@ def balance(coin: str): print("") amounts = [float(current_balance["free"]), float(current_balance["locked"])] total = np.sum(amounts) - df = pd.DataFrame(amounts) + df = pd.DataFrame(amounts).apply(lambda x: str(float(x)) df.columns = ["Amount"] df.index = ["Free", "Locked"] df["Percent"] = df.div(df.sum(axis=0), axis=1).round(3) diff --git a/gamestonk_terminal/cryptocurrency/coinmarketcap_view.py b/gamestonk_terminal/cryptocurrency/coinmarketcap_view.py index f09b4cd41bd..3fcd0d09dee 100644 --- a/gamestonk_terminal/cryptocurrency/coinmarketcap_view.py +++ b/gamestonk_terminal/cryptocurrency/coinmarketcap_view.py @@ -6,7 +6,7 @@ from typing import List import pandas as pd from tabulate import tabulate from coinmarketcapapi import CoinMarketCapAPI -from gamestonk_terminal.config_terminal import API_CMC_KEY +import gamestonk_terminal.config_terminal as cfg from gamestonk_terminal.helper_funcs import parse_known_args_and_warn sort_options = ["Symbol", "CMC_Rank", "LastPrice", "DayPctChange", "MarketCap"] @@ -62,7 +62,7 @@ def get_cmc_top_n(other_args: List[str]): if not ns_parser: return - cmc = CoinMarketCapAPI(API_CMC_KEY) + cmc = CoinMarketCapAPI(cfg.API_CMC_KEY) ratings = cmc.cryptocurrency_listings_latest().data symbol, rank, price, pchange1d, mkt_cap = [], [], [], [], [] From 8259c5a4688dd3ced573c5b6e554ada0df2f661a Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Mon, 3 May 2021 12:57:48 +0100 Subject: [PATCH 53/74] Update README.md --- gamestonk_terminal/discovery/README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/gamestonk_terminal/discovery/README.md b/gamestonk_terminal/discovery/README.md index 1159e591b56..b7b3ac0200a 100644 --- a/gamestonk_terminal/discovery/README.md +++ b/gamestonk_terminal/discovery/README.md @@ -2,6 +2,8 @@ This menu aims to discover new stocks, and the usage of the following commands along with an example will be exploited below. +* [ipo](#ipo) + * past and future IPOs [Finnhub] * [map](#map) * S&P500 index stocks map [Finviz] * [rtp_sectors](#rtp_sectors) @@ -42,6 +44,20 @@ This menu aims to discover new stocks, and the usage of the following commands a * dark pool tickers with growing activity [FINRA] +## ipo + +```shell +usage: ipo [-p PAST_DAYS] [-f FUTURE_DAYS] +``` + +Past and future IPOs. [Source: https://finnhub.io] + +* -p : Number of past days to look for IPOs. Default 0. +* -f : Number of future days to look for IPOs. Default 10. + +Captura de ecrรฃ 2021-05-03, ร s 12 54 44 + + ## map ```shell From efc8f4199f79fc7d4e1ab76fe89d8d0b09ee6913 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Mon, 3 May 2021 12:58:49 +0100 Subject: [PATCH 54/74] Update README.md --- gamestonk_terminal/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/gamestonk_terminal/README.md b/gamestonk_terminal/README.md index 5ac456b0be8..8c8462f4026 100644 --- a/gamestonk_terminal/README.md +++ b/gamestonk_terminal/README.md @@ -68,6 +68,7 @@ export -f GNUS_data -F csv ## Discover Stocks [ยป](discovery/README.md) Command|Description|Source ---|---|--- +`ipo` |past and future IPOs |[Finnhub](https://finnhub.io) `map` |S&P500 index stocks map |[Finviz](https://finviz.com) `rtp_sectors` |real-time performance sectors |[Alpha Vantage](www.alphavantage.co) `gainers` |show latest top gainers |[Yahoo Finance](https://finance.yahoo.com/) From 8ce04ae33b0776f19f6c45cab267dd0d754effd9 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Mon, 3 May 2021 13:00:06 +0100 Subject: [PATCH 55/74] Update ROADMAP.md --- ROADMAP.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ROADMAP.md b/ROADMAP.md index e2ddc7cf15d..d38442ec6ed 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -40,6 +40,7 @@ * [x] Add latest and trending news commands (@hinxx) - [PR #347](https://github.com/DidierRLopes/GamestonkTerminal/pull/347) * [x] Add top ratings updates (@hinxx) - [PR #357](https://github.com/DidierRLopes/GamestonkTerminal/pull/357) * [x] Add dark pool (ATS) data of tickers with growing trades activity (@didier) - [PR #372](https://github.com/DidierRLopes/GamestonkTerminal/pull/372) +* [x] Past and future IPOs calendar (@didier) - [PR #432](https://github.com/DidierRLopes/GamestonkTerminal/pull/432) **NEXT** From 9e3f772a31083c48928557ebe65173a3462710d3 Mon Sep 17 00:00:00 2001 From: Jamie Date: Mon, 3 May 2021 08:08:07 -0400 Subject: [PATCH 56/74] round 2 changes part 2 --- gamestonk_terminal/cryptocurrency/binance_model.py | 2 +- gamestonk_terminal/cryptocurrency/coinmarketcap_view.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gamestonk_terminal/cryptocurrency/binance_model.py b/gamestonk_terminal/cryptocurrency/binance_model.py index 03b5589214c..f1f6b179ada 100644 --- a/gamestonk_terminal/cryptocurrency/binance_model.py +++ b/gamestonk_terminal/cryptocurrency/binance_model.py @@ -245,7 +245,7 @@ def balance(coin: str): print("") amounts = [float(current_balance["free"]), float(current_balance["locked"])] total = np.sum(amounts) - df = pd.DataFrame(amounts).apply(lambda x: str(float(x)) + df = pd.DataFrame(amounts).apply(lambda x: str(float(x))) df.columns = ["Amount"] df.index = ["Free", "Locked"] df["Percent"] = df.div(df.sum(axis=0), axis=1).round(3) diff --git a/gamestonk_terminal/cryptocurrency/coinmarketcap_view.py b/gamestonk_terminal/cryptocurrency/coinmarketcap_view.py index 3fcd0d09dee..981b55ec092 100644 --- a/gamestonk_terminal/cryptocurrency/coinmarketcap_view.py +++ b/gamestonk_terminal/cryptocurrency/coinmarketcap_view.py @@ -6,7 +6,7 @@ from typing import List import pandas as pd from tabulate import tabulate from coinmarketcapapi import CoinMarketCapAPI -import gamestonk_terminal.config_terminal as cfg +import gamestonk_terminal.config_terminal as cfg from gamestonk_terminal.helper_funcs import parse_known_args_and_warn sort_options = ["Symbol", "CMC_Rank", "LastPrice", "DayPctChange", "MarketCap"] From 1f1ee6b0255048a0dc7193254f5350ae3f197480 Mon Sep 17 00:00:00 2001 From: didier Date: Mon, 3 May 2021 15:01:15 +0100 Subject: [PATCH 57/74] binance doc refactor --- .../cryptocurrency/binance_model.py | 23 ++++--------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/gamestonk_terminal/cryptocurrency/binance_model.py b/gamestonk_terminal/cryptocurrency/binance_model.py index f1f6b179ada..2d69f8d669f 100644 --- a/gamestonk_terminal/cryptocurrency/binance_model.py +++ b/gamestonk_terminal/cryptocurrency/binance_model.py @@ -27,8 +27,7 @@ def check_valid_binance_str(symbol: str) -> str: # pylint: disable=inconsistent-return-statements def select_binance_coin(other_args: List[str]): - """ - Define current_coin from binance + """Define current_coin from binance Parameters ---------- @@ -75,8 +74,7 @@ def select_binance_coin(other_args: List[str]): def order_book(coin: str, other_args: List[str]): - """ - Get order book for currency + """Get order book for currency Parameters ---------- @@ -84,10 +82,6 @@ def order_book(coin: str, other_args: List[str]): Coin to get order book for other_args: List[str] Argparse arguments - - Returns - ------- - """ limit_list = [5, 10, 20, 50, 100, 500, 1000, 5000] parser = argparse.ArgumentParser( @@ -120,8 +114,7 @@ def order_book(coin: str, other_args: List[str]): def show_candles(coin: str, other_args: List[str]): - """ - Get klines/candles for coin + """Get klines/candles for coin Parameters ---------- @@ -130,9 +123,6 @@ def show_candles(coin: str, other_args: List[str]): other_args: List[str] Argparse arguments - Returns - ------- - """ client = Client(cfg.API_BINANCE_KEY, cfg.API_BINANCE_SECRET) @@ -224,17 +214,12 @@ def show_candles(coin: str, other_args: List[str]): def balance(coin: str): - """ - Get account holdings for asset + """Get account holdings for asset Parameters ---------- coin: str Coin to get holdings of - - Returns - ------- - """ client = Client(cfg.API_BINANCE_KEY, cfg.API_BINANCE_SECRET) try: From b02dfd70515c8d1328e0672929a084de17efbbae Mon Sep 17 00:00:00 2001 From: didier Date: Mon, 3 May 2021 15:02:24 +0100 Subject: [PATCH 58/74] sentiment stats --- .../behavioural_analysis/ba_controller.py | 7 ++ .../behavioural_analysis/finnhub_view.py | 80 +++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 gamestonk_terminal/behavioural_analysis/finnhub_view.py diff --git a/gamestonk_terminal/behavioural_analysis/ba_controller.py b/gamestonk_terminal/behavioural_analysis/ba_controller.py index e8ae8f60aee..725bbdbfa55 100644 --- a/gamestonk_terminal/behavioural_analysis/ba_controller.py +++ b/gamestonk_terminal/behavioural_analysis/ba_controller.py @@ -14,6 +14,7 @@ from gamestonk_terminal.behavioural_analysis import ( reddit_view, stocktwits_view, finbrain_view, + finnhub_view, ) @@ -41,6 +42,7 @@ class BehaviouralAnalysisController: "queries", "rise", "finbrain", + "stats", ] def __init__(self, ticker: str, start: datetime): @@ -63,6 +65,7 @@ class BehaviouralAnalysisController: print(" quit quit to abandon program") print("") print(" finbrain sentiment from 15+ major news headlines") + print(" stats sentiment stats including comparison with sector") print("") print("Reddit:") print( @@ -218,6 +221,10 @@ class BehaviouralAnalysisController: """Process finbrain command""" finbrain_view.sentiment_analysis(other_args, self.ticker) + def call_stats(self, other_args: List[str]): + """Process stats command""" + finnhub_view.sentiment_stats(other_args, self.ticker) + def menu(ticker: str, start: datetime): """Behavioural Analysis Menu""" diff --git a/gamestonk_terminal/behavioural_analysis/finnhub_view.py b/gamestonk_terminal/behavioural_analysis/finnhub_view.py new file mode 100644 index 00000000000..d226f9e7445 --- /dev/null +++ b/gamestonk_terminal/behavioural_analysis/finnhub_view.py @@ -0,0 +1,80 @@ +import argparse +from typing import List, Dict +import requests +import pandas as pd +from gamestonk_terminal import config_terminal as cfg +from gamestonk_terminal.helper_funcs import ( + parse_known_args_and_warn, +) + + +def get_sentiment_stats(ticker: str) -> Dict: + """Get sentiment stats + + Parameters + ---------- + ticker : str + Ticker to get sentiment stats + + Returns + ------- + Dict + Get sentiment stats + """ + response = requests.get( + f"https://finnhub.io/api/v1/news-sentiment?symbol={ticker}&token={cfg.API_FINNHUB_KEY}" + ) + if response.status_code == 200: + return response.json() + + return {} + + +def sentiment_stats(other_args: List[str], ticker: str): + """Sentiment stats + + Parameters + ---------- + other_args : List[str] + Command line arguments to be processed with argparse + ticker : str + Ticker to get sentiment stats + """ + parser = argparse.ArgumentParser( + add_help=False, + prog="stats", + description=""" + Sentiment stats. [Source: https://finnhub.io] + """, + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + d_stats = get_sentiment_stats(ticker) + + if d_stats: + print(f"Buzz: {round(100*d_stats['buzz']['buzz'],2)} %") + print(f"News Score: {round(100*d_stats['companyNewsScore'],2)} %") + print("") + print(f"Articles Last Week: {d_stats['buzz']['articlesInLastWeek']}") + print(f"Articles Weekly Week: {d_stats['buzz']['weeklyAverage']}") + print("") + print(f"Bullish: {round(100*d_stats['sentiment']['bullishPercent'],2)} %") + print(f"Bearish: {round(100*d_stats['sentiment']['bearishPercent'],2)} %") + print("") + print( + f"Sector Average Bullish: {round(100*d_stats['sectorAverageBullishPercent'],2)} %" + ) + print( + f"Sector Average News Score: {round(100*d_stats['sectorAverageNewsScore'],2)} %" + ) + else: + print("No sentiment stats found.") + + print("") + + except Exception as e: + print(e, "\n") From 5cea53e46749c64b34d2ffe3baeda60cf6694cf3 Mon Sep 17 00:00:00 2001 From: didier Date: Mon, 3 May 2021 15:04:29 +0100 Subject: [PATCH 59/74] remove unused import --- gamestonk_terminal/behavioural_analysis/finnhub_view.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gamestonk_terminal/behavioural_analysis/finnhub_view.py b/gamestonk_terminal/behavioural_analysis/finnhub_view.py index d226f9e7445..9b6e76dd0df 100644 --- a/gamestonk_terminal/behavioural_analysis/finnhub_view.py +++ b/gamestonk_terminal/behavioural_analysis/finnhub_view.py @@ -1,7 +1,6 @@ import argparse from typing import List, Dict import requests -import pandas as pd from gamestonk_terminal import config_terminal as cfg from gamestonk_terminal.helper_funcs import ( parse_known_args_and_warn, From cca2f431db91f93992663991a9de3ba0ae5dd504 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Mon, 3 May 2021 15:10:50 +0100 Subject: [PATCH 60/74] Update README.md --- gamestonk_terminal/cryptocurrency/README.md | 31 +++++++++++++-------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/gamestonk_terminal/cryptocurrency/README.md b/gamestonk_terminal/cryptocurrency/README.md index 7b2dbabe1db..da8832da854 100644 --- a/gamestonk_terminal/cryptocurrency/README.md +++ b/gamestonk_terminal/cryptocurrency/README.md @@ -50,11 +50,15 @@ Plot the loaded crypto data. ![crypto_view](https://user-images.githubusercontent.com/25267873/115787452-20889a80-a3ba-11eb-9216-f7fd1ffc98cf.png) + ## trend ```` usage: trend ```` -Print the top 7 trending coins from coingecko +Print the top 7 trending coins from coingecko. + +Captura de ecrรฃ 2021-05-03, ร s 15 07 15 + # COINMARKETCAP @@ -73,44 +77,47 @@ This command displays the top n cryptocurrencies from coinmarketcap.com. crypto # BINANCE -## select +## select ```` usage: select [-c --coin] [-q --quote] ```` -Select a coin/currency to the current object. Note that in binance, the exchange "ticker" is usually COINCURR, such as BTCEUR -for BTC to EURO. There is no USD, but it uses a coin tethered to the USD (USDT), which is the default. Some symbols -are a combination of coins, as `ETHBTC` is a valid symbol +Select a coin/currency to the current object. Note that in binance, the exchange "ticker" is usually COINCURR, such as BTCEUR for BTC to EURO. There is no USD, but it uses a coin tethered to the USD (USDT), which is the default. Some symbols are a combination of coins, as `ETHBTC` is a valid symbol * -c/--coin Coin to load. If not specified, BTC will be loaded to prevent errors later. * -q/--quote Quote currency. Defaults to `USDT` (which is 1-to-1 with USD) -*Note that the usage `select btc` will also work, as it will add `-c` if not detected. ## book ```` usage: book -l/--limit ```` + Gets and shows the order book for the given coin/currency. Shows the cumulative amount of orders. * -l/--limit Number of orders to get on each side. One of [5,10,20,50,100,500,1000,5000]. Defaults to 100. +![orderbook](https://user-images.githubusercontent.com/25267873/116886857-84fcf280-ac21-11eb-9803-5baa8bceca05.png) + + ## candle ```` usage: candle [-i --interval] [-l --limit] ```` -Show candle chart for loaded coin/currency. (Terminal will quit if coin is not defined). -* -i/--interval. Interval for candles. One of [1day,3day,1hour,2hour,4hour,6hour,8hour,12hour,1week,1min,3min,5min,15min,30min,1month]. - Defaults to 1day. +Show candle chart for loaded coin/currency. + +* -i/--interval. Interval for candles. One of [1day,3day,1hour,2hour,4hour,6hour,8hour,12hour,1week,1min,3min,5min,15min,30min,1month]. Defaults to 1day. * -l/--limit. Number of candles to get. Defaults to 100. -Example output (using 3day interval and 50 candles) - -![im](https://user-images.githubusercontent.com/18151143/116797645-f455d380-aab5-11eb-8dbb-df257425302d.png) +![candle](https://user-images.githubusercontent.com/25267873/116886993-abbb2900-ac21-11eb-9ff8-b6a8131fdac5.png) + ## balance ```` usage: balance ```` + Shows the current holding balance in your account. + + From b8fa3470434edfbb59f280d84e3f49dd4543efd1 Mon Sep 17 00:00:00 2001 From: didier Date: Mon, 3 May 2021 15:20:51 +0100 Subject: [PATCH 61/74] fix candle/book exiting if coin wasnt selected --- .../cryptocurrency/binance_model.py | 30 ++++++++++++++----- .../cryptocurrency/crypto_controller.py | 6 ++-- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/gamestonk_terminal/cryptocurrency/binance_model.py b/gamestonk_terminal/cryptocurrency/binance_model.py index 2d69f8d669f..5304d527dc0 100644 --- a/gamestonk_terminal/cryptocurrency/binance_model.py +++ b/gamestonk_terminal/cryptocurrency/binance_model.py @@ -73,15 +73,17 @@ def select_binance_coin(other_args: List[str]): return None, None -def order_book(coin: str, other_args: List[str]): +def order_book(other_args: List[str], coin: str, currency: str): """Get order book for currency Parameters ---------- - coin: str - Coin to get order book for other_args: List[str] Argparse arguments + coin: str + Coin to get symbol of + currency : str + Currency against which to check symbol """ limit_list = [5, 10, 20, 50, 100, 500, 1000, 5000] parser = argparse.ArgumentParser( @@ -98,6 +100,12 @@ def order_book(coin: str, other_args: List[str]): ) try: + if not coin or not currency: + print("Coin needs to be selected prior to this command\n") + return + else: + coin += currency + ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return @@ -113,16 +121,17 @@ def order_book(coin: str, other_args: List[str]): print(e, "\n") -def show_candles(coin: str, other_args: List[str]): +def show_candles(other_args: List[str], coin: str, currency: str): """Get klines/candles for coin Parameters ---------- - coin: str - Coin to get symbol of other_args: List[str] Argparse arguments - + coin: str + Coin to get symbol of + currency : str + Currency against which to check symbol """ client = Client(cfg.API_BINANCE_KEY, cfg.API_BINANCE_SECRET) @@ -165,9 +174,16 @@ def show_candles(coin: str, other_args: List[str]): ) try: + if not coin or not currency: + print("Coin needs to be selected prior to this command\n") + return + else: + coin += currency + ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return + candles = client.get_klines( symbol=coin, interval=interval_map[ns_parser.interval], diff --git a/gamestonk_terminal/cryptocurrency/crypto_controller.py b/gamestonk_terminal/cryptocurrency/crypto_controller.py index 60a9359631c..958b345f5d5 100644 --- a/gamestonk_terminal/cryptocurrency/crypto_controller.py +++ b/gamestonk_terminal/cryptocurrency/crypto_controller.py @@ -115,13 +115,11 @@ class CryptoController: def call_book(self, other_args): """Process book command""" - binance_model.order_book(self.current_coin + self.current_currency, other_args) + binance_model.order_book(other_args, self.current_coin, self.current_currency) def call_candle(self, other_args): """Process candle command""" - binance_model.show_candles( - self.current_coin + self.current_currency, other_args - ) + binance_model.show_candles(other_args, self.current_coin, self.current_currency) def call_balance(self, _): """Process balance command""" From 2d438d956feb5a41ad0f5e94305d9021ca17f61e Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Mon, 3 May 2021 15:26:39 +0100 Subject: [PATCH 62/74] Update README.md --- gamestonk_terminal/behavioural_analysis/README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/gamestonk_terminal/behavioural_analysis/README.md b/gamestonk_terminal/behavioural_analysis/README.md index 18a7cda7b10..e8e14c1f25e 100644 --- a/gamestonk_terminal/behavioural_analysis/README.md +++ b/gamestonk_terminal/behavioural_analysis/README.md @@ -3,6 +3,9 @@ This menu aims to extrapolate behavioural analysis regarding a pre-loaded ticker, and the usage of the following commands along with an example will be exploited below. * [finbrain](#finbrain) + - sentiment from 15+ major news headlines + * [stats](#stats) + - sentiment stats including comparison with sector [REDDIT](#REDDIT) * [wsb](#wsb) @@ -54,6 +57,15 @@ FinBrain collects the news headlines from 15+ major financial news sources on a Captura de ecrรฃ 2021-03-18, ร s 12 53 31 +## stats +``` +usage: stats +``` +Sentiment stats which displays buzz, news score, articles last week, articles weekly average, bullish vs bearish percentages, sector average bullish percentage, and sector average news score. [Source: https://finnhub.io] + +Captura de ecrรฃ 2021-05-03, ร s 15 26 09 + + # REDDIT ## wsb From c6bd12c1bb5224520d28bc65fae051c34e43c01b Mon Sep 17 00:00:00 2001 From: didier Date: Mon, 3 May 2021 15:28:03 +0100 Subject: [PATCH 63/74] some fixes here and there --- gamestonk_terminal/behavioural_analysis/finnhub_view.py | 6 +++--- gamestonk_terminal/cryptocurrency/binance_model.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/gamestonk_terminal/behavioural_analysis/finnhub_view.py b/gamestonk_terminal/behavioural_analysis/finnhub_view.py index 9b6e76dd0df..99f4b383106 100644 --- a/gamestonk_terminal/behavioural_analysis/finnhub_view.py +++ b/gamestonk_terminal/behavioural_analysis/finnhub_view.py @@ -30,7 +30,7 @@ def get_sentiment_stats(ticker: str) -> Dict: def sentiment_stats(other_args: List[str], ticker: str): - """Sentiment stats + """Sentiment stats which displays buzz, news score, articles last week, articles weekly average, bullish vs bearish percentages, sector average bullish percentage, and sector average news score Parameters ---------- @@ -43,7 +43,7 @@ def sentiment_stats(other_args: List[str], ticker: str): add_help=False, prog="stats", description=""" - Sentiment stats. [Source: https://finnhub.io] + Sentiment stats which displays buzz, news score, articles last week, articles weekly average, bullish vs bearish percentages, sector average bullish percentage, and sector average news score. [Source: https://finnhub.io] """, ) @@ -59,7 +59,7 @@ def sentiment_stats(other_args: List[str], ticker: str): print(f"News Score: {round(100*d_stats['companyNewsScore'],2)} %") print("") print(f"Articles Last Week: {d_stats['buzz']['articlesInLastWeek']}") - print(f"Articles Weekly Week: {d_stats['buzz']['weeklyAverage']}") + print(f"Articles Weekly Average: {d_stats['buzz']['weeklyAverage']}") print("") print(f"Bullish: {round(100*d_stats['sentiment']['bullishPercent'],2)} %") print(f"Bearish: {round(100*d_stats['sentiment']['bearishPercent'],2)} %") diff --git a/gamestonk_terminal/cryptocurrency/binance_model.py b/gamestonk_terminal/cryptocurrency/binance_model.py index 5304d527dc0..7bfad6acd6f 100644 --- a/gamestonk_terminal/cryptocurrency/binance_model.py +++ b/gamestonk_terminal/cryptocurrency/binance_model.py @@ -103,8 +103,8 @@ def order_book(other_args: List[str], coin: str, currency: str): if not coin or not currency: print("Coin needs to be selected prior to this command\n") return - else: - coin += currency + + coin += currency ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: @@ -177,8 +177,8 @@ def show_candles(other_args: List[str], coin: str, currency: str): if not coin or not currency: print("Coin needs to be selected prior to this command\n") return - else: - coin += currency + + coin += currency ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: From 3429eec4547986670c64cf55c334dd2e0c85f3aa Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Mon, 3 May 2021 15:28:56 +0100 Subject: [PATCH 64/74] Update README.md --- gamestonk_terminal/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/gamestonk_terminal/README.md b/gamestonk_terminal/README.md index 32a42a6e67a..77330111111 100644 --- a/gamestonk_terminal/README.md +++ b/gamestonk_terminal/README.md @@ -95,6 +95,7 @@ Command|Description ----|---- [FinBrain](https://finbrain.tech)| `finbrain` |sentiment from 15+ major news headlines +`stats` |sentiment stats including comparison with sector [Reddit](https://reddit.com)| `wsb` |show what WSB gang is up to in subreddit wallstreetbets `watchlist` |show other users watchlist From c55c90cd56887880bddff6ed91c12857759dd301 Mon Sep 17 00:00:00 2001 From: didier Date: Mon, 3 May 2021 15:30:35 +0100 Subject: [PATCH 65/74] fix line too long --- gamestonk_terminal/behavioural_analysis/finnhub_view.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/gamestonk_terminal/behavioural_analysis/finnhub_view.py b/gamestonk_terminal/behavioural_analysis/finnhub_view.py index 99f4b383106..f719184d096 100644 --- a/gamestonk_terminal/behavioural_analysis/finnhub_view.py +++ b/gamestonk_terminal/behavioural_analysis/finnhub_view.py @@ -30,7 +30,8 @@ def get_sentiment_stats(ticker: str) -> Dict: def sentiment_stats(other_args: List[str], ticker: str): - """Sentiment stats which displays buzz, news score, articles last week, articles weekly average, bullish vs bearish percentages, sector average bullish percentage, and sector average news score + """Sentiment stats which displays buzz, news score, articles last week, articles weekly average, + bullish vs bearish percentages, sector average bullish percentage, and sector average news score Parameters ---------- @@ -43,7 +44,9 @@ def sentiment_stats(other_args: List[str], ticker: str): add_help=False, prog="stats", description=""" - Sentiment stats which displays buzz, news score, articles last week, articles weekly average, bullish vs bearish percentages, sector average bullish percentage, and sector average news score. [Source: https://finnhub.io] + Sentiment stats which displays buzz, news score, articles last week, articles weekly average, + bullish vs bearish percentages, sector average bullish percentage, and sector average news score. + [Source: https://finnhub.io] """, ) From 86345ecc5fe579960ede2bc50459765950bef619 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Mon, 3 May 2021 15:32:19 +0100 Subject: [PATCH 66/74] Add files via upload --- images/gst_logo_lockup_rGreen_with_letters.png | Bin 0 -> 86589 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 images/gst_logo_lockup_rGreen_with_letters.png diff --git a/images/gst_logo_lockup_rGreen_with_letters.png b/images/gst_logo_lockup_rGreen_with_letters.png new file mode 100644 index 0000000000000000000000000000000000000000..a3e6d6782f64a3da237c4c7d6582124ee7a41bad GIT binary patch literal 86589 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êr41xwExC~@71Y7qOWN4 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 ve*lZ|za=3{#Q%ih_qq805{87_$G@1 Date: Mon, 3 May 2021 15:35:42 +0100 Subject: [PATCH 67/74] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f35535093b3..3b4ae343c73 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@

Gamestonk Terminal ๐Ÿš€

From 233ca58c0dc0663017a08969ecd172421cbd04ba Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Mon, 3 May 2021 15:37:44 +0100 Subject: [PATCH 68/74] Update ROADMAP.md --- ROADMAP.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ROADMAP.md b/ROADMAP.md index b7c80d2c599..638ca8940ba 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -49,6 +49,7 @@ ## Behavioral Analysis * [x] Add FinBrain sentiment from news (@didier) - [PR #223](https://github.com/DidierRLopes/GamestonkTerminal/pull/223) +* [x] Sentiment stats from Finnhub (@didier) - [PR #433](https://github.com/DidierRLopes/GamestonkTerminal/pull/433) **NEXT** * [ ] Implement a better Twitter client (@aia) From 70de28589a1b00bf6b6712ba228797257eca0015 Mon Sep 17 00:00:00 2001 From: Ivar Stangeby Date: Mon, 3 May 2021 18:44:08 +0200 Subject: [PATCH 69/74] Added mypy --- .pre-commit-config.yaml | 27 +++++++++++++++++++++++++++ README.md | 17 ++++------------- 2 files changed, 31 insertions(+), 13 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000000..807a2f0d709 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,27 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.4.0 + hooks: + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/psf/black + rev: 21.4b2 + hooks: + - id: black + - repo: https://github.com/pycqa/flake8 + rev: 3.9.1 + hooks: + - id: flake8 + entry: flake8 --max-line-length=122 --ignore=E203,W503 + - repo: https://github.com/codespell-project/codespell + rev: v2.0.0 + hooks: + - id: codespell + entry: codespell + args: [ '--ignore-words-list="mape","ba","hist"', '--quiet-level=2', '--skip=".git,*.yaml,*.json,./tests"' ] + - repo: https://github.com/pre-commit/mirrors-mypy + rev: 'v0.812' + hooks: + - id: mypy + args: [--ignore-missing-imports] diff --git a/README.md b/README.md index 3b4ae343c73..b96f9fc3dcc 100644 --- a/README.md +++ b/README.md @@ -88,8 +88,6 @@ As a modern Python-based environment, GamestonkTerminal opens access to numerous ## Getting Started ### Install -If you'd like to see a video recording of the installation process, @JohnnyDankseed has made one available [here](https://www.twitch.tv/videos/1008414642). - This project was originally written and tested with Python 3.6.8. It should now support Python 3.6, 3.7, and 3.8. Our current recommendation is to use this project with Anaconda's Python distribution - either full [__Anaconda3 Latest__](https://repo.anaconda.com/archive/) or [__Miniconda3 Latest__](https://repo.anaconda.com/archive/). Several features in this project utilize Machine Learning. Machine Learning Python dependencies are optional. If you decided to add Machine Learning features at a later point, you will likely have better user experience with Anaconda's Python distribution. @@ -145,8 +143,6 @@ conda activate gst ``` *The `conda deactivate` -> `conda activate` in the middle is on purpose, this is sometimes required to avoid issues with poetry* -7.2. If on Windows, install/update Microsoft C++ Build Tools from here: https://visualstudio.microsoft.com/visual-cpp-build-tools/ - 8. Install poetry dependencies ``` poetry install @@ -160,7 +156,7 @@ python terminal.py ``` 10. (Windows - Optional) Speeding up opening process in the future -After you've installed Gamestonk Terminal, you'll find a file named "Gamestonk Terminal.bat". You can use this file to open Gamestonk Terminal quicker. This file can be moved to your desktop if you'd like. If you run into issues while trying to run the batch file. If you run into issues with the batch files, edit the file and check to see if the directories match up. This file assumes you used the default directories when installing. +After you've installed Gamestonk Terminal, you'll find a file named "Gamestonk Terminal.bat". You can use this file to open Gamestonk Terminal quicker. This file can be moved to your desktop if you'd like. If you run into issues while trying to run the batch file. If you run into issues with the batch files, edit the file and check to see if the directories match up. This file assumes you used the default directories when installing. **NOTE:** When you close the terminal and re-open it, the only command you need to re-call is `conda activate gst` before you call `python terminal.py` again. @@ -228,9 +224,6 @@ These are the ones where a key is necessary: * News API: https://newsapi.org * Tradier: https://developer.tradier.com/getting_started * Oanda API: https://developer.oanda.com - * CoinMarketCap API: https://coinmarketcap.com/api/ - * Finhub API: https://finnhub.io - * Binance: https://binance.us (US) / https://binance.com (Outside US) When these are obtained, don't forget to update [config_terminal.py](/gamestonk_terminal/config_terminal.py). @@ -299,9 +292,9 @@ Recommended if you bought the dip, and the share price keeps dipping. You may as 1. Fork the Project 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) -4. Appease the linters and commit again if needed - 1. Install and run `black` for every change you've made - 2. Install and run `flake8` for every change you've made. `flake8 . --count --ignore=E203,W503 --max-line-length=122 --show-source --statistics` +4. Install the pre-commit hooks by running: + ```pre-commit install```. + Any time you commit a change, linters will be run automatically. On changes, you will have to re-commit. 5. Push to your Branch (`git push origin feature/AmazingFeature`) 6. Open a Pull Request @@ -333,8 +326,6 @@ Distributed under the MIT License. See [LICENSE](https://github.com/DidierRLopes "A few things I am not. I am not a cat. I am not an institutional investor, nor am I a hedge fund. I do not have clients and I do not provide personalized investment advice for fees or commissions." DFV -Trading in financial instruments involves high risks including the risk of losing some, or all, of your investment amount, and may not be suitable for all investors. Before deciding to trade in financial instrument you should be fully informed of the risks and costs associated with trading the financial markets, carefully consider your investment objectives, level of experience, and risk appetite, and seek professional advice where needed. The data contained in GST is not necessarily accurate. GST and any provider of the data contained in this website will not accept liability for any loss or damage as a result of your trading, or your reliance on the information displayed. - ## Contacts [Didier Rodrigues Lopes](https://www.linkedin.com/in/didier-lopes/) - dro.lopes@campus.fct.unl.pt From 9cc09158712eb99d4efc039b0a3f1a9374cb21b4 Mon Sep 17 00:00:00 2001 From: Ivar Stangeby Date: Mon, 3 May 2021 18:50:01 +0200 Subject: [PATCH 70/74] Added pylint --- .pre-commit-config.yaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 807a2f0d709..27bb06d6fac 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,4 +24,9 @@ repos: rev: 'v0.812' hooks: - id: mypy - args: [--ignore-missing-imports] + args: [ --ignore-missing-imports ] + - repo: https://github.com/pre-commit/mirrors-pylint + rev: 'v3.0.0a3' + hooks: + - id: pylint + entry: pylint terminal.py gamestonk_terminal tests From 43806015d5588dc651834b26af76ce9d6b2bc0dc Mon Sep 17 00:00:00 2001 From: Ivar Stangeby Date: Mon, 3 May 2021 18:56:30 +0200 Subject: [PATCH 71/74] Added detect-secrets as pre-commit hook. Scanned the repository for a secrets baseline. --- .pre-commit-config.yaml | 8 +++- .secrets.baseline | 103 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 .secrets.baseline diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 27bb06d6fac..29a1f38c2d3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,7 +26,13 @@ repos: - id: mypy args: [ --ignore-missing-imports ] - repo: https://github.com/pre-commit/mirrors-pylint - rev: 'v3.0.0a3' + rev: 'v3.0.0a3' hooks: - id: pylint entry: pylint terminal.py gamestonk_terminal tests + - repo: https://github.com/Yelp/detect-secrets + rev: v1.1.0 + hooks: + - id: detect-secrets + args: [ '--baseline', '.secrets.baseline' ] + exclude: package.lock.json diff --git a/.secrets.baseline b/.secrets.baseline new file mode 100644 index 00000000000..10a1fe2c470 --- /dev/null +++ b/.secrets.baseline @@ -0,0 +1,103 @@ +{ + "version": "1.1.0", + "plugins_used": [ + { + "name": "ArtifactoryDetector" + }, + { + "name": "AWSKeyDetector" + }, + { + "name": "AzureStorageKeyDetector" + }, + { + "name": "Base64HighEntropyString", + "limit": 4.5 + }, + { + "name": "BasicAuthDetector" + }, + { + "name": "CloudantDetector" + }, + { + "name": "HexHighEntropyString", + "limit": 3.0 + }, + { + "name": "IbmCloudIamDetector" + }, + { + "name": "IbmCosHmacDetector" + }, + { + "name": "JwtTokenDetector" + }, + { + "name": "KeywordDetector", + "keyword_exclude": "" + }, + { + "name": "MailchimpDetector" + }, + { + "name": "NpmDetector" + }, + { + "name": "PrivateKeyDetector" + }, + { + "name": "SlackDetector" + }, + { + "name": "SoftlayerDetector" + }, + { + "name": "SquareOAuthDetector" + }, + { + "name": "StripeDetector" + }, + { + "name": "TwilioKeyDetector" + } + ], + "filters_used": [ + { + "path": "detect_secrets.filters.allowlist.is_line_allowlisted" + }, + { + "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", + "min_level": 2 + }, + { + "path": "detect_secrets.filters.heuristic.is_indirect_reference" + }, + { + "path": "detect_secrets.filters.heuristic.is_likely_id_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_lock_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_potential_uuid" + }, + { + "path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign" + }, + { + "path": "detect_secrets.filters.heuristic.is_sequential_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_swagger_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_templated_secret" + } + ], + "results": {}, + "generated_at": "2021-05-03T16:53:30Z" +} From 376d7f39b2f62e82bc1155e3de83deb2ac65a2c4 Mon Sep 17 00:00:00 2001 From: Ivar Stangeby Date: Mon, 3 May 2021 19:10:30 +0200 Subject: [PATCH 72/74] Fixed README.md --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index b96f9fc3dcc..496050b2059 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,8 @@ As a modern Python-based environment, GamestonkTerminal opens access to numerous ## Getting Started ### Install +If you'd like to see a video recording of the installation process, @JohnnyDankseed has made one available [here](https://www.twitch.tv/videos/1008414642). + This project was originally written and tested with Python 3.6.8. It should now support Python 3.6, 3.7, and 3.8. Our current recommendation is to use this project with Anaconda's Python distribution - either full [__Anaconda3 Latest__](https://repo.anaconda.com/archive/) or [__Miniconda3 Latest__](https://repo.anaconda.com/archive/). Several features in this project utilize Machine Learning. Machine Learning Python dependencies are optional. If you decided to add Machine Learning features at a later point, you will likely have better user experience with Anaconda's Python distribution. @@ -143,6 +145,8 @@ conda activate gst ``` *The `conda deactivate` -> `conda activate` in the middle is on purpose, this is sometimes required to avoid issues with poetry* +7.2. If on Windows, install/update Microsoft C++ Build Tools from here: https://visualstudio.microsoft.com/visual-cpp-build-tools/ + 8. Install poetry dependencies ``` poetry install @@ -224,6 +228,9 @@ These are the ones where a key is necessary: * News API: https://newsapi.org * Tradier: https://developer.tradier.com/getting_started * Oanda API: https://developer.oanda.com + * CoinMarketCap API: https://coinmarketcap.com/api/ + * Finhub API: https://finnhub.io + * Binance: https://binance.us (US) / https://binance.com (Outside US) When these are obtained, don't forget to update [config_terminal.py](/gamestonk_terminal/config_terminal.py). @@ -326,6 +333,8 @@ Distributed under the MIT License. See [LICENSE](https://github.com/DidierRLopes "A few things I am not. I am not a cat. I am not an institutional investor, nor am I a hedge fund. I do not have clients and I do not provide personalized investment advice for fees or commissions." DFV +Trading in financial instruments involves high risks including the risk of losing some, or all, of your investment amount, and may not be suitable for all investors. Before deciding to trade in financial instrument you should be fully informed of the risks and costs associated with trading the financial markets, carefully consider your investment objectives, level of experience, and risk appetite, and seek professional advice where needed. The data contained in GST is not necessarily accurate. GST and any provider of the data contained in this website will not accept liability for any loss or damage as a result of your trading, or your reliance on the information displayed. + ## Contacts [Didier Rodrigues Lopes](https://www.linkedin.com/in/didier-lopes/) - dro.lopes@campus.fct.unl.pt From eaf61e0392668f94d1a4906d0bdba1bc3e9357e7 Mon Sep 17 00:00:00 2001 From: jose-donato <43375532+jose-donato@users.noreply.github.com> Date: Tue, 4 May 2021 20:29:55 +0100 Subject: [PATCH 73/74] Added twitter shield to README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 496050b2059..5d6ddd3a4e6 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) ![Discord Shield](https://discordapp.com/api/guilds/831165782750789672/widget.png?style=shield) - +[![Twitter](https://img.shields.io/twitter/url/https/twitter.com/gamestonkt.svg?style=social&label=Follow%20%40gamestonkt)](https://twitter.com/gamestonkt)
From b33975c551adb330ed623beb0a5b1009f336a859 Mon Sep 17 00:00:00 2001 From: DidierRLopes Date: Wed, 5 May 2021 22:33:49 +0100 Subject: [PATCH 74/74] Update fred_view.py --- gamestonk_terminal/econ/fred_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gamestonk_terminal/econ/fred_view.py b/gamestonk_terminal/econ/fred_view.py index 59ccd2c2682..88ba23b42e8 100644 --- a/gamestonk_terminal/econ/fred_view.py +++ b/gamestonk_terminal/econ/fred_view.py @@ -58,7 +58,7 @@ def display_fred(other_args: List[str], choice: str): "-i", "--id", dest="series_id", - required=bool(choice), + required=not bool(choice), type=str, default=choice.upper(), help="FRED Series ID from https://fred.stlouisfed.org",

- Logo + Logo