From 0b554c6893aa4e805c27b606f7de7ae6c0eb454d Mon Sep 17 00:00:00 2001 From: jp Date: Mon, 13 Sep 2021 20:07:00 +0200 Subject: [PATCH] Coinbase pro api (#743) * Add separate view for finbrain for crypto curreny sentiment analysis * Move json with symbols to separate directory, little refactoring, update readme * Add screenshot of finbrain for PolkaDot * Coinbase menu added * Add more methods for coinbase * Add coinbase views, coinbase models. Add coinbase to load, chart, find commands. Add coinbase to controller * Add auth client for coinbase pro endpoints that needs auth * Add more views for coinbase to crypto menu * Add some print tests for Coinbase view * Add 2 more tests for coinbase view * Update README with Coinbase API keys * Cleaning empty lines, spaces etc. * Use os.path.join in imports, change column names in one normalized df * Move Coinbase to portfolio brokers. Adjust tests, and imports * precommit config return to prev ver * Fix coinbase accounts command, del column filter for deposits --- .pre-commit-config.yaml | 2 +- README.md | 2 + gamestonk_terminal/config_terminal.py | 5 + .../cryptocurrency/coinbase_helpers.py | 167 ++++++++ .../cryptocurrency/crypto_controller.py | 177 ++++++++- .../cryptocurrency/cryptocurrency_helpers.py | 291 ++++++++++---- .../data/coinbase_gecko_map.json | 101 +++++ .../discovery/pycoingecko_model.py | 33 +- .../due_diligence/binance_model.py | 35 -- .../due_diligence/binance_view.py | 4 +- .../due_diligence/coinbase_model.py | 193 +++++++++ .../due_diligence/coinbase_view.py | 152 ++++++++ .../due_diligence/dd_controller.py | 334 +++++++++++++--- .../cryptocurrency/overview/coinbase_model.py | 35 ++ .../cryptocurrency/overview/coinbase_view.py | 60 +++ .../overview/overview_controller.py | 73 ++++ .../portfolio/brokers/bro_controller.py | 12 + .../portfolio/brokers/coinbase/__init__.py | 0 .../brokers/coinbase/coinbase_controller.py | 368 ++++++++++++++++++ .../brokers/coinbase/coinbase_model.py | 177 +++++++++ .../brokers/coinbase/coinbase_view.py | 198 ++++++++++ .../test_cryptocurrency/test_coinbase_view.py | 239 ++++++++++++ 22 files changed, 2436 insertions(+), 222 deletions(-) create mode 100644 gamestonk_terminal/cryptocurrency/coinbase_helpers.py create mode 100644 gamestonk_terminal/cryptocurrency/data/coinbase_gecko_map.json create mode 100644 gamestonk_terminal/cryptocurrency/due_diligence/coinbase_model.py create mode 100644 gamestonk_terminal/cryptocurrency/due_diligence/coinbase_view.py create mode 100644 gamestonk_terminal/cryptocurrency/overview/coinbase_model.py create mode 100644 gamestonk_terminal/cryptocurrency/overview/coinbase_view.py create mode 100644 gamestonk_terminal/portfolio/brokers/coinbase/__init__.py create mode 100644 gamestonk_terminal/portfolio/brokers/coinbase/coinbase_controller.py create mode 100644 gamestonk_terminal/portfolio/brokers/coinbase/coinbase_model.py create mode 100644 gamestonk_terminal/portfolio/brokers/coinbase/coinbase_view.py create mode 100644 tests/test_cryptocurrency/test_coinbase_view.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ce0dd7dcbc7..132d4a82a05 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,7 +26,7 @@ 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.0a4' hooks: diff --git a/README.md b/README.md index 63ea3173cce..2461ee21146 100644 --- a/README.md +++ b/README.md @@ -266,6 +266,7 @@ These are the ones where a key is necessary: * SentimentInvestor: https://sentimentinvestor.com * Tradier: https://developer.tradier.com/getting_started * Twitter: https://developer.twitter.com +* Coinbase Pro API: https://docs.pro.coinbase.com/ When these are obtained, don't forget to update [config_terminal.py](/gamestonk_terminal/config_terminal.py). @@ -288,6 +289,7 @@ Alternatively, you can also set them to the following environment variables: | [SentimentInvestor](https://sentimentinvestor.com) | GT_API_SENTIMENTINVESTOR_TOKEN
GT_API_SENTIMENTINVESTOR_KEY | | [Tradier](https://developer.tradier.com) | GT_TRADIER_TOKEN | | [Twitter](https://developer.twitter.com) | GT_API_TWITTER_KEY
GT_API_TWITTER_SECRET_KEY
GT_API_TWITTER_BEARER_TOKEN | +| [Coinbase](https://docs.pro.coinbase.com/) | GT_API_COINBASE_KEY
GT_API_COINBASE_SECRET
GT_API_COINBASE_PASS_PHRASE | Example: ``` diff --git a/gamestonk_terminal/config_terminal.py b/gamestonk_terminal/config_terminal.py index 91cef9f0e05..b81493537f8 100644 --- a/gamestonk_terminal/config_terminal.py +++ b/gamestonk_terminal/config_terminal.py @@ -85,3 +85,8 @@ API_SENTIMENTINVESTOR_KEY = os.getenv("GT_API_SENTIMENTINVESTOR_KEY") or "REPLAC API_SENTIMENTINVESTOR_TOKEN = ( os.getenv("GT_API_SENTIMENTINVESTOR_TOKEN") or "REPLACE_ME" ) + +# https://pro.coinbase.com/profile/api +API_COINBASE_KEY = os.getenv("GT_API_COINBASE_KEY") or "REPLACE_ME" +API_COINBASE_SECRET = os.getenv("GT_API_COINBASE_SECRET") or "REPLACE_ME" +API_COINBASE_PASS_PHRASE = os.getenv("GT_API_COINBASE_PASS_PHRASE") or "REPLACE_ME" diff --git a/gamestonk_terminal/cryptocurrency/coinbase_helpers.py b/gamestonk_terminal/cryptocurrency/coinbase_helpers.py new file mode 100644 index 00000000000..99ca482b5eb --- /dev/null +++ b/gamestonk_terminal/cryptocurrency/coinbase_helpers.py @@ -0,0 +1,167 @@ +"""Coinbase helpers model""" +__docformat__ = "numpy" + +import argparse +import binascii + +from typing import Optional, Any, Union +import hmac +import hashlib +import time +import base64 +import requests +from requests.auth import AuthBase +import gamestonk_terminal.config_terminal as cfg + + +class CoinbaseProAuth(AuthBase): + """Authorize CoinbasePro requests. Source: https://docs.pro.coinbase.com/?python#signing-a-message""" + + def __init__(self, api_key, secret_key, passphrase): + self.api_key = api_key + self.secret_key = secret_key + self.passphrase = passphrase + + def __call__(self, request): + timestamp = str(time.time()) + message = timestamp + request.method + request.path_url + (request.body or "") + message = message.encode("ascii") + + try: + hmac_key = base64.b64decode(self.secret_key) + signature = hmac.new(hmac_key, message, hashlib.sha256) + signature_b64 = base64.b64encode(signature.digest()) + except binascii.Error: + signature_b64 = "" + + request.headers.update( + { + "CB-ACCESS-SIGN": signature_b64, + "CB-ACCESS-TIMESTAMP": timestamp, + "CB-ACCESS-KEY": self.api_key, + "CB-ACCESS-PASSPHRASE": self.passphrase, + "Content-Type": "application/json", + } + ) + return request + + +class CoinbaseRequestException(Exception): + """Coinbase Request Exception object""" + + def __init__(self, message: str): + super().__init__(message) + self.message = message + + def __str__(self) -> str: + return "CoinbaseRequestException: %s" % self.message + + +class CoinbaseApiException(Exception): + """Coinbase API Exception object""" + + def __init__(self, message: str): + super().__init__(message) + self.message = message + + def __str__(self) -> str: + return "CoinbaseApiException: %s" % self.message + + +def check_validity_of_product(product_id: str) -> str: + """Helper method that checks if provided product_id exists. It's a pair of coins in format COIN-COIN. + If product exists it return it, in other case it raise an error. [Source: Coinbase] + + Parameters + ---------- + product_id: str + Trading pair of coins on Coinbase e.g ETH-USDT or UNI-ETH + + Returns + ------- + str + pair of coins in format COIN-COIN + """ + + products = [pair["id"] for pair in make_coinbase_request("/products")] + if product_id.upper() not in products: + raise argparse.ArgumentTypeError( + f"You provided wrong pair of coins {product_id}. " + f"It should be provided as a pair in format COIN-COIN e.g UNI-USD" + ) + return product_id.upper() + + +def make_coinbase_request( + endpoint, params: Optional[dict] = None, auth: Optional[Any] = None +) -> dict: + """Request handler for Coinbase Pro Api. Prepare a request url, params and payload and call endpoint. + [Source: Coinbase] + + Parameters + ---------- + endpoint: str + Endpoint path e.g /products + params: dict + Parameter dedicated for given endpoint + auth: any + Api credentials for purpose of using endpoints that needs authentication + + Returns + ------- + dict + response from Coinbase Pro Api + """ + + url = "https://api.pro.coinbase.com" + response = requests.get(url + endpoint, params=params, auth=auth) + + if not 200 <= response.status_code < 300: + raise CoinbaseApiException("Invalid Authentication: %s" % response.text) + try: + return response.json() + except ValueError as e: + raise CoinbaseRequestException("Invalid Response: %s" % response.text) from e + + +def _get_account_coin_dict() -> dict: + """Helper method that returns dictionary with all symbols and account ids in dictionary format. [Source: Coinbase] + + Returns + ------- + dict: + Your accounts in coinbase + {'1INCH': '0c29b708-d73b-4e1c-a58c-9c261cb4bedb', 'AAVE': '0712af66-c069-45b5-84ae-7b2347c2fd24', ..} + + """ + auth = CoinbaseProAuth( + cfg.API_COINBASE_KEY, cfg.API_COINBASE_SECRET, cfg.API_COINBASE_PASS_PHRASE + ) + accounts = make_coinbase_request("/accounts", auth=auth) + return {acc["currency"]: acc["id"] for acc in accounts} + + +def _check_account_validity(account: str) -> Union[str, Any]: + """Helper methods that checks if given account exists. [Source: Coinbase] + + Parameters + ---------- + account: str + coin or account id + + Returns + ------- + Union[str, Any] + Your account id or None + """ + + accounts = _get_account_coin_dict() + + if account in list(accounts.keys()): + return accounts[account] + + if account in list(accounts.values()): + return account + + print("Wrong account id or coin symbol") + return None diff --git a/gamestonk_terminal/cryptocurrency/crypto_controller.py b/gamestonk_terminal/cryptocurrency/crypto_controller.py index ec7c58d492b..7e7cb4ba3a1 100644 --- a/gamestonk_terminal/cryptocurrency/crypto_controller.py +++ b/gamestonk_terminal/cryptocurrency/crypto_controller.py @@ -38,10 +38,9 @@ from gamestonk_terminal.cryptocurrency.cryptocurrency_helpers import ( plot_chart, ) from gamestonk_terminal.cryptocurrency.report import report_controller +from gamestonk_terminal.cryptocurrency.due_diligence import binance_model +from gamestonk_terminal.cryptocurrency.due_diligence import coinbase_model from gamestonk_terminal.cryptocurrency.onchain import onchain_controller -from gamestonk_terminal.cryptocurrency.due_diligence.binance_model import ( - show_available_pairs_for_given_symbol, -) import gamestonk_terminal.config_terminal as cfg @@ -67,6 +66,7 @@ class CryptoController: "bin": "Binance", "cg": "CoinGecko", "cp": "CoinPaprika", + "cb": "Coinbase", } DD_VIEWS_MAPPING = { @@ -178,7 +178,7 @@ Note: Some of CoinGecko commands can fail. Team is working on fix. formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="load", description="Load crypto currency to perform analysis on. " - "Available data sources are CoinGecko, CoinPaprika, and Binance" + "Available data sources are CoinGecko, CoinPaprika, Binance, Coinbase" "By default main source used for analysis is CoinGecko (cg). To change it use --source flag", ) @@ -196,7 +196,7 @@ Note: Some of CoinGecko commands can fail. Team is working on fix. "--source", help="Source of data", dest="source", - choices=("cp", "cg", "bin"), + choices=("cp", "cg", "bin", "cb"), default="cg", required=False, ) @@ -236,11 +236,8 @@ Note: Some of CoinGecko commands can fail. Team is working on fix. add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, prog="chart", - description="""Loads data for technical analysis. You can specify currency vs which you want - to show chart and also number of days to get data for. - By default currency: usd and days: 30. - E.g. if you loaded in previous step Ethereum and you want to see it's price vs btc - in last 90 days range use `ta --vs btc --days 90`""", + description="""Display chart for loaded coin. You can specify currency vs which you want + to show chart and also number of days to get data for.""", ) if self.source == "cp": @@ -295,7 +292,9 @@ Note: Some of CoinGecko commands can fail. Team is working on fix. "1month": client.KLINE_INTERVAL_1MONTH, } - _, quotes = show_available_pairs_for_given_symbol(self.current_coin) + _, quotes = binance_model.show_available_pairs_for_given_symbol( + self.current_coin + ) parser.add_argument( "--vs", @@ -325,13 +324,61 @@ Note: Some of CoinGecko commands can fail. Team is working on fix. type=check_positive, ) + if self.source == "cb": + interval_map = { + "1min": 60, + "5min": 300, + "15min": 900, + "1hour": 3600, + "6hour": 21600, + "24hour": 86400, + "1day": 86400, + } + + _, quotes = coinbase_model.show_available_pairs_for_given_symbol( + self.current_coin + ) + if len(quotes) < 0: + print( + f"Couldn't find any quoted coins for provided symbol {self.current_coin}" + ) + return + + parser.add_argument( + "--vs", + help="Quote currency (what to view coin vs)", + dest="vs", + type=str, + default="USDT" if "USDT" in quotes else quotes[0], + choices=quotes, + ) + + parser.add_argument( + "-i", + "--interval", + help="Interval to get data", + choices=list(interval_map.keys()), + dest="interval", + default="1day", + type=str, + ) + + parser.add_argument( + "-l", + "--limit", + dest="limit", + default=100, + help="Number to get", + type=check_positive, + ) + try: ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return - if self.source == "bin": + if self.source in ["bin", "cb"]: limit = ns_parser.limit interval = ns_parser.interval days = 0 @@ -424,7 +471,9 @@ Note: Some of CoinGecko commands can fail. Team is working on fix. "1month": client.KLINE_INTERVAL_1MONTH, } - _, quotes = show_available_pairs_for_given_symbol(self.current_coin) + _, quotes = binance_model.show_available_pairs_for_given_symbol( + self.current_coin + ) parser.add_argument( "--vs", help="Quote currency (what to view coin vs)", @@ -453,13 +502,109 @@ Note: Some of CoinGecko commands can fail. Team is working on fix. type=check_positive, ) + if self.source == "cb": + interval_map = { + "1min": 60, + "5min": 300, + "15min": 900, + "1hour": 3600, + "6hour": 21600, + "24hour": 86400, + "1day": 86400, + } + + _, quotes = coinbase_model.show_available_pairs_for_given_symbol( + self.current_coin + ) + if len(quotes) < 0: + print( + f"Couldn't find any quoted coins for provided symbol {self.current_coin}" + ) + return + + parser.add_argument( + "--vs", + help="Quote currency (what to view coin vs)", + dest="vs", + type=str, + default="USDT" if "USDT" in quotes else quotes[0], + choices=quotes, + ) + + parser.add_argument( + "-i", + "--interval", + help="Interval to get data", + choices=list(interval_map.keys()), + dest="interval", + default="1day", + type=str, + ) + + parser.add_argument( + "-l", + "--limit", + dest="limit", + default=100, + help="Number to get", + type=check_positive, + ) + + if self.source == "cb": + interval_map = { + "1min": 60, + "5min": 300, + "15min": 900, + "1hour": 3600, + "6hour": 21600, + "24hour": 86400, + "1day": 86400, + } + + _, quotes = coinbase_model.show_available_pairs_for_given_symbol( + self.current_coin + ) + if len(quotes) < 0: + print( + f"Couldn't find any quoted coins for provided symbol {self.current_coin}" + ) + return + + parser.add_argument( + "--vs", + help="Quote currency (what to view coin vs)", + dest="vs", + type=str, + default="USDT" if "USDT" in quotes else quotes[0], + choices=quotes, + ) + + parser.add_argument( + "-i", + "--interval", + help="Interval to get data", + choices=list(interval_map.keys()), + dest="interval", + default="1day", + type=str, + ) + + parser.add_argument( + "-l", + "--limit", + dest="limit", + default=100, + help="Number to get", + type=check_positive, + ) + try: ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return - if self.source == "bin": + if self.source in ["bin", "cb"]: limit = ns_parser.limit interval = ns_parser.interval days = 0 @@ -599,7 +744,7 @@ Note: Some of CoinGecko commands can fail. Team is working on fix. formatter_class=argparse.ArgumentDefaultsHelpFormatter, description=""" Find similar coin by coin name,symbol or id. If you don't remember exact name or id of the Coin at CoinGecko, - Binance or CoinPaprika you can use this command to display coins with similar name, symbol or id + Binance, Coinbase or CoinPaprika you can use this command to display coins with similar name, symbol or id to your search query. Example of usage: coin name is something like "polka". So I can try: find -c polka -k name -t 25 It will search for coin that has similar name to polka and display top 25 matches. @@ -639,7 +784,7 @@ Note: Some of CoinGecko commands can fail. Team is working on fix. parser.add_argument( "--source", dest="source", - choices=["cp", "cg", "bin"], + choices=["cp", "cg", "bin", "cb"], default="cg", help="Source of data.", type=str, diff --git a/gamestonk_terminal/cryptocurrency/cryptocurrency_helpers.py b/gamestonk_terminal/cryptocurrency/cryptocurrency_helpers.py index 32a4fffb6bf..fd06778d276 100644 --- a/gamestonk_terminal/cryptocurrency/cryptocurrency_helpers.py +++ b/gamestonk_terminal/cryptocurrency/cryptocurrency_helpers.py @@ -2,9 +2,11 @@ __docformat__ = "numpy" import os +import json from typing import Tuple, Any, Optional, Union import difflib import pandas as pd +import numpy as np from binance.client import Client import matplotlib.pyplot as plt from tabulate import tabulate @@ -13,15 +15,12 @@ from gamestonk_terminal.helper_funcs import ( plot_autoscale, export_data, ) +from gamestonk_terminal.config_plot import PLOT_DPI from gamestonk_terminal.cryptocurrency.due_diligence import ( pycoingecko_model, coinpaprika_model, ) -from gamestonk_terminal.cryptocurrency.discovery.pycoingecko_model import ( - get_coin_list, - get_mapping_matrix_for_binance, - load_binance_map, -) +from gamestonk_terminal.cryptocurrency.discovery.pycoingecko_model import get_coin_list from gamestonk_terminal.cryptocurrency.overview.coinpaprika_model import ( get_list_of_coins, ) @@ -30,11 +29,35 @@ from gamestonk_terminal.cryptocurrency.due_diligence.binance_model import ( show_available_pairs_for_given_symbol, plot_candles, ) + +from gamestonk_terminal.cryptocurrency.due_diligence import coinbase_model import gamestonk_terminal.config_terminal as cfg from gamestonk_terminal.feature_flags import USE_ION as ion from gamestonk_terminal import feature_flags as gtff +def _load_coin_map(file_name: str) -> pd.DataFrame: + if file_name.split(".")[1] != "json": + raise TypeError("Please load json file") + + current_dir = os.path.dirname(os.path.abspath(__file__)) + path = os.path.join(current_dir, "data", file_name) + with open(path, encoding="utf8") as f: + coins = json.load(f) + + coins_df = pd.Series(coins).reset_index() + coins_df.columns = ["symbol", "id"] + return coins_df + + +def load_binance_map(): + return _load_coin_map("binance_gecko_map.json") + + +def load_coinbase_map(): + return _load_coin_map("coinbase_gecko_map.json") + + def prepare_all_coins_df() -> pd.DataFrame: """Helper method which loads coins from all sources: CoinGecko, CoinPaprika, Binance and merge those coins on keys: @@ -47,12 +70,17 @@ def prepare_all_coins_df() -> pd.DataFrame: CoinGecko - id for coin in CoinGecko API: uniswap CoinPaprika - id for coin in CoinPaprika API: uni-uniswap Binance - symbol (baseAsset) for coin in Binance API: UNI + Coinbase - symbol for coin in Coinbase Pro API e.g UNI Symbol: uni """ gecko_coins_df = get_coin_list() paprika_coins_df = get_list_of_coins() + + # TODO: Think about scheduled job, that once a day will update data + binance_coins_df = load_binance_map().rename(columns={"symbol": "Binance"}) + coinbase_coins_df = load_coinbase_map().rename(columns={"symbol": "Coinbase"}) gecko_paprika_coins_df = pd.merge( gecko_coins_df, paprika_coins_df, on="name", how="left" ) @@ -72,7 +100,15 @@ def prepare_all_coins_df() -> pd.DataFrame: inplace=True, ) - return df_merged[["CoinGecko", "CoinPaprika", "Binance", "Symbol"]] + df_merged = pd.merge( + left=df_merged, + right=coinbase_coins_df, + left_on="CoinGecko", + right_on="id", + how="left", + ) + + return df_merged[["CoinGecko", "CoinPaprika", "Binance", "Coinbase", "Symbol"]] def _create_closest_match_df( @@ -109,7 +145,7 @@ def load( coin: str, source: str, ) -> Tuple[Union[Optional[str], pycoingecko_model.Coin], Any]: - """Load cryptocurrency from given source. Available sources are: CoinGecko, CoinPaprika and Binance. + """Load cryptocurrency from given source. Available sources are: CoinGecko, CoinPaprika, Coinbase and Binance. Loading coin from Binance and CoinPaprika means validation if given coins exists in chosen source, if yes then id of the coin is returned as a string. @@ -151,6 +187,17 @@ def load( current_coin, _ = coinpaprika_model.validate_coin(coin, paprika_coins_dict) return current_coin, source + if source == "cb": + coinbase_coin = coin.upper() + current_coin, pairs = coinbase_model.show_available_pairs_for_given_symbol( + coinbase_coin + ) + if len(pairs) > 0: + print(f"Coin found : {current_coin}\n") + else: + print(f"Couldn't find coin with symbol {current_coin}\n") + return current_coin, source + return current_coin, None @@ -158,7 +205,7 @@ def load( def find(source: str, coin: str, key: str, top: int, export: str) -> None: """Find similar coin by coin name,symbol or id. - If you don't remember exact name or id of the Coin at CoinGecko or CoinPaprika + If you don't remember exact name or id of the Coin at CoinGecko CoinPaprika, Binance or Coinbase you can use this command to display coins with similar name, symbol or id to your search query. Example of usage: coin name is something like "polka". So I can try: find -c polka -k name -t 25 It will search for coin that has similar name to polka and display top 25 matches. @@ -175,7 +222,7 @@ def find(source: str, coin: str, key: str, top: int, export: str) -> None: key: str Searching key (symbol, id, name) source: str - Data source of coins. CoinGecko (cg) or CoinPaprika (cp) or Binance (bin) + Data source of coins. CoinGecko (cg) or CoinPaprika (cp) or Binance (bin), Coinbase (cb) export : str Export dataframe data to csv,json,xlsx file """ @@ -183,76 +230,35 @@ def find(source: str, coin: str, key: str, top: int, export: str) -> None: if source == "cg": coins_df = get_coin_list() coins_list = coins_df[key].to_list() + if key in ["symbol", "id"]: + coin = coin.lower() sim = difflib.get_close_matches(coin, coins_list, top) df = pd.Series(sim).to_frame().reset_index() df.columns = ["index", key] coins_df.drop("index", axis=1, inplace=True) df = df.merge(coins_df, on=key) - if gtff.USE_TABULATE_DF: - print( - tabulate( - df, - headers=df.columns, - floatfmt=".1f", - showindex=False, - tablefmt="fancy_grid", - ) - ) - else: - print(df.to_string, "\n") - - export_data( - export, - os.path.dirname(os.path.abspath(__file__)), - "find", - df, - ) - elif source == "cp": coins_df = get_list_of_coins() coins_list = coins_df[key].to_list() - keys = {"name": "title", "symbol": "upper", "id": "lower"} - key = keys[key] - coin = getattr(coin, str(key))() + func_key = keys[key] + coin = getattr(coin, str(func_key))() sim = difflib.get_close_matches(coin, coins_list, top) df = pd.Series(sim).to_frame().reset_index() df.columns = ["index", key] df = df.merge(coins_df, on=key) - if gtff.USE_TABULATE_DF: - print( - tabulate( - df, - headers=df.columns, - floatfmt=".1f", - showindex=False, - tablefmt="fancy_grid", - ) - ) - else: - print(df.to_string, "\n") - - export_data( - export, - os.path.dirname(os.path.abspath(__file__)), - "find", - df, - ) - elif source == "bin": # TODO: Fix it in future. Determine if user looks for symbol like ETH or ethereum - if len(coin) > 4: + if len(coin) > 5: key = "id" coins_df_gecko = get_coin_list() - coins_bin = get_mapping_matrix_for_binance() - coins_df_bin = pd.Series(coins_bin).reset_index() - coins_df_bin.columns = ["symbol", "id"] + coins_df_bin = load_binance_map() coins = pd.merge( coins_df_bin, coins_df_gecko[["id", "name"]], how="left", on="id" ) @@ -263,29 +269,48 @@ def find(source: str, coin: str, key: str, top: int, export: str) -> None: df.columns = ["index", key] df = df.merge(coins, on=key) - if gtff.USE_TABULATE_DF: - print( - tabulate( - df, - headers=df.columns, - floatfmt=".1f", - showindex=False, - tablefmt="fancy_grid", - ) - ) - else: - print(df.to_string, "\n") + elif source == "cb": + if len(coin) > 5: + key = "id" - export_data( - export, - os.path.dirname(os.path.abspath(__file__)), - "find", - df, + coins_df_gecko = get_coin_list() + coins_df_bin = load_coinbase_map() + coins = pd.merge( + coins_df_bin, coins_df_gecko[["id", "name"]], how="left", on="id" ) + coins_list = coins[key].to_list() + + sim = difflib.get_close_matches(coin, coins_list, top) + df = pd.Series(sim).to_frame().reset_index() + df.columns = ["index", key] + df = df.merge(coins, on=key) else: - print("Couldn't execute find methods for CoinPaprika, Binance or CoinGecko") - print("") + print( + "Couldn't execute find methods for CoinPaprika, Binance, Coinbase or CoinGecko\n" + ) + df = pd.DataFrame() + + if gtff.USE_TABULATE_DF: + print( + tabulate( + df, + headers=df.columns, + floatfmt=".1f", + showindex=False, + tablefmt="fancy_grid", + ), + "\n", + ) + else: + print(df.to_string, "\n") + + export_data( + export, + os.path.dirname(os.path.abspath(__file__)), + "find", + df, + ) def display_all_coins( @@ -293,7 +318,7 @@ def display_all_coins( ) -> None: """Find similar coin by coin name,symbol or id. - If you don't remember exact name or id of the Coin at CoinGecko or CoinPaprika + If you don't remember exact name or id of the Coin at CoinGecko, CoinPaprika, Coinbase, Binance you can use this command to display coins with similar name, symbol or id to your search query. Example of usage: coin name is something like "polka". So I can try: find -c polka -k name -t 25 It will search for coin that has similar name to polka and display top 25 matches. @@ -307,7 +332,7 @@ def display_all_coins( coin: str Cryptocurrency source: str - Data source of coins. CoinGecko (cg) or CoinPaprika (cp) or Binance (bin) + Data source of coins. CoinGecko (cg) or CoinPaprika (cp) or Binance (bin), Coinbase (cb) skip: int Skip N number of records show_all: bool @@ -315,12 +340,13 @@ def display_all_coins( export : str Export dataframe data to csv,json,xlsx file """ - + sources = ["cg", "cp", "bin", "cb"] limit, cutoff = 30, 0.75 coins_func_map = { "cg": get_coin_list, "cp": get_list_of_coins, "bin": load_binance_map, + "cb": load_coinbase_map, } if show_all: @@ -330,7 +356,7 @@ def display_all_coins( else: df = prepare_all_coins_df() - elif not source or source not in ["cg", "cp", "bin"]: + elif not source or source not in sources: df = prepare_all_coins_df() cg_coins_list = df["CoinGecko"].to_list() sim = difflib.get_close_matches(coin.lower(), cg_coins_list, limit, cutoff) @@ -362,6 +388,17 @@ def display_all_coins( df = df[["index", "symbol", "name"]] df.columns = ["index", "id", "name"] + elif source == "cb": + coins_df_gecko = get_coin_list() + coins_df_cb = load_coinbase_map() + coins_df_cb.columns = ["symbol", "id"] + coins_df = pd.merge( + coins_df_cb, coins_df_gecko[["id", "name"]], how="left", on="id" + ) + df = _create_closest_match_df(coin.lower(), coins_df, limit, cutoff) + df = df[["index", "symbol", "name"]] + df.columns = ["index", "id", "name"] + else: df = pd.DataFrame(columns=["index", "id", "symbol"]) print("Couldn't find any coins") @@ -417,7 +454,7 @@ def load_ta_data( Returns ---------- Tuple[pd.DataFrame, str] - dataframe with prices + df with prices quoted currency """ @@ -505,6 +542,24 @@ def load_ta_data( df.index.name = "date" return df, currency + if source == "cb": + assert isinstance(coin, str) + coin, currency = coin.upper(), currency.upper() + pair = f"{coin}-{currency}" + + if coinbase_model.check_validity_of_product(pair): + print(f"{coin} loaded vs {currency}") + + df = coinbase_model.get_candles( + product_id=pair, + interval=interval or "24hour", + ).head(limit) + + df_coin = df.set_index(pd.to_datetime(df["Time0"], unit="s")).drop( + "Time0", axis=1 + ) + return df_coin, currency + return pd.DataFrame(), currency @@ -668,3 +723,79 @@ def plot_chart( plt.ion() plt.show() print("") + + if source == "cb": + assert isinstance(coin, str) + coin, currency = coin.upper(), currency.upper() + pair = f"{coin}-{currency}" + + if coinbase_model.check_validity_of_product(pair): + print(f"{coin} loaded vs {currency}") + + df = coinbase_model.get_candles( + product_id=pair, + interval=interval or "24hour", + ).head(limit) + df = df.astype(float).iloc[:, :6] + df.sort_values(by="Time0", inplace=True, ascending=True) + df = df.set_index(pd.to_datetime(df["Time0"], unit="s")).drop( + "Time0", axis=1 + ) + + title = ( + f"\n{coin}/{currency} from {df.index[0].strftime('%Y/%m/%d')} to {df.index[-1].strftime('%Y/%m/%d')}", + ) + df["Volume"] = df["Volume"] / 1_000 + mpf.plot( + df, + type="candle", + volume=True, + ylabel_lower="Volume [1K]", + title=str(title[0]) if isinstance(title, tuple) else title, + xrotation=20, + 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() + print("") + + +def plot_order_book(bids: np.array, asks: np.array, coin: str) -> None: + """ + Plots Bid/Ask. Can be used for Coinbase and Binance + + 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 + + """ + + _, 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/data/coinbase_gecko_map.json b/gamestonk_terminal/cryptocurrency/data/coinbase_gecko_map.json new file mode 100644 index 00000000000..33e0be5dc27 --- /dev/null +++ b/gamestonk_terminal/cryptocurrency/data/coinbase_gecko_map.json @@ -0,0 +1,101 @@ +{ + "BTC": "bitcoin", + "ETH": "ethereum", + "SOL": "solana", + "ADA": "cardano", + "ALGO": "algorand", + "USDT": "tether", + "WBTC": "wrapped-bitcoin", + "LTC": "litecoin", + "XTZ": "tezos", + "LINK": "chainlink", + "MATIC": "matic-network", + "DAI": "dai", + "DOT": "polkadot", + "ATOM": "cosmos", + "BCH": "bitcoin-cash", + "XLM": "stellar", + "DOGE": "dogecoin", + "FIL": "filecoin", + "USDC": "usd-coin", + "EOS": "eos", + "GRT": "the-graph", + "ETC": "ethereum-classic", + "AXS": "axie-infinity", + "YFI": "yearn-finance", + "UNI": "uniswap", + "ICP": "internet-computer", + "AAVE": "aave", + "OMG": "omisego", + "DASH": "dash", + "QNT": "quant-network", + "UST": "terrausd", + "SUSHI": "sushi", + "ACH": "alchemy-pay", + "MKR": "maker", + "COMP": "compound-governance-token", + "FET": "fetch-ai", + "CGLD": "celo", + "AMP": "amp-token", + "WLUNA": "wrapped-terra", + "CRV": "curve-dao-token", + "ZRX": "0x", + "POLY": "polymath", + "LRC": "loopring", + "SNX": "havven", + "ENJ": "enjincoin", + "ANKR": "ankr", + "XYO": "xyo-network", + "REN": "republic-protocol", + "MANA": "decentraland", + "STORJ": "storj", + "PAX": "paxos-standard", + "CHZ": "chiliz", + "1INCH": "1inch", + "ZEC": "zcash", + "CVC": "civic", + "SKL": "skale", + "RLC": "iexec-rlc", + "UMA": "uma", + "BAND": "band-protocol", + "LPT": "livepeer", + "NKN": "nkn", + "BAT": "basic-attention-token", + "TRB": "tellor", + "IOTX": "iotex", + "MASK": "mask-network", + "KEEP": "keep-network", + "BAL": "balancer", + "RLY": "rally-2", + "NU": "nucypher", + "BOND": "barnbridge", + "CTSI": "cartesi", + "OGN": "origin-protocol", + "OXT": "orchid-protocol", + "COTI": "coti", + "MIR": "mirror-protocol", + "KNC": "kyber-network", + "TRU": "truefi", + "GTC": "gitcoin", + "NMR": "numeraire", + "YFII": "yfii-finance", + "MLN": "melon", + "REQ": "request-network", + "CLV": "clover-finance", + "FARM": "harvest-finance", + "RAD": "radicle", + "FORTH": "ampleforth-governance-token", + "DNT": "district0x", + "RGT": "rari-governance-token", + "ORN": "orion-protocol", + "QUICK": "quick", + "PLA": "playdapp", + "TRIBE": "tribe-2", + "BNT": "bancor", + "LOOM": "loom-network", + "DDX": "derivadao", + "GNT": "golem", + "RAI": "rai", + "REP": "augur", + "SHIB": "shiba-inu" +} diff --git a/gamestonk_terminal/cryptocurrency/discovery/pycoingecko_model.py b/gamestonk_terminal/cryptocurrency/discovery/pycoingecko_model.py index 5706035d6dc..795bba0563a 100644 --- a/gamestonk_terminal/cryptocurrency/discovery/pycoingecko_model.py +++ b/gamestonk_terminal/cryptocurrency/discovery/pycoingecko_model.py @@ -1,8 +1,6 @@ """CoinGecko model""" __docformat__ = "numpy" -import os -import json import pandas as pd from pycoingecko import CoinGeckoAPI from gamestonk_terminal.cryptocurrency.dataframe_helpers import ( @@ -469,9 +467,16 @@ def get_coins_for_given_exchange(exchange_id: str = "binance", page: int = 1) -> return binance_coins["tickers"] -def get_mapping_matrix_for_binance() -> dict: +def get_mapping_matrix_for_exchange(exchange_id: str, pages: int = 12) -> dict: """Creates a matrix with all coins available on Binance with corresponding coingecko coin_id. [Source: CoinGecko] + Parameters + ---------- + exchange_id: str + id of exchange: binance + pages: int + number of pages. One page contains 100 records + Returns ------- dict @@ -479,28 +484,10 @@ def get_mapping_matrix_for_binance() -> dict: """ coins_dct = {} - for i in range(12): - coins = get_coins_for_given_exchange(page=i) + for i in range(pages): + coins = get_coins_for_given_exchange(exchange_id=exchange_id, page=i) for coin in coins: bin_symbol, gecko_id = coin["base"], coin["coin_id"] if bin_symbol not in coins_dct: coins_dct[bin_symbol] = gecko_id return coins_dct - - -def load_binance_map() -> pd.DataFrame: - """Loads a data with all coins available on Binance with corresponding coingecko coin_ids. [Source: CoinGecko] - - Returns - ------- - pd.DataFrame - Columns: symbol, id - """ - - path = os.path.abspath(__file__ + "/../../") - with open(path + "/data/binance_gecko_map.json", encoding="utf8") as f: - coins = json.load(f) - - coins_df = pd.Series(coins).reset_index() - coins_df.columns = ["symbol", "id"] - return coins_df diff --git a/gamestonk_terminal/cryptocurrency/due_diligence/binance_model.py b/gamestonk_terminal/cryptocurrency/due_diligence/binance_model.py index c2b07ee33c2..24aa5359ab9 100644 --- a/gamestonk_terminal/cryptocurrency/due_diligence/binance_model.py +++ b/gamestonk_terminal/cryptocurrency/due_diligence/binance_model.py @@ -8,12 +8,10 @@ from collections import defaultdict import matplotlib.pyplot as plt import mplfinance as mpf import pandas as pd -import numpy as np from binance.client import Client from binance.exceptions import BinanceAPIException import gamestonk_terminal.config_terminal as cfg from gamestonk_terminal.helper_funcs import plot_autoscale -from gamestonk_terminal.config_plot import PLOT_DPI from gamestonk_terminal.feature_flags import USE_ION as ion @@ -125,39 +123,6 @@ def show_available_pairs_for_given_symbol( return None, [] -def plot_order_book(bids: np.array, asks: np.array, coin: str) -> None: - """ - Plots Bid/Ask. [Source: Binance] - - 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 - - """ - - _, 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("") - - def plot_candles(candles_df: pd.DataFrame, title: str) -> None: """Plot candle chart from dataframe. [Source: Binance] diff --git a/gamestonk_terminal/cryptocurrency/due_diligence/binance_view.py b/gamestonk_terminal/cryptocurrency/due_diligence/binance_view.py index 6ef6380a3d1..5d299c7f722 100644 --- a/gamestonk_terminal/cryptocurrency/due_diligence/binance_view.py +++ b/gamestonk_terminal/cryptocurrency/due_diligence/binance_view.py @@ -9,9 +9,7 @@ import pandas as pd from gamestonk_terminal.helper_funcs import ( export_data, ) -from gamestonk_terminal.cryptocurrency.due_diligence.binance_model import ( - plot_order_book, -) +from gamestonk_terminal.cryptocurrency.cryptocurrency_helpers import plot_order_book import gamestonk_terminal.config_terminal as cfg from gamestonk_terminal import feature_flags as gtff diff --git a/gamestonk_terminal/cryptocurrency/due_diligence/coinbase_model.py b/gamestonk_terminal/cryptocurrency/due_diligence/coinbase_model.py new file mode 100644 index 00000000000..de954f93ac9 --- /dev/null +++ b/gamestonk_terminal/cryptocurrency/due_diligence/coinbase_model.py @@ -0,0 +1,193 @@ +"""Coinbase model""" +__docformat__ = "numpy" + +from typing import Optional, Any, Tuple +import pandas as pd +import numpy as np +from gamestonk_terminal.cryptocurrency.coinbase_helpers import ( + check_validity_of_product, + make_coinbase_request, +) + + +def show_available_pairs_for_given_symbol(symbol: str = "ETH") -> Tuple[str, list]: + pairs = make_coinbase_request("/products") + df = pd.DataFrame(pairs)[["base_currency", "quote_currency"]] + + if not isinstance(symbol, str): + print( + f"You did not provide correct symbol {symbol}. Symbol needs to be a string.\n" + ) + return symbol, [] + + coin_df = df[df["base_currency"] == symbol.upper()] + return symbol, coin_df["quote_currency"].to_list() + + +def get_trading_pair_info(product_id: str) -> pd.DataFrame: + """Get information about chosen trading pair. [Source: Coinbase] + + Parameters + ---------- + product_id: str + Trading pair of coins on Coinbase e.g ETH-USDT or UNI-ETH + + Returns + ------- + pd.DataFrame + Basic information about given trading pair + """ + + product_id = check_validity_of_product(product_id) + pair = make_coinbase_request(f"/products/{product_id}") + df = pd.Series(pair).to_frame().reset_index() + df.columns = ["Metric", "Value"] + print(df) + return df + + +def get_order_book(product_id: str) -> Tuple[np.array, np.array, str, dict]: + """Get orders book for chosen trading pair. [Source: Coinbase] + + Parameters + ---------- + product_id: str + Trading pair of coins on Coinbase e.g ETH-USDT or UNI-ETH + + Returns + ------- + Tuple[np.array, np.array, str, dict] + array with bid prices, order sizes and cumulative order sizes + array with ask prices, order sizes and cumulative order sizes + trading pair + dict with raw data + """ + + # TODO: Order with price much higher then current price. E.g current price 200 USD, sell order with 10000 USD + # makes chart look very ugly (bad scaling). Think about removing outliers or add log scale ? + + product_id = check_validity_of_product(product_id) + market_book = make_coinbase_request(f"/products/{product_id}/book?level=2") + + size = min( + len(market_book["bids"]), len(market_book["asks"]) + ) # arrays needs to have equal size. + + market_book["bids"] = market_book["bids"][:size] + market_book["asks"] = market_book["asks"][:size] + market_book.pop("sequence") + + bids = np.asarray(market_book["bids"], dtype=float)[:size] + asks = np.asarray(market_book["asks"], dtype=float)[:size] + + bids = np.insert(bids, 3, (bids[:, 1] * bids[:, 2]).cumsum(), axis=1) + asks = np.insert(asks, 3, np.flipud(asks[:, 1] * asks[:, 2]).cumsum(), axis=1) + bids = np.delete(bids, 2, axis=1) + asks = np.delete(asks, 2, axis=1) + return bids, asks, product_id, market_book + + +def get_trades( + product_id: str, limit: int = 1000, side: Optional[Any] = None +) -> pd.DataFrame: + """Get last N trades for chosen trading pair. [Source: Coinbase] + + Parameters + ---------- + product_id: str + Trading pair of coins on Coinbase e.g ETH-USDT or UNI-ETH + limit: int + Last of trades. Maximum is 1000. + side: str + You can chose either sell or buy side. If side is not set then all trades will be displayed. + Returns + ------- + pd.DataFrame + Last N trades for chosen trading pairs. + """ + + params = {"limit": limit} + if side is not None and side in ["buy", "sell"]: + params["side"] = side + + product_id = check_validity_of_product(product_id) + product = make_coinbase_request(f"/products/{product_id}/trades", params=params) + return pd.DataFrame(product)[["time", "price", "size", "side"]] + + +def get_candles(product_id: str, interval: str = "24h") -> pd.DataFrame: + """Get candles for chosen trading pair and time interval. [Source: Coinbase] + + Parameters + ---------- + product_id: str + Trading pair of coins on Coinbase e.g ETH-USDT or UNI-ETH + interval: str + Time interval. One from 1min, 5min ,15min, 1hour, 6hour, 24hour + + Returns + ------- + pd.DataFrame + Candles for chosen trading pair. + """ + + interval_map = { + "1min": 60, + "5min": 300, + "15min": 900, + "1hour": 3600, + "6hour": 21600, + "24hour": 86400, + "1day": 86400, + } + if interval not in interval_map: + print(f"Wrong interval. Please use on from {list(interval_map.keys())}\n") + return pd.DataFrame() + + granularity: int = interval_map[interval] + + product_id = check_validity_of_product(product_id) + candles = make_coinbase_request( + f"/products/{product_id}/candles", params={"granularity": granularity} + ) + df = pd.DataFrame(candles) + df.columns = [ + "Time0", + "Low", + "High", + "Open", + "Close", + "Volume", + ] + return df[ + [ + "Time0", + "Open", + "High", + "Low", + "Close", + "Volume", + ] + ] + + +def get_product_stats(product_id: str) -> pd.DataFrame: + """Get 24 hr stats for the product. Volume is in base currency units. + Open, high and low are in quote currency units. [Source: Coinbase] + + Parameters + ---------- + product_id: str + Trading pair of coins on Coinbase e.g ETH-USDT or UNI-ETH + + Returns + ------- + pd.DataFrame + 24h stats for chosen trading pair + """ + + product_id = check_validity_of_product(product_id) + product = make_coinbase_request(f"/products/{product_id}/stats") + df = pd.Series(product).reset_index() + df.columns = ["Metric", "Value"] + return df diff --git a/gamestonk_terminal/cryptocurrency/due_diligence/coinbase_view.py b/gamestonk_terminal/cryptocurrency/due_diligence/coinbase_view.py new file mode 100644 index 00000000000..b93c86a58ef --- /dev/null +++ b/gamestonk_terminal/cryptocurrency/due_diligence/coinbase_view.py @@ -0,0 +1,152 @@ +"""Coinbase view""" +__docformat__ = "numpy" + +import os +from typing import Optional +import pandas as pd +from tabulate import tabulate +from pandas.plotting import register_matplotlib_converters +from gamestonk_terminal.helper_funcs import export_data +from gamestonk_terminal import feature_flags as gtff +from gamestonk_terminal.cryptocurrency.due_diligence import coinbase_model +from gamestonk_terminal.cryptocurrency.cryptocurrency_helpers import plot_order_book + +register_matplotlib_converters() + + +def display_order_book(product_id: str, export: str = "") -> None: + """Displays a list of available currency pairs for trading. [Source: Coinbase] + + Parameters + ---------- + product_id: str + Trading pair of coins on Coinbase e.g ETH-USDT or UNI-ETH + export : str + Export dataframe data to csv,json,xlsx file + """ + bids, asks, pair, market_book = coinbase_model.get_order_book(product_id) + plot_order_book(bids, asks, pair) + + export_data( + export, + os.path.dirname(os.path.abspath(__file__)), + "book", + pd.DataFrame(market_book), + ) + + +def display_trades( + product_id: str, limit: int = 1000, side: Optional[str] = None, export: str = "" +) -> None: + """Display last N trades for chosen trading pair. [Source: Coinbase] + + Parameters + ---------- + product_id: str + Trading pair of coins on Coinbase e.g ETH-USDT or UNI-ETH + limit: int + Last of trades. Maximum is 1000. + side: Optional[str] + You can chose either sell or buy side. If side is not set then all trades will be displayed. + export : str + Export dataframe data to csv,json,xlsx file + """ + + df = coinbase_model.get_trades(product_id, limit, side) + df_data = df.copy() + + if gtff.USE_TABULATE_DF: + print( + tabulate( + df, + headers=df.columns, + floatfmt=".3f", + showindex=False, + tablefmt="fancy_grid", + ), + "\n", + ) + else: + print(df.to_string, "\n") + + export_data( + export, + os.path.dirname(os.path.abspath(__file__)), + "trades", + df_data, + ) + + +def display_candles(product_id: str, interval: str, export) -> None: + """Get candles for chosen trading pair and time interval. [Source: Coinbase] + + Parameters + ---------- + product_id: str + Trading pair of coins on Coinbase e.g ETH-USDT or UNI-ETH + interval: str + Time interval. One from 1m, 5m ,15m, 1h, 6h, 24h + export : str + Export dataframe data to csv,json,xlsx file + """ + + df = coinbase_model.get_candles(product_id, interval) + df_data = df.copy() + + if gtff.USE_TABULATE_DF: + print( + tabulate( + df, + headers=df.columns, + floatfmt=".1f", + showindex=True, + tablefmt="fancy_grid", + ), + "\n", + ) + else: + print(df.to_string, "\n") + + export_data( + export, + os.path.dirname(os.path.abspath(__file__)), + "candles", + df_data, + ) + + +def display_stats(product_id: str, export: str) -> None: + """Get 24 hr stats for the product. Volume is in base currency units. + Open, high and low are in quote currency units. [Source: Coinbase] + + Parameters + ---------- + product_id: str + Trading pair of coins on Coinbase e.g ETH-USDT or UNI-ETH + export : str + Export dataframe data to csv,json,xlsx file + """ + + df = coinbase_model.get_product_stats(product_id) + df_data = df.copy() + + if gtff.USE_TABULATE_DF: + print( + tabulate( + df, + headers=df.columns, + floatfmt=".3f", + showindex=False, + tablefmt="fancy_grid", + ), + "\n", + ) + else: + print(df.to_string, "\n") + + export_data( + export, + os.path.dirname(os.path.abspath(__file__)), + "stats", + df_data, + ) diff --git a/gamestonk_terminal/cryptocurrency/due_diligence/dd_controller.py b/gamestonk_terminal/cryptocurrency/due_diligence/dd_controller.py index 658f2ec5b38..6fa9e460d26 100644 --- a/gamestonk_terminal/cryptocurrency/due_diligence/dd_controller.py +++ b/gamestonk_terminal/cryptocurrency/due_diligence/dd_controller.py @@ -13,15 +13,16 @@ from gamestonk_terminal.cryptocurrency.due_diligence import ( pycoingecko_view, coinpaprika_view, binance_view, + coinbase_model, + binance_model, + coinbase_view, ) from gamestonk_terminal.helper_funcs import ( get_flair, parse_known_args_and_warn, check_positive, ) -from gamestonk_terminal.cryptocurrency.due_diligence.binance_model import ( - show_available_pairs_for_given_symbol, -) + from gamestonk_terminal.cryptocurrency.due_diligence.coinpaprika_view import CURRENCIES from gamestonk_terminal.cryptocurrency.cryptocurrency_helpers import plot_chart import gamestonk_terminal.config_terminal as cfg @@ -62,6 +63,7 @@ class DueDiligenceController: "book", "balance", ], + "cb": ["book", "trades", "stats"], } DD_VIEWS_MAPPING = { @@ -122,6 +124,13 @@ Binance: book show order book balance show coin balance """ + if self.source == "cb": + help_text += """ +Coinbase: + book show order book + trades show last trades + stats show coin stats +""" help_text += " chart display chart\n" print(help_text) @@ -527,69 +536,103 @@ Binance: # binance def call_book(self, other_args): """Process book command""" - limit_list = [5, 10, 20, 50, 100, 500, 1000, 5000] - _, quotes = show_available_pairs_for_given_symbol(self.current_coin) - - parser = argparse.ArgumentParser( - prog="book", - add_help=False, - formatter_class=argparse.ArgumentDefaultsHelpFormatter, - 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.add_argument( - "--vs", - help="Quote currency (what to view coin vs)", - dest="vs", - type=str, - default="USDT", - choices=quotes, - ) - - parser.add_argument( - "--export", - choices=["csv", "json", "xlsx"], - default="", - type=str, - dest="export", - help="Export dataframe data to csv,json,xlsx file", - ) - - try: - - ns_parser = parse_known_args_and_warn(parser, other_args) - if not ns_parser: - return - - binance_view.display_order_book( - coin=self.current_coin, - limit=ns_parser.limit, - currency=ns_parser.vs, - export=ns_parser.export, + if self.current_coin: + parser = argparse.ArgumentParser( + prog="book", + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + description="Get the order book for selected coin", ) - except Exception as e: - print(e, "\n") + if self.source == "bin": + + limit_list = [5, 10, 20, 50, 100, 500, 1000, 5000] + _, quotes = binance_model.show_available_pairs_for_given_symbol( + self.current_coin + ) + + parser.add_argument( + "-l", + "--limit", + dest="limit", + help="Limit parameter. Adjusts the weight", + default=100, + type=int, + choices=limit_list, + ) + + parser.add_argument( + "--vs", + help="Quote currency (what to view coin vs)", + dest="vs", + type=str, + default="USDT", + choices=quotes, + ) + + if self.source == "cb": + _, quotes = coinbase_model.show_available_pairs_for_given_symbol( + self.current_coin + ) + if len(quotes) < 0: + print( + f"Couldn't find any quoted coins for provided symbol {self.current_coin}" + ) + return + + parser.add_argument( + "--vs", + help="Quote currency (what to view coin vs)", + dest="vs", + type=str, + default="USDT" if "USDT" in quotes else quotes[0], + choices=quotes, + ) + + parser.add_argument( + "--export", + choices=["csv", "json", "xlsx"], + default="", + type=str, + dest="export", + help="Export dataframe data to csv,json,xlsx file", + ) + + try: + + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + if self.source == "bin": + binance_view.display_order_book( + coin=self.current_coin, + limit=ns_parser.limit, + currency=ns_parser.vs, + export=ns_parser.export, + ) + + elif self.source == "cb": + pair = f"{self.current_coin.upper()}-{ns_parser.vs.upper()}" + coinbase_view.display_order_book( + product_id=pair, + export=ns_parser.export, + ) + + except Exception as e: + print(e, "\n") def call_balance(self, other_args): """Process balance command""" - _, quotes = show_available_pairs_for_given_symbol(self.current_coin) + _, quotes = binance_model.show_available_pairs_for_given_symbol( + self.current_coin + ) parser = argparse.ArgumentParser( prog="balance", add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, - description="Chart", + description="Display balance", ) parser.add_argument( @@ -622,18 +665,131 @@ Binance: except Exception as e: print(e, "\n") + def call_trades(self, other_args): + """Process trades command""" + parser = argparse.ArgumentParser( + prog="trades", + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + description="Show last trades on Coinbase", + ) + + _, quotes = coinbase_model.show_available_pairs_for_given_symbol( + self.current_coin + ) + if len(quotes) < 0: + print( + f"Couldn't find any quoted coins for provided symbol {self.current_coin}" + ) + return + + parser.add_argument( + "--vs", + help="Quote currency (what to view coin vs)", + dest="vs", + type=str, + default="USDT" if "USDT" in quotes else quotes[0], + choices=quotes, + ) + + parser.add_argument( + "--side", + help="Side of trade: buy, sell, all", + dest="side", + type=str, + default="all", + choices=["all", "buy", "sell"], + ) + + parser.add_argument( + "-t", + "--top", + default=15, + dest="top", + help="Limit of records", + type=check_positive, + ) + + parser.add_argument( + "--export", + choices=["csv", "json", "xlsx"], + default="", + type=str, + dest="export", + help="Export dataframe data to csv,json,xlsx file", + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + + if not ns_parser: + return + + pair = f"{self.current_coin.upper()}-{ns_parser.vs.upper()}" + if ns_parser.side.upper() == "all": + side = None + else: + side = ns_parser.side + + coinbase_view.display_trades( + product_id=pair, limit=ns_parser.top, side=side, export=ns_parser.export + ) + + except Exception as e: + print(e, "\n") + + def call_stats(self, other_args): + """Process stats command""" + _, quotes = coinbase_model.show_available_pairs_for_given_symbol( + self.current_coin + ) + + parser = argparse.ArgumentParser( + prog="stats", + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + description="Display coin stats", + ) + + parser.add_argument( + "--vs", + help="Quote currency (what to view coin vs)", + dest="vs", + type=str, + default="USDT" if "USDT" in quotes else quotes[0], + choices=quotes, + ) + + parser.add_argument( + "--export", + choices=["csv", "json", "xlsx"], + default="", + type=str, + dest="export", + help="Export dataframe data to csv,json,xlsx file", + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + + if not ns_parser: + return + + pair = f"{self.current_coin.upper()}-{ns_parser.vs.upper()}" + coinbase_view.display_stats(pair, ns_parser.export) + + except Exception as e: + print(e, "\n") + def call_chart(self, other_args): """Process chart command""" if self.current_coin: parser = argparse.ArgumentParser( add_help=False, formatter_class=argparse.ArgumentDefaultsHelpFormatter, - prog="ta", - description="""Loads data for technical analysis. You can specify currency vs which you want - to show chart and also number of days to get data for. - By default currency: usd and days: 30. - E.g. if you loaded in previous step Ethereum and you want to see it's price vs btc - in last 90 days range use `ta --vs btc --days 90`""", + prog="chart", + description="""Display chart for loaded coin. You can specify currency vs which you want + to show chart and also number of days to get data for.""", ) if self.source == "cp": @@ -688,7 +844,9 @@ Binance: "1month": client.KLINE_INTERVAL_1MONTH, } - _, quotes = show_available_pairs_for_given_symbol(self.current_coin) + _, quotes = binance_model.show_available_pairs_for_given_symbol( + self.current_coin + ) parser.add_argument( "--vs", @@ -718,13 +876,61 @@ Binance: type=check_positive, ) + if self.source == "cb": + interval_map = { + "1min": 60, + "5min": 300, + "15min": 900, + "1hour": 3600, + "6hour": 21600, + "24hour": 86400, + "1day": 86400, + } + + _, quotes = coinbase_model.show_available_pairs_for_given_symbol( + self.current_coin + ) + if len(quotes) < 0: + print( + f"Couldn't find any quoted coins for provided symbol {self.current_coin}" + ) + return + + parser.add_argument( + "--vs", + help="Quote currency (what to view coin vs)", + dest="vs", + type=str, + default="USDT" if "USDT" in quotes else quotes[0], + choices=quotes, + ) + + parser.add_argument( + "-i", + "--interval", + help="Interval to get data", + choices=list(interval_map.keys()), + dest="interval", + default="1day", + type=str, + ) + + parser.add_argument( + "-l", + "--limit", + dest="limit", + default=100, + help="Number to get", + type=check_positive, + ) + try: ns_parser = parse_known_args_and_warn(parser, other_args) if not ns_parser: return - if self.source == "bin": + if self.source in ["bin", "cb"]: limit = ns_parser.limit interval = ns_parser.interval days = 0 diff --git a/gamestonk_terminal/cryptocurrency/overview/coinbase_model.py b/gamestonk_terminal/cryptocurrency/overview/coinbase_model.py new file mode 100644 index 00000000000..6fa2ebc29c7 --- /dev/null +++ b/gamestonk_terminal/cryptocurrency/overview/coinbase_model.py @@ -0,0 +1,35 @@ +"""Coinbase model""" +__docformat__ = "numpy" + +import pandas as pd +from gamestonk_terminal.cryptocurrency.coinbase_helpers import ( + make_coinbase_request, +) + + +def get_trading_pairs() -> pd.DataFrame: + """Get a list of available currency pairs for trading. [Source: Coinbase] + + base_min_size - min order size + base_max_size - max order size + min_market_funds - min funds allowed in a market order. + max_market_funds - max funds allowed in a market order. + + Returns + ------- + pd.DataFrame + Available trading pairs on Coinbase + """ + + columns = [ + "id", + "display_name", + "base_currency", + "quote_currency", + "base_min_size", + "base_max_size", + "min_market_funds", + "max_market_funds", + ] + pairs = make_coinbase_request("/products") + return pd.DataFrame(pairs)[columns] diff --git a/gamestonk_terminal/cryptocurrency/overview/coinbase_view.py b/gamestonk_terminal/cryptocurrency/overview/coinbase_view.py new file mode 100644 index 00000000000..e11db7015f8 --- /dev/null +++ b/gamestonk_terminal/cryptocurrency/overview/coinbase_view.py @@ -0,0 +1,60 @@ +"""Coinbase view""" +__docformat__ = "numpy" + +import os + +from tabulate import tabulate +from gamestonk_terminal.helper_funcs import export_data +from gamestonk_terminal import feature_flags as gtff +from gamestonk_terminal.cryptocurrency.overview import coinbase_model +from gamestonk_terminal.helper_funcs import long_number_format + + +def display_trading_pairs(top: int, sortby: str, descend: bool, export: str) -> None: + """Displays a list of available currency pairs for trading. [Source: Coinbase] + + Parameters + ---------- + top: int + Top n of pairs + sortby: str + Key to sortby data + descend: bool + Sort descending flag + export : str + Export dataframe data to csv,json,xlsx file + """ + + df = coinbase_model.get_trading_pairs() + df_data = df.copy() + + for col in [ + "base_min_size", + "base_max_size", + "min_market_funds", + "max_market_funds", + ]: + df[col] = df[col].apply(lambda x: long_number_format(x)) + + df = df.sort_values(by=sortby, ascending=descend).head(top) + + if gtff.USE_TABULATE_DF: + print( + tabulate( + df, + headers=df.columns, + floatfmt=".2f", + showindex=False, + tablefmt="fancy_grid", + ), + "\n", + ) + else: + print(df.to_string, "\n") + + export_data( + export, + os.path.dirname(os.path.abspath(__file__)), + "pairs", + df_data, + ) diff --git a/gamestonk_terminal/cryptocurrency/overview/overview_controller.py b/gamestonk_terminal/cryptocurrency/overview/overview_controller.py index 6c962d69584..e8db102d019 100644 --- a/gamestonk_terminal/cryptocurrency/overview/overview_controller.py +++ b/gamestonk_terminal/cryptocurrency/overview/overview_controller.py @@ -20,6 +20,7 @@ from gamestonk_terminal.cryptocurrency.overview.coinpaprika_view import CURRENCI from gamestonk_terminal.cryptocurrency.overview.coinpaprika_model import ( get_all_contract_platforms, ) +from gamestonk_terminal.cryptocurrency.overview import coinbase_view class Controller: @@ -52,6 +53,7 @@ class Controller: "cpexchanges", "cpplatforms", "cpcontracts", + "cbpairs", ] def __init__(self): @@ -93,6 +95,8 @@ CoinPaprika: cpexmarkets all available markets on given exchange cpplatforms list blockchain platforms eg. ethereum, solana, kusama, terra cpcontracts all smart contracts for given platform +Coinbase: + cbpairs info about available trading pairs on Coinbase """ print(help_text) @@ -1524,6 +1528,75 @@ CoinPaprika: except Exception as e: print(e, "\n") + def call_cbpairs(self, other_args): + """Process news command""" + parser = argparse.ArgumentParser( + prog="cbpairs", + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + description="Shows available trading pairs on Coinbase ", + ) + + parser.add_argument( + "-t", + "--top", + dest="top", + type=int, + help="top N number of news >=10", + default=15, + ) + + parser.add_argument( + "-s", + "--sort", + dest="sortby", + type=str, + help="Sort by given column. Default: id", + default="id", + choices=[ + "id", + "display_name", + "base_currency", + "quote_currency", + "base_min_size", + "base_max_size", + "min_market_funds", + "max_market_funds", + ], + ) + + parser.add_argument( + "--descend", + action="store_false", + help="Flag to sort in descending order (lowest first)", + dest="descend", + default=True, + ) + + parser.add_argument( + "--export", + choices=["csv", "json", "xlsx"], + default="", + type=str, + dest="export", + help="Export dataframe data to csv,json,xlsx file", + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + coinbase_view.display_trading_pairs( + top=ns_parser.top, + export=ns_parser.export, + sortby=ns_parser.sortby, + descend=ns_parser.descend, + ) + + except Exception as e: + print(e, "\n") + def menu(): overview_controller = Controller() diff --git a/gamestonk_terminal/portfolio/brokers/bro_controller.py b/gamestonk_terminal/portfolio/brokers/bro_controller.py index 89a0d22bb52..4ab143f3f3d 100644 --- a/gamestonk_terminal/portfolio/brokers/bro_controller.py +++ b/gamestonk_terminal/portfolio/brokers/bro_controller.py @@ -17,6 +17,7 @@ from gamestonk_terminal.portfolio.brokers import ( ally_api, ) from gamestonk_terminal.portfolio.brokers.degiro import degiro_controller +from gamestonk_terminal.portfolio.brokers.coinbase import coinbase_controller from gamestonk_terminal.portfolio.brokers.brokers_helpers import ( merge_brokers_holdings, print_brokers_holdings, @@ -40,6 +41,7 @@ class BrokersController: "quit", "rhhold", "rhhist", + "cb", ] BROKERS = [ @@ -66,6 +68,8 @@ class BrokersController: " ally - Ally Invest\n" " alp - Alpaca\n" " rh - Robinhood\n" + "\nCrypto Exchanges Supported:\n" + " cb - Coinbase Pro\n" "\nPortfolio:\n" " cls clear screen\n" " ?/help show this menu again\n" @@ -87,6 +91,8 @@ class BrokersController: " rhhist plot rh brokers holdings history\n" "\nDegiro:\n" "> degiro degiro standalone menu\n" + "\nCoinbase:\n" + "> cb coinbase standalone menu\n" "\nMerge:\n" " login login to your brokers\n" " hold view net holdings across all logins\n" @@ -201,6 +207,12 @@ class BrokersController: return True print("") + def call_cb(self, _): + """Process degiro command.""" + if coinbase_controller.menu(): + return True + print("") + def call_hold(self, _): """Process hold command.""" holdings = pd.DataFrame( diff --git a/gamestonk_terminal/portfolio/brokers/coinbase/__init__.py b/gamestonk_terminal/portfolio/brokers/coinbase/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/gamestonk_terminal/portfolio/brokers/coinbase/coinbase_controller.py b/gamestonk_terminal/portfolio/brokers/coinbase/coinbase_controller.py new file mode 100644 index 00000000000..1f659dfb53b --- /dev/null +++ b/gamestonk_terminal/portfolio/brokers/coinbase/coinbase_controller.py @@ -0,0 +1,368 @@ +"""Coinbase Controller""" +__docformat__ = "numpy" + +# pylint: disable=R0904, C0302, W0622 +import argparse +import os +from prompt_toolkit.completion import NestedCompleter +from gamestonk_terminal import feature_flags as gtff +from gamestonk_terminal.menu import session +from gamestonk_terminal.portfolio.brokers.coinbase import ( + coinbase_view, +) +from gamestonk_terminal.helper_funcs import ( + get_flair, + parse_known_args_and_warn, + check_positive, +) + + +class CoinbaseController: + + CHOICES = [ + "?", + "cls", + "help", + "q", + "quit", + ] + + CB_CHOICES = ["account", "history", "orders", "deposits"] + + def __init__(self): + """CONSTRUCTOR""" + + self._cb_parser = argparse.ArgumentParser(add_help=False, prog="cb") + self.CHOICES.extend(self.CB_CHOICES) + + self._cb_parser.add_argument("cmd", choices=self.CHOICES) + + def print_help(self): + """Print help""" + help_text = """ +Coinbase: + cls clear screen + ?/help show this menu again + q quit this menu, and shows back to main menu + quit quit to abandon the program + + account show balance of your account + history show history of your account + deposits show all your deposits or internal transfers + orders show all your orders + +""" + + print(help_text) + + def switch(self, an_input: str): + """Process and dispatch input + + Returns + ------- + True, False or None + False - quit the menu + True - quit the program + None - continue in the menu + """ + + # Empty command + if not an_input: + print("") + return None + + (known_args, other_args) = self._cb_parser.parse_known_args(an_input.split()) + + # Help menu again + if known_args.cmd == "?": + self.print_help() + return None + + # Clear screen + if known_args.cmd == "cls": + os.system("cls||clear") + return None + + return getattr( + self, "call_" + known_args.cmd, lambda: "Command not recognized!" + )(other_args) + + def call_help(self, _): + """Process Help command""" + self.print_help() + + def call_q(self, _): + """Process Q command - quit the menu.""" + return False + + def call_quit(self, _): + """Process Quit command - quit the program.""" + return True + + def call_account(self, other_args): + """Process account command""" + parser = argparse.ArgumentParser( + prog="account", + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + description="Display info about your trading accounts on Coinbase", + ) + + parser.add_argument( + "--all", + action="store_true", + help="Flag to display all your account", + dest="all", + default=False, + ) + + parser.add_argument( + "--export", + choices=["csv", "json", "xlsx"], + default="", + type=str, + dest="export", + help="Export dataframe data to csv,json,xlsx file", + ) + + try: + if other_args: + if not other_args[0][0] == "-": + other_args.insert(0, "--acc") + + ns_parser = parse_known_args_and_warn(parser, other_args) + + if not ns_parser: + return + + coinbase_view.display_account(ns_parser.all, ns_parser.export) + + except Exception as e: + print(e, "\n") + + def call_history(self, other_args): + """Process account command""" + parser = argparse.ArgumentParser( + prog="history", + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + description="Display account history", + ) + + parser.add_argument( + "-a", + "--acc", + dest="account", + help="Symbol of coin of account or id", + type=str, + default="BTC", + required=False, + ) + + parser.add_argument( + "-l", + "--limit", + dest="limit", + help="Limit parameter.", + default=20, + type=check_positive, + ) + + parser.add_argument( + "--export", + choices=["csv", "json", "xlsx"], + default="", + type=str, + dest="export", + help="Export dataframe data to csv,json,xlsx file", + ) + + try: + if other_args: + if not other_args[0][0] == "-": + other_args.insert(0, "--acc") + + ns_parser = parse_known_args_and_warn(parser, other_args) + + if not ns_parser: + return + + coinbase_view.display_history( + ns_parser.account, ns_parser.export, ns_parser.limit + ) + + except Exception as e: + print(e, "\n") + + def call_orders(self, other_args): + """Process orders command""" + parser = argparse.ArgumentParser( + prog="orders", + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + description="List your current open orders", + ) + + parser.add_argument( + "-l", + "--limit", + dest="limit", + help="Limit parameter.", + default=20, + type=check_positive, + ) + + parser.add_argument( + "-s", + "--sort", + dest="sortby", + type=str, + help="Sort by given column. Default: created_at", + default="created_at", + choices=[ + "product_id", + "side", + "price", + "size", + "type", + "created_at", + "status", + ], + ) + + parser.add_argument( + "--descend", + action="store_false", + help="Flag to sort in descending order (lowest first)", + dest="descend", + default=False, + ) + + parser.add_argument( + "--export", + choices=["csv", "json", "xlsx"], + default="", + type=str, + dest="export", + help="Export dataframe data to csv,json,xlsx file", + ) + + try: + if other_args: + if not other_args[0][0] == "-": + other_args.insert(0, "--acc") + + ns_parser = parse_known_args_and_warn(parser, other_args) + + if not ns_parser: + return + + coinbase_view.display_orders( + ns_parser.limit, ns_parser.sortby, ns_parser.descend, ns_parser.export + ) + + except Exception as e: + print(e, "\n") + + def call_deposits(self, other_args): + """Process deposits command""" + parser = argparse.ArgumentParser( + prog="deposits", + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + description="Display a list of deposits for your account.", + ) + + parser.add_argument( + "-t", + "--type", + dest="type", + type=str, + help="Deposit type. Either: internal_deposits (transfer between portfolios) or deposit", + default="deposit", + choices=["internal_deposit", "deposit"], + ) + + parser.add_argument( + "-l", + "--limit", + dest="limit", + help="Limit parameter.", + default=20, + type=check_positive, + ) + + parser.add_argument( + "-s", + "--sort", + dest="sortby", + type=str, + help="Sort by given column. Default: created_at", + default="created_at", + choices=[ + "created_at", + "amount", + ], + ) + + parser.add_argument( + "--descend", + action="store_false", + help="Flag to sort in descending order (lowest first)", + dest="descend", + default=False, + ) + + parser.add_argument( + "--export", + choices=["csv", "json", "xlsx"], + default="", + type=str, + dest="export", + help="Export dataframe data to csv,json,xlsx file", + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + coinbase_view.display_deposits( + ns_parser.limit, + ns_parser.sortby, + ns_parser.type, + ns_parser.descend, + ns_parser.export, + ) + + except Exception as e: + print(e, "\n") + + +def menu(): + + cb_controller = CoinbaseController() + cb_controller.print_help() + + while True: + # Get input command from user + if session and gtff.USE_PROMPT_TOOLKIT: + completer = NestedCompleter.from_nested_dict( + {c: None for c in cb_controller.CHOICES} + ) + an_input = session.prompt( + f"{get_flair()} (bro)>(cb)> ", + completer=completer, + ) + else: + an_input = input(f"{get_flair()} (bro)>(cb)> ") + + try: + process_input = cb_controller.switch(an_input) + + if process_input is not None: + return process_input + + except SystemExit: + print("The command selected doesn't exist\n") + continue diff --git a/gamestonk_terminal/portfolio/brokers/coinbase/coinbase_model.py b/gamestonk_terminal/portfolio/brokers/coinbase/coinbase_model.py new file mode 100644 index 00000000000..40a5ef69528 --- /dev/null +++ b/gamestonk_terminal/portfolio/brokers/coinbase/coinbase_model.py @@ -0,0 +1,177 @@ +"""Coinbase model""" +__docformat__ = "numpy" + +import pandas as pd +import gamestonk_terminal.config_terminal as cfg +from gamestonk_terminal.cryptocurrency.coinbase_helpers import ( + CoinbaseProAuth, + _check_account_validity, + make_coinbase_request, +) + + +def get_accounts() -> pd.DataFrame: + """Get list of all your trading accounts. [Source: Coinbase] + + Single account information: + { + "id": "71452118-efc7-4cc4-8780-a5e22d4baa53", + "currency": "BTC", + "balance": "0.0000000000000000", + "available": "0.0000000000000000", + "hold": "0.0000000000000000", + "profile_id": "75da88c5-05bf-4f54-bc85-5c775bd68254" + } + + Returns + ------- + pd.DataFrame + DataFrame with all your trading accounts. + """ + + auth = CoinbaseProAuth( + cfg.API_COINBASE_KEY, cfg.API_COINBASE_SECRET, cfg.API_COINBASE_PASS_PHRASE + ) + resp = make_coinbase_request("/accounts", auth=auth) + if not resp: + return pd.DataFrame() + + df = pd.DataFrame(resp) + return df[["id", "currency", "balance", "available", "hold"]] + + +def get_account_history(account: str) -> pd.DataFrame: + """Get your account history. Account activity either increases or decreases your account balance. [Source: Coinbase] + + Example api response: + { + "id": "100", + "created_at": "2014-11-07T08:19:27.028459Z", + "amount": "0.001", + "balance": "239.669", + "type": "fee", + "details": { + "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b", + "trade_id": "74", + "product_id": "BTC-USD" + } + } + + + Parameters + ---------- + account: str + id ("71452118-efc7-4cc4-8780-a5e22d4baa53") or currency (BTC) + Returns + ------- + pd.DataFrame + DataFrame with account history. + """ + + auth = CoinbaseProAuth( + cfg.API_COINBASE_KEY, cfg.API_COINBASE_SECRET, cfg.API_COINBASE_PASS_PHRASE + ) + + account = _check_account_validity(account) + if not account: + return pd.DataFrame() + + resp = make_coinbase_request(f"/accounts/{account}/holds", auth=auth) + if not resp: + return pd.DataFrame() + df = pd.json_normalize(resp) + + try: + df.columns = [ + col.replace("details.", "") if "details" in col else col + for col in df.columns + ] + except Exception as e: + print(e) + + return df + + +def get_orders() -> pd.DataFrame: + """List your current open orders. Only open or un-settled orders are returned. [Source: Coinbase] + + Example response from API: + + { + "id": "d0c5340b-6d6c-49d9-b567-48c4bfca13d2", + "price": "0.10000000", + "size": "0.01000000", + "product_id": "BTC-USD", + "side": "buy", + "stp": "dc", + "type": "limit", + "time_in_force": "GTC", + "post_only": false, + "created_at": "2016-12-08T20:02:28.53864Z", + "fill_fees": "0.0000000000000000", + "filled_size": "0.00000000", + "executed_value": "0.0000000000000000", + "status": "open", + "settled": false + } + + Returns + ------- + pd.DataFrame + Open orders in your account + """ + + auth = CoinbaseProAuth( + cfg.API_COINBASE_KEY, cfg.API_COINBASE_SECRET, cfg.API_COINBASE_PASS_PHRASE + ) + resp = make_coinbase_request("/orders", auth=auth) + if not resp: + df = pd.DataFrame( + columns=[ + "product_id", + "side", + "price", + "size", + "type", + "created_at", + "status", + ] + ) + return df + return pd.DataFrame(resp)[ + ["product_id", "side", "price", "size", "type", "created_at", "status"] + ] + + +def get_deposits(deposit_type: str = "deposit") -> pd.DataFrame: + """Get a list of deposits for your account. [Source: Coinbase] + + Parameters + ---------- + deposit_type: str + internal_deposits (transfer between portfolios) or deposit + + Returns + ------- + pd.DataFrame + List of deposits + """ + + auth = CoinbaseProAuth( + cfg.API_COINBASE_KEY, cfg.API_COINBASE_SECRET, cfg.API_COINBASE_PASS_PHRASE + ) + params = {"type": deposit_type} + if deposit_type not in ["internal_deposit", "deposit"]: + params["type"] = "deposit" + resp = make_coinbase_request("/transfers", auth=auth, params=params) + if not resp: + return pd.DataFrame() + + if isinstance(resp, tuple): + resp = resp[0] + + if deposit_type == "deposit": + df = pd.json_normalize(resp) + else: + df = pd.DataFrame(resp)[["type", "created_at", "amount", "currency"]] + return df diff --git a/gamestonk_terminal/portfolio/brokers/coinbase/coinbase_view.py b/gamestonk_terminal/portfolio/brokers/coinbase/coinbase_view.py new file mode 100644 index 00000000000..4dcffb7d294 --- /dev/null +++ b/gamestonk_terminal/portfolio/brokers/coinbase/coinbase_view.py @@ -0,0 +1,198 @@ +"""Coinbase view""" +__docformat__ = "numpy" + +import os +from tabulate import tabulate +from gamestonk_terminal.helper_funcs import export_data +from gamestonk_terminal import feature_flags as gtff +from gamestonk_terminal.portfolio.brokers.coinbase import coinbase_model + + +def display_account(show_all: bool = True, export: str = "") -> None: + """Display list of all your trading accounts. [Source: Coinbase] + + Parameters + ---------- + show_all: bool + Indicate if you want to show all your accounts or only one. + export : str + Export dataframe data to csv,json,xlsx file + """ + if show_all: + df = coinbase_model.get_accounts() + else: + df = coinbase_model.get_accounts() + df.balance = df["balance"].astype(float) + df = df[df.balance > 0] + + if df.empty: + print( + "No funds found on you account. To display all wallets (including 0 balance) use account --all\n" + ) + return + + df_data = df.copy() + + if gtff.USE_TABULATE_DF: + print( + tabulate( + df, + headers=df.columns, + floatfmt=".3f", + showindex=False, + tablefmt="fancy_grid", + ), + "\n", + ) + else: + print(df.to_string, "\n") + + export_data( + export, + os.path.dirname(os.path.abspath(__file__)), + "account", + df_data, + ) + + +def display_history(account: str, export: str = "", limit: int = 20) -> None: + """Display account history. [Source: Coinbase] + + Parameters + ---------- + account: str + Symbol or account id + limit: int + For all accounts display only top n + export : str + Export dataframe data to csv,json,xlsx file + """ + + df = coinbase_model.get_account_history(account) + df_data = df.copy() + + if df.empty: + print( + f"Your account {account} doesn't have any funds or you provide wrong account name or id. " + f"To check all your accounts use command account --all\n" + ) + return + + if gtff.USE_TABULATE_DF: + print( + tabulate( + df.head(limit), + headers=df.columns, + floatfmt=".3f", + showindex=False, + tablefmt="fancy_grid", + ), + "\n", + ) + else: + print(df.head(limit).to_string, "\n") + + export_data( + export, + os.path.dirname(os.path.abspath(__file__)), + "history", + df_data, + ) + + +def display_orders(limit: int, sortby: str, descend: bool, export: str = "") -> None: + """List your current open orders [Source: Coinbase] + + Parameters + ---------- + limit: int + Last of trades. Maximum is 1000. + sortby: str + Key to sort by + descend: bool + Flag to sort descending + export : str + Export dataframe data to csv,json,xlsx file + """ + + df = coinbase_model.get_orders() + + if df.empty: + print("No orders found for your account\n") + return + + df_data = df.copy() + + df = df.sort_values(by=sortby, ascending=descend).head(limit) + + if gtff.USE_TABULATE_DF: + print( + tabulate( + df, + headers=df.columns, + floatfmt=".3f", + showindex=False, + tablefmt="fancy_grid", + ), + "\n", + ) + else: + print(df.to_string, "\n") + + export_data( + export, + os.path.dirname(os.path.abspath(__file__)), + "orders", + df_data, + ) + + +def display_deposits( + limit: int, sortby: str, deposite_type: str, descend: bool, export: str = "" +) -> None: + """Display last N trades for chosen trading pair. [Source: Coinbase] + + Parameters + ---------- + limit: int + Last of trades. Maximum is 1000. + sortby: str + Key to sort by + descend: bool + Flag to sort descending + deposite_type: str + internal_deposits (transfer between portfolios) or deposit + export : str + Export dataframe data to csv,json,xlsx file + """ + + df = coinbase_model.get_deposits(deposit_type=deposite_type) + + if df.empty: + print("No deposits found for your account\n") + return + + df_data = df.copy() + + df = df.sort_values(by=sortby, ascending=descend).head(limit) + + if gtff.USE_TABULATE_DF: + print( + tabulate( + df, + headers=df.columns, + floatfmt=".3f", + showindex=False, + tablefmt="fancy_grid", + ), + "\n", + ) + else: + print(df.to_string, "\n") + + export_data( + export, + os.path.dirname(os.path.abspath(__file__)), + "deposits", + df_data, + ) diff --git a/tests/test_cryptocurrency/test_coinbase_view.py b/tests/test_cryptocurrency/test_coinbase_view.py new file mode 100644 index 00000000000..c515a36b77e --- /dev/null +++ b/tests/test_cryptocurrency/test_coinbase_view.py @@ -0,0 +1,239 @@ +from unittest import mock, TestCase +import vcr +from gamestonk_terminal.cryptocurrency.due_diligence import ( + coinbase_view as dd_coinbase_view, +) +from gamestonk_terminal.portfolio.brokers.coinbase import ( + coinbase_view as bro_coinbase_view, +) +from gamestonk_terminal.cryptocurrency.overview import coinbase_view as ov_coinbase_view +from tests.helpers import check_print + + +# pylint: disable=unused-import + + +class TestCoinbaseView(TestCase): # pragma: allowlist secret + @check_print(assert_in="Open") + @mock.patch( + "gamestonk_terminal.cryptocurrency.due_diligence.coinbase_model.make_coinbase_request" + ) + @mock.patch( + "gamestonk_terminal.cryptocurrency.due_diligence.coinbase_model.check_validity_of_product" + ) + def test_display_candles(self, mock_validity, mock_request): + mock_validity.return_value = "ETH" + mock_request.return_value = [ + [1631318400, 0.0715, 0.07314, 0.07155, 0.07245, 5957.08396321], + [1631232000, 0.07088, 0.07476, 0.07382, 0.07159, 13264.33844153], + [1631145600, 0.07369, 0.07669, 0.07599, 0.07378, 12462.35265359], + ] + dd_coinbase_view.display_candles("ETH-BTC", "1day", "") + + @check_print(assert_in="Value") + @mock.patch( + "gamestonk_terminal.cryptocurrency.due_diligence.coinbase_model.make_coinbase_request" + ) + @mock.patch( + "gamestonk_terminal.cryptocurrency.due_diligence.coinbase_model.check_validity_of_product" + ) + def test_display_stats(self, mock_validity, mock_request): + mock_validity.return_value = "ETH" + mock_request.return_value = { + "open": "3245.61000000", + "high": "3392.11000000", + "low": "3150.00000000", + "volume": "26185.51325269", + "last": "3333.19000000", + "volume_30day": "1019451.11188405", + } + dd_coinbase_view.display_stats("ETH-USDT", "") + + @check_print(assert_in="price") + @mock.patch( + "gamestonk_terminal.cryptocurrency.due_diligence.coinbase_model.make_coinbase_request" + ) + @mock.patch( + "gamestonk_terminal.cryptocurrency.due_diligence.coinbase_model.check_validity_of_product" + ) + def test_display_trades(self, mock_validity, mock_request): + mock_validity.return_value = "ETH" + mock_request.return_value = [ + { + "time": "2014-11-07T22:19:28.578544Z", + "trade_id": 74, + "price": "10.00000000", + "size": "0.01000000", + "side": "buy", + }, + { + "time": "2014-11-07T01:08:43.642366Z", + "trade_id": 73, + "price": "100.00000000", + "size": "0.01000000", + "side": "sell", + }, + ] + dd_coinbase_view.display_trades("ETH-USDT", limit=100, side=None, export="") + + @check_print(assert_in="product_id") + @mock.patch( + "gamestonk_terminal.portfolio.brokers.coinbase.coinbase_model.make_coinbase_request" + ) + @mock.patch( + "gamestonk_terminal.portfolio.brokers.coinbase.coinbase_model._check_account_validity" + ) + def test_display_orders(self, mock_validity, mock_request): + mock_validity.return_value = "ETH" + mock_request.return_value = [ + { + "id": "d0c5340b-6d6c-49d9-b567-48c4bfca13d2", + "price": "0.10000000", + "size": "0.01000000", + "product_id": "BTC-USD", + "side": "buy", + "stp": "dc", + "type": "limit", + "time_in_force": "GTC", + "post_only": False, + "created_at": "2016-12-08T20:02:28.53864Z", + "fill_fees": "0.0000000000000000", + "filled_size": "0.00000000", + "executed_value": "0.0000000000000000", + "status": "open", + "settled": False, + }, + { + "id": "b227e691-365c-470f-a860-a9b4a37dd1d8", + "price": "1.00000000", + "size": "1.00000000", + "product_id": "BTC-USD", + "side": "buy", + "type": "limit", + "created_at": "2016-12-08T20:01:19.038644Z", + "fill_fees": "0.0000000000000000", + "filled_size": "0.00000000", + "executed_value": "0.0000000000000000", + "status": "pending", + "settled": False, + }, + { + "id": "b24327e691-365c-470f-a860-a9b4a37dd1d8", + "price": "2.00000000", + "size": "1.00000000", + "product_id": "BTC-USD", + "side": "buy", + "type": "limit", + "created_at": "2016-12-08T20:01:19.038644Z", + "fill_fees": "0.0000000000000000", + "filled_size": "0.00000000", + "executed_value": "0.0000000000000000", + "status": "pending", + "settled": False, + }, + ] + bro_coinbase_view.display_orders(2, "created_at", descend=True, export="") + + @check_print(assert_in="crypto_address") + @mock.patch( + "gamestonk_terminal.portfolio.brokers.coinbase.coinbase_model.make_coinbase_request" + ) + @mock.patch( + "gamestonk_terminal.portfolio.brokers.coinbase.coinbase_model._check_account_validity" + ) + def test_display_deposits(self, mock_validity, mock_request): + mock_validity.return_value = "ETH" + mock_request.return_value = ( + [ + { + "id": "6cca6a14-a5e3-4219-9542-86123fc9d6c3", + "type": "deposit", + "created_at": "2019-06-18 01:37:48.78953+00", + "completed_at": "2019-06-18 01:37:49.756147+00", + "canceled_at": None, + "processed_at": "2019-06-18 01:37:49.756147+00", + "account_id": "bf091906-ca7f-499e-95fa-5bc15e918b46", + "user_id": "5eeac63c90b913bf3cf7c92e", + "user_nonce": None, + "amount": "40.00000000", + "details": { + "crypto_address": "testaddress", + "destination_tag": "379156162", + "coinbase_account_id": "7f8803e2-1be5-4a29-bfd2-3bc6645f5a24", + "destination_tag_name": "XRP Tag", + "crypto_transaction_id": "5eeac64cc46b34f5332e5326", + "coinbase_transaction_id": "5eeac652be6cf8b17f7625bd", + "crypto_transaction_hash": "testhash", + }, + }, + { + "id": "543cca6a14-a5e3-4219-9542-86123fc9d6c3", + "type": "deposit", + "created_at": "2019-06-18 01:37:48.78953+00", + "completed_at": "2019-06-18 01:37:49.756147+00", + "canceled_at": None, + "processed_at": "2019-06-18 01:37:49.756147+00", + "account_id": "bf091906-ca7f-499e-95fa-5bc15e918b46", + "user_id": "5eeac63c90b913bf3cf7c92e", + "user_nonce": None, + "amount": "50.00000000", + "details": { + "crypto_address": "12g", + "destination_tag": "379156162", + "coinbase_account_id": "7f8803e2-1be5-4a29-bfd2-3bc6645f5a24", + "destination_tag_name": "XRP Tag", + "crypto_transaction_id": "5eeac64cc46b34f5332e5326", + "coinbase_transaction_id": "5eeac652be6cf8b17f7625bd", + "crypto_transaction_hash": "324", + }, + }, + ], + ) + bro_coinbase_view.display_deposits( + 2, "created_at", "deposit", descend=True, export="" + ) + + @check_print(assert_in="balance") + @mock.patch( + "gamestonk_terminal.portfolio.brokers.coinbase.coinbase_model.make_coinbase_request" + ) + @mock.patch( + "gamestonk_terminal.portfolio.brokers.coinbase.coinbase_model._check_account_validity" + ) + def test_display_history(self, mock_validity, mock_request): + mock_validity.return_value = "ETH" + mock_request.return_value = [ + { + "id": "100", + "created_at": "2014-11-07T08:19:27.028459Z", + "amount": "0.001", + "balance": "239.669", + "type": "fee", + "details": { + "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b", + "trade_id": "74", + "product_id": "ETH-USD", + }, + }, + { + "id": "111", + "created_at": "2014-11-07T08:19:27.028459Z", + "amount": "0.001", + "balance": "239.669", + "type": "fee", + "details": { + "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b", + "trade_id": "75", + "product_id": "ETH-USD", + }, + }, + ] + bro_coinbase_view.display_history("ETH", "", 2) + + @check_print(assert_in="base_currency") + @vcr.use_cassette( + "tests/cassettes/test_cryptocurrency/test_coinbase/test_trading_pairs.yaml", + record_mode="new_episodes", + ) + def test_display_trading_pairs(self): + ov_coinbase_view.display_trading_pairs(10, "base_currency", False, "")