mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-27 04:31:13 +00:00
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
This commit is contained in:
2
.pre-commit-config.yaml
vendored
2
.pre-commit-config.yaml
vendored
@@ -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:
|
||||
|
||||
2
README.md
vendored
2
README.md
vendored
@@ -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:
|
||||
```
|
||||
|
||||
@@ -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"
|
||||
|
||||
167
gamestonk_terminal/cryptocurrency/coinbase_helpers.py
Normal file
167
gamestonk_terminal/cryptocurrency/coinbase_helpers.py
Normal file
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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("")
|
||||
|
||||
101
gamestonk_terminal/cryptocurrency/data/coinbase_gecko_map.json
vendored
Normal file
101
gamestonk_terminal/cryptocurrency/data/coinbase_gecko_map.json
vendored
Normal file
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 <limit> 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
|
||||
152
gamestonk_terminal/cryptocurrency/due_diligence/coinbase_view.py
Normal file
152
gamestonk_terminal/cryptocurrency/due_diligence/coinbase_view.py
Normal file
@@ -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 <limit> 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,
|
||||
)
|
||||
@@ -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
|
||||
|
||||
35
gamestonk_terminal/cryptocurrency/overview/coinbase_model.py
Normal file
35
gamestonk_terminal/cryptocurrency/overview/coinbase_model.py
Normal file
@@ -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]
|
||||
60
gamestonk_terminal/cryptocurrency/overview/coinbase_view.py
Normal file
60
gamestonk_terminal/cryptocurrency/overview/coinbase_view.py
Normal file
@@ -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,
|
||||
)
|
||||
@@ -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()
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
177
gamestonk_terminal/portfolio/brokers/coinbase/coinbase_model.py
Normal file
177
gamestonk_terminal/portfolio/brokers/coinbase/coinbase_model.py
Normal file
@@ -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
|
||||
198
gamestonk_terminal/portfolio/brokers/coinbase/coinbase_view.py
Normal file
198
gamestonk_terminal/portfolio/brokers/coinbase/coinbase_view.py
Normal file
@@ -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 <limit> 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 <limit> 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,
|
||||
)
|
||||
239
tests/test_cryptocurrency/test_coinbase_view.py
Normal file
239
tests/test_cryptocurrency/test_coinbase_view.py
Normal file
@@ -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, "")
|
||||
Reference in New Issue
Block a user