diff options
author | jp <plutakuba@gmail.com> | 2021-09-13 20:07:00 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-09-13 14:07:00 -0400 |
commit | 0b554c6893aa4e805c27b606f7de7ae6c0eb454d (patch) | |
tree | 390d2a378fd60d35f34d4e140887451ba74cc9e7 | |
parent | 6a2792ea8e79e799d0eacc732127dc3ca7d1ce80 (diff) |
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
22 files changed, 2410 insertions, 196 deletions
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 <br> GT_API_SENTIMENTINVESTOR_KEY | | [Tradier](https://developer.tradier.com) | GT_TRADIER_TOKEN | | [Twitter](https://developer.twitter.com) | GT_API_TWITTER_KEY <br/> GT_API_TWITTER_SECRET_KEY <br/> GT_API_TWITTER_BEARER_TOKEN | +| [Coinbase](https://docs.pro.coinbase.com/) | GT_API_COINBASE_KEY <br/> GT_API_COINBASE_SECRET <br/> 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 |