From 7154f9da2dea2c5d6434270f4d2983e43362dbc8 Mon Sep 17 00:00:00 2001 From: godmin Date: Mon, 1 Nov 2021 00:31:35 +0000 Subject: [PATCH 01/17] Added venv to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1dbc687..fbf2807 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ __pycache__/ # Distribution / packaging .Python env/ +venv/ build/ develop-eggs/ dist/ From 127d2a853283bf633faa75a9c5f1132ac88c7dc0 Mon Sep 17 00:00:00 2001 From: godmin Date: Mon, 1 Nov 2021 00:43:00 +0000 Subject: [PATCH 02/17] Updated README file --- README.md | 29 ++++++++++++----------------- geminipy/__init__.py | 3 ++- setup.py | 15 ++++++++++----- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 833ab0b..557f165 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Geminipy -A library for the Gemini bitcoin exchange API +A library for the Gemini bitcoin exchange API. Easily communicate with the Gemini Bitcoin exchange API without having to deal with HTTP requests. @@ -7,14 +7,15 @@ having to deal with HTTP requests. Requirements ============ -Please get an API key from [https://exchange.gemini.com/settings/api](https://exchange.gemini.com/settings/api) +A Python 3 interpreter (versions < 3.8 not tested) +Please get your API keys from [https://exchange.gemini.com/settings/api](https://exchange.gemini.com/settings/api) Installation ============ ```shell -pip install geminipy +pip install "git+https://github.com/pl0mo/geminipy" ``` Example @@ -23,27 +24,21 @@ Example ```python from geminipy import Geminipy -# The connection defaults to the Gemini sandbox. -# Add 'live=True' to use the live exchange -con = Geminipy(api_key='your API key', secret_key='your secret key', live=True) +# Set live param to False to use Gemini testing (sandbox) enviroment. +api = Geminipy(apikey='your API key', secret='your secret key') # public request -symbols = con.symbols() - -# a Requests response is returned. -# So we can access the HTTP reponse code, -# the raw response content, or a json object -print symbols.status_code -print symbols.content -print symbols.json() +symbols = api.symbols() + +print(symbols.json()) # authenticated request -order = con.new_order(amount='1', price='200',side='buy') +order = api.new_order(amount=1, price=200.0,side='buy') -print order.json() +print(order) #send a heartbeat -con.heartbeat() +api.heartbeat() ``` The required nonce is the current millisecond timestamp. diff --git a/geminipy/__init__.py b/geminipy/__init__.py index 20c0a3c..9932581 100644 --- a/geminipy/__init__.py +++ b/geminipy/__init__.py @@ -1,3 +1,4 @@ +# -*- coding:utf-8 -*- """ This module contains a class to make requests to the Gemini API. @@ -11,7 +12,7 @@ import requests -class Geminipy(object): +class Geminipy: """ A class to make requests to the Gemini API. diff --git a/setup.py b/setup.py index c9d0387..49f2bcd 100644 --- a/setup.py +++ b/setup.py @@ -1,17 +1,22 @@ +# -*- coding:utf-8 -*- from setuptools import setup +from pathlib import Path -with open('README.md') as f: - readme = f.read().replace('```', '') +readme = Path('README.md') +if readme.is_file(): + readme = readme.read_text().replace('```', '') +else: + readme = '' setup( name='geminipy', - version='0.0.4', + version='0.0.5', packages=['geminipy'], - url='https://github.com/geminipy/geminipy', + url='https://github.com/pl0mo/geminipy', license='GNU GPL', author='Mike Marzigliano', author_email='marzig76@gmail.com', zip_safe=False, long_description=readme, - description='API client for Gemini', + description='API client for Gemini' ) From 59b9d2335da69b8cf58b5db360585edf64d52e2a Mon Sep 17 00:00:00 2001 From: godmin Date: Mon, 1 Nov 2021 03:20:43 +0000 Subject: [PATCH 03/17] New version 0.0.5 changes: - Code update to python 3.6 or newer version. - Code simplified and refactored - Created new method "_requests" to unify all requests. - Code comments changed. - Method names changed for more friendly and autodoc ones. - New class Geminipy method "get_account_details" - New class Geminipy method "get_earn_balance" - New class Geminipy method "request_approved_address" - New class Geminipy method "remove_approved_address" - New class Geminipy method "get_notional_balances" - Changelog updated --- changelog | 26 +++ geminipy/__init__.py | 497 +++++++++++++++++++++---------------------- 2 files changed, 268 insertions(+), 255 deletions(-) diff --git a/changelog b/changelog index 274a0ad..cde3c90 100644 --- a/changelog +++ b/changelog @@ -1,5 +1,31 @@ # Geminipy Changelog +# v0.0.5 +Code update to python 3.6 or newer version. +Code simplied and refactored +Created new method "_requests" to unifiy all requests. +Code comments changed. +Method names changed for more friendly and autodoc ones. +New class Geminipy method "get_account_details" +New class Geminipy method "get_earn_balance" +New class Geminipy method "request_approved_address" +New class Geminipy method "remove_approved_address" +New class Geminipy method "get_notional_balances" + +# v0.0.5 +Code update to python 3.6 or newer version. +Code simplified and refactored +Created new method "_requests" to unify all requests. +Code comments changed. +Method names changed for more friendly and autodoc ones. +New class Geminipy method "get_account_details" +New class Geminipy method "get_earn_balance" +New class Geminipy method "request_approved_address" +New class Geminipy method "remove_approved_address" +New class Geminipy method "get_notional_balances" +Changelog updated + + # v0.0.4 Added "fees" function for the fee and notional volume API endpoint diff --git a/geminipy/__init__.py b/geminipy/__init__.py index 9932581..d1373f7 100644 --- a/geminipy/__init__.py +++ b/geminipy/__init__.py @@ -4,327 +4,314 @@ Author: Mike Marzigliano """ -import time -import json -import hmac import base64 import hashlib +import hmac +import json +import time +from typing import Dict + import requests +def _get_params(params: Dict, *exclude) -> Dict: + check = lambda k, v: v is not None and k[0].isalpha() and k not in exclude + return {k: v for k, v in params.items() if check(k, v) and k != 'self'} + + class Geminipy: - """ - A class to make requests to the Gemini API. + """Gemini API wrapper. Make public or authenticated requests according to the API documentation: https://docs.gemini.com/ """ - live_url = 'https://api.gemini.com' - sandbox_url = 'https://api.sandbox.gemini.com' - base_url = sandbox_url - api_key = '' - secret_key = '' - - def __init__(self, api_key='', secret_key='', live=False): + def __init__(self, + apikey=None, + secret=None, + live=True, + nonce_mult=1000.0, + api_version=1, + timeout=15, + proxy=None): + """Initialize the class. + + :param str apikey: your Gemini API key. + :param str secret: your Gemini API secret key for signatures. + :param float nonce_mult: a custom multiplier for nonce value. """ - Initialize the class. - - Arguments: - api_key -- your Gemini API key - secret_key -- your Gemini API secret key for signatures - live -- use the live API? otherwise, use the sandbox (default False) - """ - self.api_key = api_key - self.secret_key = secret_key - + # self._api_version = api_version if live: - self.base_url = self.live_url + self._url = f'https://api.gemini.com' + else: + self._url = f'https://api.sandbox.gemini.com' + self._proxy = {'http': proxy} if proxy else None + self._timeout = timeout + self._url = f'{self._url}/v{api_version}' + self._apikey = apikey + self._nonce_mult = nonce_mult + self._secret = secret.encode() + self._headers = {'X-GEMINI-APIKEY': self._apikey} + + # ================ Class private methods ================ + + def _request(self, end_point, method=None, params=None): + method = str(method or 'get').lower() + + request_args = { + 'url': f'{self._url}/{end_point}', + 'method': method, + 'timeout': self._timeout, + 'headers': self._headers + } - # public requests - def symbols(self): - """Send a request for all trading symbols, return the response.""" - url = self.base_url + '/v1/symbols' + if params: + if method in ('post',): + params.update( + request=end_point, + nonce=self._nonce + ) + json_params = json.dumps(params or {}).encode() + payload = base64.b64encode(json_params) + signature = hmac.new(self._secret, payload, hashlib.sha384) + request_args.update(headers={ + 'X-GEMINI-PAYLOAD': payload, + 'X-GEMINI-SIGNATURE': signature.hexdigest(), + }) + else: + request_args.update(params=params) + + if self._proxy: + request_args.update(proxies=self._proxy) + + response = requests.request(**request_args) + + if response: + if response.ok: + return response.json() + else: + # elif any('json' in h for h in response.headers): + try: + data = response.json() + return {'error': data} + except json.JSONDecodeError: + return { + 'error': { + 'code': response.status_code, + 'reason': response.reason, + 'content': response.text + } + } + except (requests.RequestException, requests.ConnectionError): + response.raise_for_status() + + @property + def _nonce(self): + """Return the current millisecond timestamp as the nonce. + + :return: current millisecond timestamp as the nonce + """ + return f'{time.time() * self._nonce_mult:.0f}' - return requests.get(url) + # ================ PUBLIC END POINTS ================ - def pubticker(self, symbol='btcusd'): - """Send a request for latest ticker info, return the response.""" - url = self.base_url + '/v1/pubticker/' + symbol + def get_symbols(self): + """Send a request to get trading symbols info.""" + return self._request('symbols') - return requests.get(url) + def get_ticker(self, symbol): + """Send a request to get a trading symbol ticker info. - def book(self, symbol='btcusd', limit_bids=0, limit_asks=0): + :param symbol: symbol id to query (example: "btcusd") + :return: trading symbol ticker info supplied by server. """ - Send a request to get the public order book, return the response. + return self._request(f'pubticker/{symbol}') - Arguments: - symbol -- currency symbol (default 'btcusd') - limit_bids -- limit the number of bids returned (default 0) - limit_asks -- limit the number of asks returned (default 0) - """ - url = self.base_url + '/v1/book/' + symbol - params = { - 'limit_bids': limit_bids, - 'limit_asks': limit_asks - } + def get_orderbook(self, symbol, limit_bids=0, limit_asks=0): + """Send a request to get a symbol order book data. - return requests.get(url, params) - - def trades(self, symbol='btcusd', since=0, limit_trades=50, - include_breaks=0): + :param symbol: symbol id to query (example: "btcusd") + :param limit_bids: limit the number of bids returned (default 0) + :param limit_asks: limit the number of asks returned (default 0) + :return: symbol order book info supplied by server. """ - Send a request to get all public trades, return the response. + params = _get_params(locals(), 'symbol') + return self._request(f'book/{symbol}', params=params) - Arguments: - symbol -- currency symbol (default 'btcusd') - since -- only return trades after this unix timestamp (default 0) - limit_trades -- maximum number of trades to return (default 50). - include_breaks -- whether to display broken trades (default False) - """ - url = self.base_url + '/v1/trades/' + symbol - params = { - 'since': since, - 'limit_trades': limit_trades, - 'include_breaks': include_breaks - } + def get_trades(self, symbol, since=0, limit_trades=50, include_breaks=0): + """Send a request to get a symbol public trades. - return requests.get(url, params) - - def auction(self, symbol='btcusd'): - """Send a request for latest auction info, return the response.""" - url = self.base_url + '/v1/auction/' + symbol + :param str symbol: symbol id to query (example: "btcusd") + :param int since: return trades after this unix epoch (default 0) + :param limit_trades: max number of trades to return (default 50). + :param int include_breaks: whether to display broken trades (default 0) + :return: supplied symbol public trades info supplied by server. + """ + params = _get_params(locals(), 'symbol') + return self._request(f'trades/{symbol}', params=params) - return requests.get(url) + def get_auction(self, symbol): + """Send a request to get a symbol latest auction info. - def auction_history(self, symbol='btcusd', since=0, - limit_auction_results=50, include_indicative=1): - """ - Send a request for auction history info, return the response. - - Arguments: - symbol -- currency symbol (default 'btcusd') - since -- only return auction events after this timestamp (default 0) - limit_auction_results -- maximum number of auction events to return - (default 50). - include_indicative -- whether to include publication of indicative - prices and quantities. (default True) + :param str symbol: symbol id to query (example: "btcusd") + :return: supplied symbol latest auction supplied by server. """ - url = self.base_url + '/v1/auction/' + symbol + '/history' - params = { - 'since': since, - 'limit_auction_results': limit_auction_results, - 'include_indicative': include_indicative - } + return self._request(f'auction/{symbol}') - return requests.get(url, params) + def get_auction_history(self, symbol, since=0, limit_auction_results=50, include_indicative=1): + """Send a request to get a symbol auction history. - # authenticated requests - def new_order(self, amount, price, side, client_order_id=None, - symbol='btcusd', type='exchange limit', options=None): + :param str symbol: symbol id to query (example: "btcusd") + :param since: only return auction events after this timestamp. + :param limit_auction_results: max number of auction events to return. + :param include_indicative: set to 0 to not include publication of indicative info. + :return: """ - Send a request to place an order, return the response. - - Arguments: - amount -- quoted decimal amount of BTC to purchase - price -- quoted decimal amount of USD to spend per BTC - side -- 'buy' or 'sell' - client_order_id -- an optional client-specified order id (default None) - symbol -- currency symbol (default 'btcusd') - type -- the order type (default 'exchange limit') + params = _get_params(locals(), 'symbol') + return self._request(f'auction/{symbol}/history', params) + + # ================ AUTHENTICATED ENDPOINTS ================ + + def place_order(self, side, symbol, amount, price=None, client_order_id=None, type=None, options=None): + """Send a request to place an order. + + :param str side: 'buy' or 'sell' + :param str symbol: symbol id to query (example: "btcusd") + :param float amount: symbol amount to order. + :param float price: order price. + :param client_order_id: an optional custom order indetifier. + :param type: order type (default 'exchange limit') + :param options: + return: """ - request = '/v1/order/new' - url = self.base_url + request - params = { - 'request': request, - 'nonce': self.get_nonce(), - 'symbol': symbol, - 'amount': amount, - 'price': price, - 'side': side, - 'type': type - } - - if client_order_id is not None: - params['client_order_id'] = client_order_id - - if options is not None: - params['options'] = options - - return requests.post(url, headers=self.prepare(params)) + params = _get_params(locals(), 'amount', 'price') + params.update(amount=f'{amount}') + return self._request('order/new', 'post', params) def cancel_order(self, order_id): - """ - Send a request to cancel an order, return the response. + """Send a request to cancel an order. - Arguments: - order_id - the order id to cancel + :param str order_id: order id to cancel. + :return: """ - request = '/v1/order/cancel' - url = self.base_url + request - params = { - 'request': request, - 'nonce': self.get_nonce(), - 'order_id': order_id - } - - return requests.post(url, headers=self.prepare(params)) + params = _get_params(locals()) + return self._request('order/cancel', 'post', params) def cancel_session(self): - """Send a request to cancel all session orders, return the response.""" - request = '/v1/order/cancel/session' - url = self.base_url + request - params = { - 'request': request, - 'nonce': self.get_nonce() - } + """Send a request to cancel all session orders. - return requests.post(url, headers=self.prepare(params)) + :return: + """ + return self._request('order/cancel/session', 'post') def cancel_all(self): - """Send a request to cancel all orders, return the response.""" - request = '/v1/order/cancel/all' - url = self.base_url + request - params = { - 'request': request, - 'nonce': self.get_nonce() - } - - return requests.post(url, headers=self.prepare(params)) + """Send a request to cancel all orders. - def order_status(self, order_id): + :return: """ - Send a request to get an order status, return the response. + return self._request('order/cancel/all', 'post') - Arguments: - order_id -- the order id to get information on + def get_order_status(self, order_id): + """Send a request to get an order current status. + + :param str order_id: the order id to get information on. + :return: """ - request = '/v1/order/status' - url = self.base_url + request - params = { - 'request': request, - 'nonce': self.get_nonce(), - 'order_id': order_id - } + params = _get_params(locals()) + return self._request('order/status', 'post', params) - return requests.post(url, headers=self.prepare(params)) + def get_active_orders(self): + """Send a request to get active orders. - def active_orders(self): - """Send a request to get active orders, return the response.""" - request = '/v1/orders' - url = self.base_url + request - params = { - 'request': request, - 'nonce': self.get_nonce() - } + :return: + """ + return self._request('orders', 'post') - return requests.post(url, headers=self.prepare(params)) + def get_past_trades(self, symbol, limit_trades=50, timestamp=0): + """Send a symbol trades history request. - def past_trades(self, symbol='btcusd', limit_trades=50, timestamp=0): + :param str symbol: symbol id to query (example: "btcusd") + :param limit_trades: max number of trades to return (default 50) + :param timestamp: return trades after supplied unix epoch (default 0) + :return: """ - Send a trade history request, return the response. + params = _get_params(locals()) + return self._request('mytrades', 'post', params) - Arguements: - symbol -- currency symbol (default 'btcusd') - limit_trades -- maximum number of trades to return (default 50) - timestamp -- only return trades after this unix timestamp (default 0) - """ - request = '/v1/mytrades' - url = self.base_url + request - params = { - 'request': request, - 'nonce': self.get_nonce(), - 'symbol': symbol, - 'limit_trades': limit_trades, - 'timestamp': timestamp - } + def get_trade_volume(self): + """Send a request to get your trade volume.""" + return self._request('tradevolume', 'post') - return requests.post(url, headers=self.prepare(params)) + def get_balances(self): + """Send an account balance request. - def tradevolume(self): - """Send a request to get your trade volume, return the response.""" - request = '/v1/tradevolume' - url = self.base_url + request - params = { - 'request': request, - 'nonce': self.get_nonce() - } + :return: + """ + return self._request('balances', 'post') - return requests.post(url, headers=self.prepare(params)) + def new_deposit_address(self, currency, label=None): + """Send a request to generate a new cryptocurrency deposit address. - def balances(self): - """Send an account balance request, return the response.""" - request = '/v1/balances' - url = self.base_url + request - params = { - 'request': request, - 'nonce': self.get_nonce() - } + :param str currency: crypto currency ID (example: btc) + :param str label: optional label for the deposit address + :return: + """ + params = _get_params(locals(), 'currency') + return self._request(f'deposit/{currency}/newAddress', 'post', params) - return requests.post(url, headers=self.prepare(params)) + def get_notional_balances(self, currency, account=None): + """Get approved addresses for supplied network and account (if supplied) - def newAddress(self, currency='btc', label=''): + :param str currency: currency to get balance for. + :param str account: account name (example: primary) + :return: """ - Send a request for a new cryptocurrency deposit address - with an optional label. Return the response. + params = _get_params(locals(), 'currency') + return self._request(f'notionalbalances/{currency}', 'post', params) + + def remove_approved_address(self, network, address, account=None): + """Get approved addresses for supplied network and account (if supplied) - Arguements: - currency -- a Gemini supported cryptocurrency (btc, eth) - label -- optional label for the deposit address + :param str network: accepted values: bitcoin, ethereum, bitcoincash, litecoin, zcash, filecoin, dogecoin, tezos + :param str address: address to remove. + :param str account: account name (example: primary) + :return: """ - request = '/v1/deposit/' + currency + '/newAddress' - url = self.base_url + request - params = { - 'request': request, - 'nonce': self.get_nonce() - } + params = _get_params(locals(), 'network') + return self._request(f'approvedAddresses/account/{network}/remove', 'post', params) - if label != '': - params['label'] = label + def request_approved_address(self, network, address, label=None, account=None): + """Get approved addresses for supplied network and account (if supplied) - return requests.post(url, headers=self.prepare(params)) + :param str network: accepted values: bitcoin, ethereum, bitcoincash, litecoin, zcash, filecoin, dogecoin, tezos + :param str address: address to remove. + :param str label: a label to identify the supplied address. + :param str account: account name (example: primary) + :return: + """ + params = _get_params(locals(), 'network') + return self._request(f'approvedAddresses/{network}/request', 'post', params) - def fees(self): - """Send a request to get fee and notional volume, return the response.""" - request = '/v1/notionalvolume' - url = self.base_url + request - params = { - 'request': request, - 'nonce': self.get_nonce() - } + def get_fees(self): + """Send a request to get fees and notional volume.""" + self._request('notionalvolume', 'post') - return requests.post(url, headers=self.prepare(params)) + def get_earn_balance(self, account=None): + """Send an account earn balance request. - def heartbeat(self): - """Send a heartbeat message, return the response.""" - request = '/v1/heartbeat' - url = self.base_url + request - params = { - 'request': request, - 'nonce': self.get_nonce() - } + :param str account: account name (example: primary) + :return: + """ + params = _get_params(locals()) + return self._request('balances/earn', 'post', params) - return requests.post(url, headers=self.prepare(params)) + def get_account_details(self): + return self._request('account', 'post') - def get_nonce(self): - """Return the current millisecond timestamp as the nonce.""" - return int(round(time.time() * 1000)) + def get_heartbeat(self): + """Send a heartbeat message. - def prepare(self, params): + :return: """ - Prepare, return the required HTTP headers. - - Base 64 encode the parameters, sign it with the secret key, - create the HTTP headers, return the whole payload. + return self._request('heartbeat', 'post') - Arguments: - params -- a dictionary of parameters - """ - jsonparams = json.dumps(params) - payload = base64.b64encode(jsonparams.encode()) - signature = hmac.new(self.secret_key.encode(), payload, - hashlib.sha384).hexdigest() - - return {'X-GEMINI-APIKEY': self.api_key, - 'X-GEMINI-PAYLOAD': payload, - 'X-GEMINI-SIGNATURE': signature} From 59d1d6f4fc4a0cc17482aabe52cbafa510cc823b Mon Sep 17 00:00:00 2001 From: godmin Date: Mon, 1 Nov 2021 03:32:04 +0000 Subject: [PATCH 04/17] Example method names fix --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 557f165..81095cf 100644 --- a/README.md +++ b/README.md @@ -28,17 +28,17 @@ from geminipy import Geminipy api = Geminipy(apikey='your API key', secret='your secret key') # public request -symbols = api.symbols() +symbols = api.get_symbols() -print(symbols.json()) +print(symbols) # authenticated request -order = api.new_order(amount=1, price=200.0,side='buy') +order = api.place_order('btcusd', amount=1, price=200.0, side='buy',) print(order) #send a heartbeat -api.heartbeat() +api.get_heartbeat() ``` The required nonce is the current millisecond timestamp. From eb77292639d834d8783b172835e5a49e2674dd4f Mon Sep 17 00:00:00 2001 From: godmin Date: Mon, 1 Nov 2021 03:48:22 +0000 Subject: [PATCH 05/17] Code organization by creating "utils.py" and "core.py" files. Some errors fixed on README examples. Using find_packages in "setup.py" file. --- .gitignore | 4 + changelog | 15 +- geminipy/__init__.py | 312 +---------------------------------------- geminipy/core.py | 323 +++++++++++++++++++++++++++++++++++++++++++ geminipy/utils.py | 8 ++ setup.py | 6 +- 6 files changed, 346 insertions(+), 322 deletions(-) create mode 100644 geminipy/core.py create mode 100644 geminipy/utils.py diff --git a/.gitignore b/.gitignore index fbf2807..1f0d331 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,7 @@ target/ #Ipython Notebook .ipynb_checkpoints + + +# Pycharm +.idea diff --git a/changelog b/changelog index cde3c90..33951d0 100644 --- a/changelog +++ b/changelog @@ -1,16 +1,9 @@ # Geminipy Changelog -# v0.0.5 -Code update to python 3.6 or newer version. -Code simplied and refactored -Created new method "_requests" to unifiy all requests. -Code comments changed. -Method names changed for more friendly and autodoc ones. -New class Geminipy method "get_account_details" -New class Geminipy method "get_earn_balance" -New class Geminipy method "request_approved_address" -New class Geminipy method "remove_approved_address" -New class Geminipy method "get_notional_balances" +# v0.0.6 +Code organization by creating "utils.py" and "core.py" files. +Some errors fixed on README examples. +Using findpackages in "setup.py" file. # v0.0.5 Code update to python 3.6 or newer version. diff --git a/geminipy/__init__.py b/geminipy/__init__.py index d1373f7..b51bf76 100644 --- a/geminipy/__init__.py +++ b/geminipy/__init__.py @@ -4,314 +4,10 @@ Author: Mike Marzigliano """ -import base64 -import hashlib -import hmac -import json -import time -from typing import Dict +from geminipy.core import Geminipy -import requests +# Geminipy = GeminiAPI -def _get_params(params: Dict, *exclude) -> Dict: - check = lambda k, v: v is not None and k[0].isalpha() and k not in exclude - return {k: v for k, v in params.items() if check(k, v) and k != 'self'} - - -class Geminipy: - """Gemini API wrapper. - - Make public or authenticated requests according to the API documentation: - https://docs.gemini.com/ - """ - - def __init__(self, - apikey=None, - secret=None, - live=True, - nonce_mult=1000.0, - api_version=1, - timeout=15, - proxy=None): - """Initialize the class. - - :param str apikey: your Gemini API key. - :param str secret: your Gemini API secret key for signatures. - :param float nonce_mult: a custom multiplier for nonce value. - """ - # self._api_version = api_version - if live: - self._url = f'https://api.gemini.com' - else: - self._url = f'https://api.sandbox.gemini.com' - self._proxy = {'http': proxy} if proxy else None - self._timeout = timeout - self._url = f'{self._url}/v{api_version}' - self._apikey = apikey - self._nonce_mult = nonce_mult - self._secret = secret.encode() - self._headers = {'X-GEMINI-APIKEY': self._apikey} - - # ================ Class private methods ================ - - def _request(self, end_point, method=None, params=None): - method = str(method or 'get').lower() - - request_args = { - 'url': f'{self._url}/{end_point}', - 'method': method, - 'timeout': self._timeout, - 'headers': self._headers - } - - if params: - if method in ('post',): - params.update( - request=end_point, - nonce=self._nonce - ) - json_params = json.dumps(params or {}).encode() - payload = base64.b64encode(json_params) - signature = hmac.new(self._secret, payload, hashlib.sha384) - request_args.update(headers={ - 'X-GEMINI-PAYLOAD': payload, - 'X-GEMINI-SIGNATURE': signature.hexdigest(), - }) - else: - request_args.update(params=params) - - if self._proxy: - request_args.update(proxies=self._proxy) - - response = requests.request(**request_args) - - if response: - if response.ok: - return response.json() - else: - # elif any('json' in h for h in response.headers): - try: - data = response.json() - return {'error': data} - except json.JSONDecodeError: - return { - 'error': { - 'code': response.status_code, - 'reason': response.reason, - 'content': response.text - } - } - except (requests.RequestException, requests.ConnectionError): - response.raise_for_status() - - @property - def _nonce(self): - """Return the current millisecond timestamp as the nonce. - - :return: current millisecond timestamp as the nonce - """ - return f'{time.time() * self._nonce_mult:.0f}' - - # ================ PUBLIC END POINTS ================ - - def get_symbols(self): - """Send a request to get trading symbols info.""" - return self._request('symbols') - - def get_ticker(self, symbol): - """Send a request to get a trading symbol ticker info. - - :param symbol: symbol id to query (example: "btcusd") - :return: trading symbol ticker info supplied by server. - """ - return self._request(f'pubticker/{symbol}') - - def get_orderbook(self, symbol, limit_bids=0, limit_asks=0): - """Send a request to get a symbol order book data. - - :param symbol: symbol id to query (example: "btcusd") - :param limit_bids: limit the number of bids returned (default 0) - :param limit_asks: limit the number of asks returned (default 0) - :return: symbol order book info supplied by server. - """ - params = _get_params(locals(), 'symbol') - return self._request(f'book/{symbol}', params=params) - - def get_trades(self, symbol, since=0, limit_trades=50, include_breaks=0): - """Send a request to get a symbol public trades. - - :param str symbol: symbol id to query (example: "btcusd") - :param int since: return trades after this unix epoch (default 0) - :param limit_trades: max number of trades to return (default 50). - :param int include_breaks: whether to display broken trades (default 0) - :return: supplied symbol public trades info supplied by server. - """ - params = _get_params(locals(), 'symbol') - return self._request(f'trades/{symbol}', params=params) - - def get_auction(self, symbol): - """Send a request to get a symbol latest auction info. - - :param str symbol: symbol id to query (example: "btcusd") - :return: supplied symbol latest auction supplied by server. - """ - return self._request(f'auction/{symbol}') - - def get_auction_history(self, symbol, since=0, limit_auction_results=50, include_indicative=1): - """Send a request to get a symbol auction history. - - :param str symbol: symbol id to query (example: "btcusd") - :param since: only return auction events after this timestamp. - :param limit_auction_results: max number of auction events to return. - :param include_indicative: set to 0 to not include publication of indicative info. - :return: - """ - params = _get_params(locals(), 'symbol') - return self._request(f'auction/{symbol}/history', params) - - # ================ AUTHENTICATED ENDPOINTS ================ - - def place_order(self, side, symbol, amount, price=None, client_order_id=None, type=None, options=None): - """Send a request to place an order. - - :param str side: 'buy' or 'sell' - :param str symbol: symbol id to query (example: "btcusd") - :param float amount: symbol amount to order. - :param float price: order price. - :param client_order_id: an optional custom order indetifier. - :param type: order type (default 'exchange limit') - :param options: - return: - """ - params = _get_params(locals(), 'amount', 'price') - params.update(amount=f'{amount}') - return self._request('order/new', 'post', params) - - def cancel_order(self, order_id): - """Send a request to cancel an order. - - :param str order_id: order id to cancel. - :return: - """ - params = _get_params(locals()) - return self._request('order/cancel', 'post', params) - - def cancel_session(self): - """Send a request to cancel all session orders. - - :return: - """ - return self._request('order/cancel/session', 'post') - - def cancel_all(self): - """Send a request to cancel all orders. - - :return: - """ - return self._request('order/cancel/all', 'post') - - def get_order_status(self, order_id): - """Send a request to get an order current status. - - :param str order_id: the order id to get information on. - :return: - """ - params = _get_params(locals()) - return self._request('order/status', 'post', params) - - def get_active_orders(self): - """Send a request to get active orders. - - :return: - """ - return self._request('orders', 'post') - - def get_past_trades(self, symbol, limit_trades=50, timestamp=0): - """Send a symbol trades history request. - - :param str symbol: symbol id to query (example: "btcusd") - :param limit_trades: max number of trades to return (default 50) - :param timestamp: return trades after supplied unix epoch (default 0) - :return: - """ - params = _get_params(locals()) - return self._request('mytrades', 'post', params) - - def get_trade_volume(self): - """Send a request to get your trade volume.""" - return self._request('tradevolume', 'post') - - def get_balances(self): - """Send an account balance request. - - :return: - """ - return self._request('balances', 'post') - - def new_deposit_address(self, currency, label=None): - """Send a request to generate a new cryptocurrency deposit address. - - :param str currency: crypto currency ID (example: btc) - :param str label: optional label for the deposit address - :return: - """ - params = _get_params(locals(), 'currency') - return self._request(f'deposit/{currency}/newAddress', 'post', params) - - def get_notional_balances(self, currency, account=None): - """Get approved addresses for supplied network and account (if supplied) - - :param str currency: currency to get balance for. - :param str account: account name (example: primary) - :return: - """ - params = _get_params(locals(), 'currency') - return self._request(f'notionalbalances/{currency}', 'post', params) - - def remove_approved_address(self, network, address, account=None): - """Get approved addresses for supplied network and account (if supplied) - - :param str network: accepted values: bitcoin, ethereum, bitcoincash, litecoin, zcash, filecoin, dogecoin, tezos - :param str address: address to remove. - :param str account: account name (example: primary) - :return: - """ - params = _get_params(locals(), 'network') - return self._request(f'approvedAddresses/account/{network}/remove', 'post', params) - - def request_approved_address(self, network, address, label=None, account=None): - """Get approved addresses for supplied network and account (if supplied) - - :param str network: accepted values: bitcoin, ethereum, bitcoincash, litecoin, zcash, filecoin, dogecoin, tezos - :param str address: address to remove. - :param str label: a label to identify the supplied address. - :param str account: account name (example: primary) - :return: - """ - params = _get_params(locals(), 'network') - return self._request(f'approvedAddresses/{network}/request', 'post', params) - - def get_fees(self): - """Send a request to get fees and notional volume.""" - self._request('notionalvolume', 'post') - - def get_earn_balance(self, account=None): - """Send an account earn balance request. - - :param str account: account name (example: primary) - :return: - """ - params = _get_params(locals()) - return self._request('balances/earn', 'post', params) - - def get_account_details(self): - return self._request('account', 'post') - - def get_heartbeat(self): - """Send a heartbeat message. - - :return: - """ - return self._request('heartbeat', 'post') - +__package__ = 'geminipy' +__all__ = ['Geminipy'] diff --git a/geminipy/core.py b/geminipy/core.py new file mode 100644 index 0000000..be0c133 --- /dev/null +++ b/geminipy/core.py @@ -0,0 +1,323 @@ +# -*- coding: utf-8 -*- +import base64 +import hashlib +import hmac +import json +import time + +import requests + +from geminipy import _get_params + + +class Client: + """Gemini API wrapper. + + Make public or authenticated requests according to the API documentation: + https://docs.gemini.com/ + """ + + def __init__(self, + apikey=None, + secret=None, + live=True, + nonce_mult=1000.0, + api_version=1, + timeout=15, + proxy=None): + """Initialize the class. + + :param str apikey: your Gemini API key. + :param str secret: your Gemini API secret key for signatures. + :param float nonce_mult: a custom multiplier for nonce value. + """ + # self._api_version = api_version + if live: + self._url = f'https://api.gemini.com' + else: + self._url = f'https://api.sandbox.gemini.com' + self._proxy = {'http': proxy} if proxy else None + self._timeout = timeout + self._url = f'{self._url}/v{api_version}' + self._apikey = apikey + self._nonce_mult = nonce_mult + self._secret = secret.encode() + self._headers = {'X-GEMINI-APIKEY': self._apikey} + + # ================ Class private methods ================ + + def _request(self, end_point, method=None, params=None): + method = str(method or 'get').lower() + + request_args = { + 'url': f'{self._url}/{end_point}', + 'method': method, + 'timeout': self._timeout, + 'headers': self._headers + } + + if params: + if method in ('post',): + params.update( + request=end_point, + nonce=self._nonce + ) + json_params = json.dumps(params or {}).encode() + payload = base64.b64encode(json_params) + signature = hmac.new(self._secret, payload, hashlib.sha384) + request_args.update(headers={ + 'X-GEMINI-PAYLOAD': payload, + 'X-GEMINI-SIGNATURE': signature.hexdigest(), + }) + else: + request_args.update(params=params) + + if self._proxy: + request_args.update(proxies=self._proxy) + + response = requests.request(**request_args) + + if response: + if response.ok: + return response.json() + else: + # elif any('json' in h for h in response.headers): + try: + data = response.json() + return {'error': data} + except json.JSONDecodeError: + return { + 'error': { + 'code': response.status_code, + 'reason': response.reason, + 'content': response.text + } + } + except (requests.RequestException, requests.ConnectionError): + response.raise_for_status() + + @property + def _nonce(self): + """Return the current millisecond timestamp as the nonce. + + :return: current millisecond timestamp as the nonce + """ + return f'{time.time() * self._nonce_mult:.0f}' + + + + +class GeminiPublic(Client): + + # ================ PUBLIC END POINTS ================ + def get_symbols(self): + """Send a request to get trading symbols info.""" + return self._request('symbols') + + def get_ticker(self, symbol): + """Send a request to get a trading symbol ticker info. + + :param symbol: symbol id to query (example: "btcusd") + :return: trading symbol ticker info supplied by server. + """ + return self._request(f'pubticker/{symbol}') + + def get_orderbook(self, symbol, limit_bids=0, limit_asks=0): + """Send a request to get a symbol order book data. + + :param symbol: symbol id to query (example: "btcusd") + :param limit_bids: limit the number of bids returned (default 0) + :param limit_asks: limit the number of asks returned (default 0) + :return: symbol order book info supplied by server. + """ + params = _get_params(locals(), 'symbol') + return self._request(f'book/{symbol}', params=params) + + def get_trades(self, symbol, since=0, limit_trades=50, include_breaks=0): + """Send a request to get a symbol public trades. + + :param str symbol: symbol id to query (example: "btcusd") + :param int since: return trades after this unix epoch (default 0) + :param limit_trades: max number of trades to return (default 50). + :param int include_breaks: whether to display broken trades (default 0) + :return: supplied symbol public trades info supplied by server. + """ + params = _get_params(locals(), 'symbol') + return self._request(f'trades/{symbol}', params=params) + + def get_auction(self, symbol): + """Send a request to get a symbol latest auction info. + + :param str symbol: symbol id to query (example: "btcusd") + :return: supplied symbol latest auction supplied by server. + """ + return self._request(f'auction/{symbol}') + + def get_auction_history(self, symbol, since=0, limit_auction_results=50, include_indicative=1): + """Send a request to get a symbol auction history. + + :param str symbol: symbol id to query (example: "btcusd") + :param since: only return auction events after this timestamp. + :param limit_auction_results: max number of auction events to return. + :param include_indicative: set to 0 to not include publication of indicative info. + :return: + """ + params = _get_params(locals(), 'symbol') + return self._request(f'auction/{symbol}/history', params) + + +class Geminipy(GeminiPublic): + """Gemini API wrapper. + + Make public or authenticated requests according to the API documentation: + https://docs.gemini.com/ + """ + + # ================ AUTHENTICATED ENDPOINTS ================ + + def place_order(self, side, symbol, amount, price=None, client_order_id=None, type=None, options=None): + """Send a request to place an order. + + :param str side: 'buy' or 'sell' + :param str symbol: symbol id to query (example: "btcusd") + :param float amount: symbol amount to order. + :param float price: order price. + :param client_order_id: an optional custom order indetifier. + :param type: order type (default 'exchange limit') + :param options: + return: + """ + params = _get_params(locals(), 'amount', 'price') + params.update(amount=f'{amount}') + return self._request('order/new', 'post', params) + + def cancel_order(self, order_id): + """Send a request to cancel an order. + + :param str order_id: order id to cancel. + :return: + """ + params = _get_params(locals()) + return self._request('order/cancel', 'post', params) + + def cancel_session(self): + """Send a request to cancel all session orders. + + :return: + """ + return self._request('order/cancel/session', 'post') + + def cancel_all(self): + """Send a request to cancel all orders. + + :return: + """ + return self._request('order/cancel/all', 'post') + + def get_order_status(self, order_id): + """Send a request to get an order current status. + + :param str order_id: the order id to get information on. + :return: + """ + params = _get_params(locals()) + return self._request('order/status', 'post', params) + + def get_active_orders(self): + """Send a request to get active orders. + + :return: + """ + return self._request('orders', 'post') + + def get_past_trades(self, symbol, limit_trades=50, timestamp=0): + """Send a symbol trades history request. + + :param str symbol: symbol id to query (example: "btcusd") + :param limit_trades: max number of trades to return (default 50) + :param timestamp: return trades after supplied unix epoch (default 0) + :return: + """ + params = _get_params(locals()) + return self._request('mytrades', 'post', params) + + def get_trade_volume(self): + """Send a request to get your trade volume.""" + return self._request('tradevolume', 'post') + + def get_balances(self): + """Send an account balance request. + + :return: + """ + return self._request('balances', 'post') + + def new_deposit_address(self, currency, label=None): + """Send a request to generate a new cryptocurrency deposit address. + + :param str currency: crypto currency ID (example: btc) + :param str label: optional label for the deposit address + :return: + """ + params = _get_params(locals(), 'currency') + return self._request(f'deposit/{currency}/newAddress', 'post', params) + + def get_notional_balances(self, currency, account=None): + """Get approved addresses for supplied network and account (if supplied) + + :param str currency: currency to get balance for. + :param str account: account name (example: primary) + :return: + """ + params = _get_params(locals(), 'currency') + return self._request(f'notionalbalances/{currency}', 'post', params) + + def remove_approved_address(self, network, address, account=None): + """Get approved addresses for supplied network and account (if supplied) + + :param str network: accepted values: bitcoin, ethereum, bitcoincash, litecoin, zcash, filecoin, dogecoin, tezos + :param str address: address to remove. + :param str account: account name (example: primary) + :return: + """ + params = _get_params(locals(), 'network') + return self._request(f'approvedAddresses/account/{network}/remove', 'post', params) + + def request_approved_address(self, network, address, label=None, account=None): + """Get approved addresses for supplied network and account (if supplied) + + :param str network: accepted values: bitcoin, ethereum, bitcoincash, litecoin, zcash, filecoin, dogecoin, tezos + :param str address: address to remove. + :param str label: a label to identify the supplied address. + :param str account: account name (example: primary) + :return: + """ + params = _get_params(locals(), 'network') + return self._request(f'approvedAddresses/{network}/request', 'post', params) + + def get_fees(self): + """Send a request to get fees and notional volume.""" + self._request('notionalvolume', 'post') + + def get_earn_balance(self, account=None): + """Send an account earn balance request. + + :param str account: account name (example: primary) + :return: + """ + params = _get_params(locals()) + return self._request('balances/earn', 'post', params) + + def get_account_details(self): + """Send a request to get account details related to supplied API keys. + + :return: + """ + return self._request('account', 'post') + + def get_heartbeat(self): + """Send a heartbeat message. + + :return: + """ + return self._request('heartbeat', 'post') diff --git a/geminipy/utils.py b/geminipy/utils.py new file mode 100644 index 0000000..5941b80 --- /dev/null +++ b/geminipy/utils.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +from typing import Dict + + +def _get_params(params: Dict, *exclude) -> Dict: + check = lambda k, v: v is not None and k[0].isalpha() and k not in exclude + return {k: v for k, v in params.items() if check(k, v) and k != 'self'} + diff --git a/setup.py b/setup.py index 49f2bcd..1966ab6 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,5 @@ # -*- coding:utf-8 -*- -from setuptools import setup +from setuptools import setup, find_packages from pathlib import Path readme = Path('README.md') @@ -10,8 +10,8 @@ setup( name='geminipy', - version='0.0.5', - packages=['geminipy'], + version='0.0.6', + packages=find_packages('geminipy'), url='https://github.com/pl0mo/geminipy', license='GNU GPL', author='Mike Marzigliano', From b203018944c4bd488059f7ff812a588f3439fbb9 Mon Sep 17 00:00:00 2001 From: godmin Date: Mon, 1 Nov 2021 03:48:22 +0000 Subject: [PATCH 06/17] Code organization by creating "utils.py" and "core.py" files. Some errors fixed on README examples. Using find_packages in "setup.py" file. --- .gitignore | 4 + changelog | 15 +- geminipy/__init__.py | 312 +---------------------------------------- geminipy/core.py | 323 +++++++++++++++++++++++++++++++++++++++++++ geminipy/utils.py | 8 ++ setup.py | 6 +- 6 files changed, 346 insertions(+), 322 deletions(-) create mode 100644 geminipy/core.py create mode 100644 geminipy/utils.py diff --git a/.gitignore b/.gitignore index fbf2807..1f0d331 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,7 @@ target/ #Ipython Notebook .ipynb_checkpoints + + +# Pycharm +.idea diff --git a/changelog b/changelog index cde3c90..33951d0 100644 --- a/changelog +++ b/changelog @@ -1,16 +1,9 @@ # Geminipy Changelog -# v0.0.5 -Code update to python 3.6 or newer version. -Code simplied and refactored -Created new method "_requests" to unifiy all requests. -Code comments changed. -Method names changed for more friendly and autodoc ones. -New class Geminipy method "get_account_details" -New class Geminipy method "get_earn_balance" -New class Geminipy method "request_approved_address" -New class Geminipy method "remove_approved_address" -New class Geminipy method "get_notional_balances" +# v0.0.6 +Code organization by creating "utils.py" and "core.py" files. +Some errors fixed on README examples. +Using findpackages in "setup.py" file. # v0.0.5 Code update to python 3.6 or newer version. diff --git a/geminipy/__init__.py b/geminipy/__init__.py index d1373f7..b51bf76 100644 --- a/geminipy/__init__.py +++ b/geminipy/__init__.py @@ -4,314 +4,10 @@ Author: Mike Marzigliano """ -import base64 -import hashlib -import hmac -import json -import time -from typing import Dict +from geminipy.core import Geminipy -import requests +# Geminipy = GeminiAPI -def _get_params(params: Dict, *exclude) -> Dict: - check = lambda k, v: v is not None and k[0].isalpha() and k not in exclude - return {k: v for k, v in params.items() if check(k, v) and k != 'self'} - - -class Geminipy: - """Gemini API wrapper. - - Make public or authenticated requests according to the API documentation: - https://docs.gemini.com/ - """ - - def __init__(self, - apikey=None, - secret=None, - live=True, - nonce_mult=1000.0, - api_version=1, - timeout=15, - proxy=None): - """Initialize the class. - - :param str apikey: your Gemini API key. - :param str secret: your Gemini API secret key for signatures. - :param float nonce_mult: a custom multiplier for nonce value. - """ - # self._api_version = api_version - if live: - self._url = f'https://api.gemini.com' - else: - self._url = f'https://api.sandbox.gemini.com' - self._proxy = {'http': proxy} if proxy else None - self._timeout = timeout - self._url = f'{self._url}/v{api_version}' - self._apikey = apikey - self._nonce_mult = nonce_mult - self._secret = secret.encode() - self._headers = {'X-GEMINI-APIKEY': self._apikey} - - # ================ Class private methods ================ - - def _request(self, end_point, method=None, params=None): - method = str(method or 'get').lower() - - request_args = { - 'url': f'{self._url}/{end_point}', - 'method': method, - 'timeout': self._timeout, - 'headers': self._headers - } - - if params: - if method in ('post',): - params.update( - request=end_point, - nonce=self._nonce - ) - json_params = json.dumps(params or {}).encode() - payload = base64.b64encode(json_params) - signature = hmac.new(self._secret, payload, hashlib.sha384) - request_args.update(headers={ - 'X-GEMINI-PAYLOAD': payload, - 'X-GEMINI-SIGNATURE': signature.hexdigest(), - }) - else: - request_args.update(params=params) - - if self._proxy: - request_args.update(proxies=self._proxy) - - response = requests.request(**request_args) - - if response: - if response.ok: - return response.json() - else: - # elif any('json' in h for h in response.headers): - try: - data = response.json() - return {'error': data} - except json.JSONDecodeError: - return { - 'error': { - 'code': response.status_code, - 'reason': response.reason, - 'content': response.text - } - } - except (requests.RequestException, requests.ConnectionError): - response.raise_for_status() - - @property - def _nonce(self): - """Return the current millisecond timestamp as the nonce. - - :return: current millisecond timestamp as the nonce - """ - return f'{time.time() * self._nonce_mult:.0f}' - - # ================ PUBLIC END POINTS ================ - - def get_symbols(self): - """Send a request to get trading symbols info.""" - return self._request('symbols') - - def get_ticker(self, symbol): - """Send a request to get a trading symbol ticker info. - - :param symbol: symbol id to query (example: "btcusd") - :return: trading symbol ticker info supplied by server. - """ - return self._request(f'pubticker/{symbol}') - - def get_orderbook(self, symbol, limit_bids=0, limit_asks=0): - """Send a request to get a symbol order book data. - - :param symbol: symbol id to query (example: "btcusd") - :param limit_bids: limit the number of bids returned (default 0) - :param limit_asks: limit the number of asks returned (default 0) - :return: symbol order book info supplied by server. - """ - params = _get_params(locals(), 'symbol') - return self._request(f'book/{symbol}', params=params) - - def get_trades(self, symbol, since=0, limit_trades=50, include_breaks=0): - """Send a request to get a symbol public trades. - - :param str symbol: symbol id to query (example: "btcusd") - :param int since: return trades after this unix epoch (default 0) - :param limit_trades: max number of trades to return (default 50). - :param int include_breaks: whether to display broken trades (default 0) - :return: supplied symbol public trades info supplied by server. - """ - params = _get_params(locals(), 'symbol') - return self._request(f'trades/{symbol}', params=params) - - def get_auction(self, symbol): - """Send a request to get a symbol latest auction info. - - :param str symbol: symbol id to query (example: "btcusd") - :return: supplied symbol latest auction supplied by server. - """ - return self._request(f'auction/{symbol}') - - def get_auction_history(self, symbol, since=0, limit_auction_results=50, include_indicative=1): - """Send a request to get a symbol auction history. - - :param str symbol: symbol id to query (example: "btcusd") - :param since: only return auction events after this timestamp. - :param limit_auction_results: max number of auction events to return. - :param include_indicative: set to 0 to not include publication of indicative info. - :return: - """ - params = _get_params(locals(), 'symbol') - return self._request(f'auction/{symbol}/history', params) - - # ================ AUTHENTICATED ENDPOINTS ================ - - def place_order(self, side, symbol, amount, price=None, client_order_id=None, type=None, options=None): - """Send a request to place an order. - - :param str side: 'buy' or 'sell' - :param str symbol: symbol id to query (example: "btcusd") - :param float amount: symbol amount to order. - :param float price: order price. - :param client_order_id: an optional custom order indetifier. - :param type: order type (default 'exchange limit') - :param options: - return: - """ - params = _get_params(locals(), 'amount', 'price') - params.update(amount=f'{amount}') - return self._request('order/new', 'post', params) - - def cancel_order(self, order_id): - """Send a request to cancel an order. - - :param str order_id: order id to cancel. - :return: - """ - params = _get_params(locals()) - return self._request('order/cancel', 'post', params) - - def cancel_session(self): - """Send a request to cancel all session orders. - - :return: - """ - return self._request('order/cancel/session', 'post') - - def cancel_all(self): - """Send a request to cancel all orders. - - :return: - """ - return self._request('order/cancel/all', 'post') - - def get_order_status(self, order_id): - """Send a request to get an order current status. - - :param str order_id: the order id to get information on. - :return: - """ - params = _get_params(locals()) - return self._request('order/status', 'post', params) - - def get_active_orders(self): - """Send a request to get active orders. - - :return: - """ - return self._request('orders', 'post') - - def get_past_trades(self, symbol, limit_trades=50, timestamp=0): - """Send a symbol trades history request. - - :param str symbol: symbol id to query (example: "btcusd") - :param limit_trades: max number of trades to return (default 50) - :param timestamp: return trades after supplied unix epoch (default 0) - :return: - """ - params = _get_params(locals()) - return self._request('mytrades', 'post', params) - - def get_trade_volume(self): - """Send a request to get your trade volume.""" - return self._request('tradevolume', 'post') - - def get_balances(self): - """Send an account balance request. - - :return: - """ - return self._request('balances', 'post') - - def new_deposit_address(self, currency, label=None): - """Send a request to generate a new cryptocurrency deposit address. - - :param str currency: crypto currency ID (example: btc) - :param str label: optional label for the deposit address - :return: - """ - params = _get_params(locals(), 'currency') - return self._request(f'deposit/{currency}/newAddress', 'post', params) - - def get_notional_balances(self, currency, account=None): - """Get approved addresses for supplied network and account (if supplied) - - :param str currency: currency to get balance for. - :param str account: account name (example: primary) - :return: - """ - params = _get_params(locals(), 'currency') - return self._request(f'notionalbalances/{currency}', 'post', params) - - def remove_approved_address(self, network, address, account=None): - """Get approved addresses for supplied network and account (if supplied) - - :param str network: accepted values: bitcoin, ethereum, bitcoincash, litecoin, zcash, filecoin, dogecoin, tezos - :param str address: address to remove. - :param str account: account name (example: primary) - :return: - """ - params = _get_params(locals(), 'network') - return self._request(f'approvedAddresses/account/{network}/remove', 'post', params) - - def request_approved_address(self, network, address, label=None, account=None): - """Get approved addresses for supplied network and account (if supplied) - - :param str network: accepted values: bitcoin, ethereum, bitcoincash, litecoin, zcash, filecoin, dogecoin, tezos - :param str address: address to remove. - :param str label: a label to identify the supplied address. - :param str account: account name (example: primary) - :return: - """ - params = _get_params(locals(), 'network') - return self._request(f'approvedAddresses/{network}/request', 'post', params) - - def get_fees(self): - """Send a request to get fees and notional volume.""" - self._request('notionalvolume', 'post') - - def get_earn_balance(self, account=None): - """Send an account earn balance request. - - :param str account: account name (example: primary) - :return: - """ - params = _get_params(locals()) - return self._request('balances/earn', 'post', params) - - def get_account_details(self): - return self._request('account', 'post') - - def get_heartbeat(self): - """Send a heartbeat message. - - :return: - """ - return self._request('heartbeat', 'post') - +__package__ = 'geminipy' +__all__ = ['Geminipy'] diff --git a/geminipy/core.py b/geminipy/core.py new file mode 100644 index 0000000..be0c133 --- /dev/null +++ b/geminipy/core.py @@ -0,0 +1,323 @@ +# -*- coding: utf-8 -*- +import base64 +import hashlib +import hmac +import json +import time + +import requests + +from geminipy import _get_params + + +class Client: + """Gemini API wrapper. + + Make public or authenticated requests according to the API documentation: + https://docs.gemini.com/ + """ + + def __init__(self, + apikey=None, + secret=None, + live=True, + nonce_mult=1000.0, + api_version=1, + timeout=15, + proxy=None): + """Initialize the class. + + :param str apikey: your Gemini API key. + :param str secret: your Gemini API secret key for signatures. + :param float nonce_mult: a custom multiplier for nonce value. + """ + # self._api_version = api_version + if live: + self._url = f'https://api.gemini.com' + else: + self._url = f'https://api.sandbox.gemini.com' + self._proxy = {'http': proxy} if proxy else None + self._timeout = timeout + self._url = f'{self._url}/v{api_version}' + self._apikey = apikey + self._nonce_mult = nonce_mult + self._secret = secret.encode() + self._headers = {'X-GEMINI-APIKEY': self._apikey} + + # ================ Class private methods ================ + + def _request(self, end_point, method=None, params=None): + method = str(method or 'get').lower() + + request_args = { + 'url': f'{self._url}/{end_point}', + 'method': method, + 'timeout': self._timeout, + 'headers': self._headers + } + + if params: + if method in ('post',): + params.update( + request=end_point, + nonce=self._nonce + ) + json_params = json.dumps(params or {}).encode() + payload = base64.b64encode(json_params) + signature = hmac.new(self._secret, payload, hashlib.sha384) + request_args.update(headers={ + 'X-GEMINI-PAYLOAD': payload, + 'X-GEMINI-SIGNATURE': signature.hexdigest(), + }) + else: + request_args.update(params=params) + + if self._proxy: + request_args.update(proxies=self._proxy) + + response = requests.request(**request_args) + + if response: + if response.ok: + return response.json() + else: + # elif any('json' in h for h in response.headers): + try: + data = response.json() + return {'error': data} + except json.JSONDecodeError: + return { + 'error': { + 'code': response.status_code, + 'reason': response.reason, + 'content': response.text + } + } + except (requests.RequestException, requests.ConnectionError): + response.raise_for_status() + + @property + def _nonce(self): + """Return the current millisecond timestamp as the nonce. + + :return: current millisecond timestamp as the nonce + """ + return f'{time.time() * self._nonce_mult:.0f}' + + + + +class GeminiPublic(Client): + + # ================ PUBLIC END POINTS ================ + def get_symbols(self): + """Send a request to get trading symbols info.""" + return self._request('symbols') + + def get_ticker(self, symbol): + """Send a request to get a trading symbol ticker info. + + :param symbol: symbol id to query (example: "btcusd") + :return: trading symbol ticker info supplied by server. + """ + return self._request(f'pubticker/{symbol}') + + def get_orderbook(self, symbol, limit_bids=0, limit_asks=0): + """Send a request to get a symbol order book data. + + :param symbol: symbol id to query (example: "btcusd") + :param limit_bids: limit the number of bids returned (default 0) + :param limit_asks: limit the number of asks returned (default 0) + :return: symbol order book info supplied by server. + """ + params = _get_params(locals(), 'symbol') + return self._request(f'book/{symbol}', params=params) + + def get_trades(self, symbol, since=0, limit_trades=50, include_breaks=0): + """Send a request to get a symbol public trades. + + :param str symbol: symbol id to query (example: "btcusd") + :param int since: return trades after this unix epoch (default 0) + :param limit_trades: max number of trades to return (default 50). + :param int include_breaks: whether to display broken trades (default 0) + :return: supplied symbol public trades info supplied by server. + """ + params = _get_params(locals(), 'symbol') + return self._request(f'trades/{symbol}', params=params) + + def get_auction(self, symbol): + """Send a request to get a symbol latest auction info. + + :param str symbol: symbol id to query (example: "btcusd") + :return: supplied symbol latest auction supplied by server. + """ + return self._request(f'auction/{symbol}') + + def get_auction_history(self, symbol, since=0, limit_auction_results=50, include_indicative=1): + """Send a request to get a symbol auction history. + + :param str symbol: symbol id to query (example: "btcusd") + :param since: only return auction events after this timestamp. + :param limit_auction_results: max number of auction events to return. + :param include_indicative: set to 0 to not include publication of indicative info. + :return: + """ + params = _get_params(locals(), 'symbol') + return self._request(f'auction/{symbol}/history', params) + + +class Geminipy(GeminiPublic): + """Gemini API wrapper. + + Make public or authenticated requests according to the API documentation: + https://docs.gemini.com/ + """ + + # ================ AUTHENTICATED ENDPOINTS ================ + + def place_order(self, side, symbol, amount, price=None, client_order_id=None, type=None, options=None): + """Send a request to place an order. + + :param str side: 'buy' or 'sell' + :param str symbol: symbol id to query (example: "btcusd") + :param float amount: symbol amount to order. + :param float price: order price. + :param client_order_id: an optional custom order indetifier. + :param type: order type (default 'exchange limit') + :param options: + return: + """ + params = _get_params(locals(), 'amount', 'price') + params.update(amount=f'{amount}') + return self._request('order/new', 'post', params) + + def cancel_order(self, order_id): + """Send a request to cancel an order. + + :param str order_id: order id to cancel. + :return: + """ + params = _get_params(locals()) + return self._request('order/cancel', 'post', params) + + def cancel_session(self): + """Send a request to cancel all session orders. + + :return: + """ + return self._request('order/cancel/session', 'post') + + def cancel_all(self): + """Send a request to cancel all orders. + + :return: + """ + return self._request('order/cancel/all', 'post') + + def get_order_status(self, order_id): + """Send a request to get an order current status. + + :param str order_id: the order id to get information on. + :return: + """ + params = _get_params(locals()) + return self._request('order/status', 'post', params) + + def get_active_orders(self): + """Send a request to get active orders. + + :return: + """ + return self._request('orders', 'post') + + def get_past_trades(self, symbol, limit_trades=50, timestamp=0): + """Send a symbol trades history request. + + :param str symbol: symbol id to query (example: "btcusd") + :param limit_trades: max number of trades to return (default 50) + :param timestamp: return trades after supplied unix epoch (default 0) + :return: + """ + params = _get_params(locals()) + return self._request('mytrades', 'post', params) + + def get_trade_volume(self): + """Send a request to get your trade volume.""" + return self._request('tradevolume', 'post') + + def get_balances(self): + """Send an account balance request. + + :return: + """ + return self._request('balances', 'post') + + def new_deposit_address(self, currency, label=None): + """Send a request to generate a new cryptocurrency deposit address. + + :param str currency: crypto currency ID (example: btc) + :param str label: optional label for the deposit address + :return: + """ + params = _get_params(locals(), 'currency') + return self._request(f'deposit/{currency}/newAddress', 'post', params) + + def get_notional_balances(self, currency, account=None): + """Get approved addresses for supplied network and account (if supplied) + + :param str currency: currency to get balance for. + :param str account: account name (example: primary) + :return: + """ + params = _get_params(locals(), 'currency') + return self._request(f'notionalbalances/{currency}', 'post', params) + + def remove_approved_address(self, network, address, account=None): + """Get approved addresses for supplied network and account (if supplied) + + :param str network: accepted values: bitcoin, ethereum, bitcoincash, litecoin, zcash, filecoin, dogecoin, tezos + :param str address: address to remove. + :param str account: account name (example: primary) + :return: + """ + params = _get_params(locals(), 'network') + return self._request(f'approvedAddresses/account/{network}/remove', 'post', params) + + def request_approved_address(self, network, address, label=None, account=None): + """Get approved addresses for supplied network and account (if supplied) + + :param str network: accepted values: bitcoin, ethereum, bitcoincash, litecoin, zcash, filecoin, dogecoin, tezos + :param str address: address to remove. + :param str label: a label to identify the supplied address. + :param str account: account name (example: primary) + :return: + """ + params = _get_params(locals(), 'network') + return self._request(f'approvedAddresses/{network}/request', 'post', params) + + def get_fees(self): + """Send a request to get fees and notional volume.""" + self._request('notionalvolume', 'post') + + def get_earn_balance(self, account=None): + """Send an account earn balance request. + + :param str account: account name (example: primary) + :return: + """ + params = _get_params(locals()) + return self._request('balances/earn', 'post', params) + + def get_account_details(self): + """Send a request to get account details related to supplied API keys. + + :return: + """ + return self._request('account', 'post') + + def get_heartbeat(self): + """Send a heartbeat message. + + :return: + """ + return self._request('heartbeat', 'post') diff --git a/geminipy/utils.py b/geminipy/utils.py new file mode 100644 index 0000000..5941b80 --- /dev/null +++ b/geminipy/utils.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +from typing import Dict + + +def _get_params(params: Dict, *exclude) -> Dict: + check = lambda k, v: v is not None and k[0].isalpha() and k not in exclude + return {k: v for k, v in params.items() if check(k, v) and k != 'self'} + diff --git a/setup.py b/setup.py index 49f2bcd..1966ab6 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,5 @@ # -*- coding:utf-8 -*- -from setuptools import setup +from setuptools import setup, find_packages from pathlib import Path readme = Path('README.md') @@ -10,8 +10,8 @@ setup( name='geminipy', - version='0.0.5', - packages=['geminipy'], + version='0.0.6', + packages=find_packages('geminipy'), url='https://github.com/pl0mo/geminipy', license='GNU GPL', author='Mike Marzigliano', From f2f225d37ab7b1c5b8d8b97c889d3097d8b1d619 Mon Sep 17 00:00:00 2001 From: godmin Date: Mon, 1 Nov 2021 03:48:22 +0000 Subject: [PATCH 07/17] Code organization by creating "utils.py" and "core.py" files. Some errors fixed on README examples. Using find_packages in "setup.py" file. --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 1966ab6..49f2bcd 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,5 @@ # -*- coding:utf-8 -*- -from setuptools import setup, find_packages +from setuptools import setup from pathlib import Path readme = Path('README.md') @@ -10,8 +10,8 @@ setup( name='geminipy', - version='0.0.6', - packages=find_packages('geminipy'), + version='0.0.5', + packages=['geminipy'], url='https://github.com/pl0mo/geminipy', license='GNU GPL', author='Mike Marzigliano', From 3c4037ea72871eb8c36d1319a242e72a431323c5 Mon Sep 17 00:00:00 2001 From: pl0mo <76693623+pl0mo@users.noreply.github.com> Date: Mon, 1 Nov 2021 04:23:08 +0000 Subject: [PATCH 08/17] Restored packages attribute Restored packages attribute on setup.py file. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1966ab6..4b844d1 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name='geminipy', version='0.0.6', - packages=find_packages('geminipy'), + packages=['geminipy'], url='https://github.com/pl0mo/geminipy', license='GNU GPL', author='Mike Marzigliano', From 509999e8440355b38d328f06b3bd828b6d3edece Mon Sep 17 00:00:00 2001 From: pl0mo <76693623+pl0mo@users.noreply.github.com> Date: Mon, 1 Nov 2021 04:25:17 +0000 Subject: [PATCH 09/17] Update setup.py Restored packages value. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1966ab6..4b844d1 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name='geminipy', version='0.0.6', - packages=find_packages('geminipy'), + packages=['geminipy'], url='https://github.com/pl0mo/geminipy', license='GNU GPL', author='Mike Marzigliano', From 04b6b6c1e8ec9a7a155cbf44afb5a169f449890a Mon Sep 17 00:00:00 2001 From: pl0mo <76693623+pl0mo@users.noreply.github.com> Date: Mon, 1 Nov 2021 04:29:00 +0000 Subject: [PATCH 10/17] Update setup.py Removed find_packages function from import --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4b844d1..b890eda 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,5 @@ # -*- coding:utf-8 -*- -from setuptools import setup, find_packages +from setuptools import setup from pathlib import Path readme = Path('README.md') From 87b9df626ec1019f5751302a04bbaf9aaed42310 Mon Sep 17 00:00:00 2001 From: pl0mo <76693623+pl0mo@users.noreply.github.com> Date: Mon, 1 Nov 2021 04:29:38 +0000 Subject: [PATCH 11/17] Update core.py Fixed import error --- geminipy/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geminipy/core.py b/geminipy/core.py index be0c133..8421627 100644 --- a/geminipy/core.py +++ b/geminipy/core.py @@ -7,7 +7,7 @@ import requests -from geminipy import _get_params +from geminipy.utils import _get_params class Client: From f3a8d26937636b8bed535ee59c4b473dee67d79a Mon Sep 17 00:00:00 2001 From: godmin Date: Mon, 1 Nov 2021 15:51:35 +0000 Subject: [PATCH 12/17] Added constants.py file Added handle error method. Added get_approved_addresses method. --- geminipy/constants.py | 96 +++++++++++++++++++++++++++++++++++++++ geminipy/core.py | 101 ++++++++++++++++++++++++++++-------------- setup.py | 4 +- 3 files changed, 166 insertions(+), 35 deletions(-) create mode 100644 geminipy/constants.py diff --git a/geminipy/constants.py b/geminipy/constants.py new file mode 100644 index 0000000..d6b7abc --- /dev/null +++ b/geminipy/constants.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- + +SYMBOLS = [ + '1inchusd', + 'aaveusd', + 'alcxusd', + 'ampusd', + 'ankrusd', + 'axsusd', + 'balusd', + 'batbtc', + 'bateth', + 'batusd', + 'bchbtc', + 'bcheth', + 'bchusd', + 'bntusd', + 'bondusd', + 'btcdai', + 'btceur', + 'btcgbp', + 'btcgusd', + 'btcsgd', + 'btcusd', + 'compusd', + 'crvusd', + 'ctxusd', + 'cubeusd', + 'daiusd', + 'dogebtc', + 'dogeeth' + 'dogeusd', + 'efilfil', + 'enjusd', + 'ethbtc', + 'ethdai', + 'etheur', + 'ethgbp', + 'ethgusd', + 'ethsgd', + 'ethusd', + 'filusd', + 'ftmusd', + 'grtusd', + 'gusdusd', + 'injusd', + 'kncusd', + 'linkbtc', + 'linketh', + 'linkusd', + 'lptusd', + 'lrcusd', + 'ltcbch', + 'ltcbtc', + 'ltceth', + 'ltcusd', + 'lunausd', + 'manausd', + 'maticusd', + 'mco2usd', + 'mirusd', + 'mkrusd', + 'oxtbtc', + 'oxteth', + 'oxtusd', + 'paxgusd', + 'renusd', + 'sandusd', + 'sklusd', + 'slpusd', + 'snxusd', + 'storjusd', + 'sushiusd', + 'umausd', + 'uniusd', + 'ustusd', + 'xtzusd', + 'yfiusd', + 'zecbch', + 'zecbtc', + 'zeceth', + 'zecltc', + 'zecusd', + 'zrxusd' +] + +NETWORKS = [ + 'bitcoin', + 'ethereum', + 'bitcoincash', + 'litecoin', + 'zcash', + 'filecoin', + 'dogecoin', + 'tezos' +] diff --git a/geminipy/core.py b/geminipy/core.py index be0c133..468f615 100644 --- a/geminipy/core.py +++ b/geminipy/core.py @@ -7,7 +7,8 @@ import requests -from geminipy import _get_params +from geminipy.utils import _get_params +from geminipy.constants import NETWORKS class Client: @@ -36,9 +37,10 @@ def __init__(self, self._url = f'https://api.gemini.com' else: self._url = f'https://api.sandbox.gemini.com' + self._api_version = f'v{api_version:d}' self._proxy = {'http': proxy} if proxy else None self._timeout = timeout - self._url = f'{self._url}/v{api_version}' + self._url = f'{self._url}/{self._api_version}' self._apikey = apikey self._nonce_mult = nonce_mult self._secret = secret.encode() @@ -56,45 +58,67 @@ def _request(self, end_point, method=None, params=None): 'headers': self._headers } - if params: - if method in ('post',): - params.update( - request=end_point, - nonce=self._nonce - ) - json_params = json.dumps(params or {}).encode() - payload = base64.b64encode(json_params) - signature = hmac.new(self._secret, payload, hashlib.sha384) - request_args.update(headers={ - 'X-GEMINI-PAYLOAD': payload, - 'X-GEMINI-SIGNATURE': signature.hexdigest(), - }) - else: - request_args.update(params=params) + params = dict(params or {}) + + if method in ('post',): + params.update( + request=f'/{self._api_version}/{end_point}', + nonce=self._nonce + ) + json_params = json.dumps(params or {}).encode() + payload = base64.b64encode(json_params) + signature = hmac.new(self._secret, payload, hashlib.sha384) + request_args.update(headers={ + 'X-GEMINI-PAYLOAD': payload, + 'X-GEMINI-SIGNATURE': signature.hexdigest(), + **request_args['headers'] + }) + else: + request_args.update(params=params) if self._proxy: request_args.update(proxies=self._proxy) response = requests.request(**request_args) - if response: + if response is not None: if response.ok: return response.json() else: - # elif any('json' in h for h in response.headers): - try: - data = response.json() - return {'error': data} - except json.JSONDecodeError: - return { - 'error': { - 'code': response.status_code, - 'reason': response.reason, - 'content': response.text - } + return self._error_handler(response, end_point, method, params) + + def _error_handler(self, response, end_point, method=None, params=None): + try: + data = response.json() + if data: + if 'reason' in data: + if 'InvalidNonce' in data['reason']: + time.sleep(1.5) + if self._nonce_mult >= 1000.0 ** 2: + self._nonce_mult *= 10.0 + else: + self._nonce_mult **= 2 + return self._request(end_point, method, params) + return {'error': data} + else: + return { + 'error': { + 'code': response.status_code, + 'reason': response.reason, + 'content': response.text } - except (requests.RequestException, requests.ConnectionError): - response.raise_for_status() + } + except json.JSONDecodeError: + return { + 'error': { + 'code': response.status_code, + 'reason': response.reason, + 'content': response.text + } + } + except (requests.RequestException, requests.ConnectionError): + response.raise_for_status() + return {'error': 'Unknown error.'} @property def _nonce(self): @@ -105,8 +129,6 @@ def _nonce(self): return f'{time.time() * self._nonce_mult:.0f}' - - class GeminiPublic(Client): # ================ PUBLIC END POINTS ================ @@ -252,6 +274,17 @@ def get_balances(self): """ return self._request('balances', 'post') + def get_approved_addresses(self, network, label=None): + """Get an approved addresses request for supplied network. + + :param str network: accepted vales -> bitcoin, ethereum, bitcoincash, litecoin, zcash, filecoin, dogecoin, tezos + :param str label: account name (example: primary) + :return: + """ + assert str(network).lower() in NETWORKS, f'Invalid network {network}' + params = _get_params(locals(), 'network') + return self._request(f'approvedAddresses/account/{network}', 'post', params) + def new_deposit_address(self, currency, label=None): """Send a request to generate a new cryptocurrency deposit address. @@ -280,6 +313,7 @@ def remove_approved_address(self, network, address, account=None): :param str account: account name (example: primary) :return: """ + assert str(network).lower() in NETWORKS, f'Invalid network {network}' params = _get_params(locals(), 'network') return self._request(f'approvedAddresses/account/{network}/remove', 'post', params) @@ -292,6 +326,7 @@ def request_approved_address(self, network, address, label=None, account=None): :param str account: account name (example: primary) :return: """ + assert str(network).lower() in NETWORKS, f'Invalid network {network}' params = _get_params(locals(), 'network') return self._request(f'approvedAddresses/{network}/request', 'post', params) diff --git a/setup.py b/setup.py index 1966ab6..b890eda 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,5 @@ # -*- coding:utf-8 -*- -from setuptools import setup, find_packages +from setuptools import setup from pathlib import Path readme = Path('README.md') @@ -11,7 +11,7 @@ setup( name='geminipy', version='0.0.6', - packages=find_packages('geminipy'), + packages=['geminipy'], url='https://github.com/pl0mo/geminipy', license='GNU GPL', author='Mike Marzigliano', From 7992898b83424c39a2ca6c4c5484513cebf96abe Mon Sep 17 00:00:00 2001 From: SalihSoleh Date: Sun, 20 Feb 2022 10:42:13 +0000 Subject: [PATCH 13/17] Some code refactors (Networks moved to new file "model.py" as class) --- geminipy/constants.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/geminipy/constants.py b/geminipy/constants.py index d6b7abc..23ff529 100644 --- a/geminipy/constants.py +++ b/geminipy/constants.py @@ -84,13 +84,3 @@ 'zrxusd' ] -NETWORKS = [ - 'bitcoin', - 'ethereum', - 'bitcoincash', - 'litecoin', - 'zcash', - 'filecoin', - 'dogecoin', - 'tezos' -] From bed712ea17b7907114eed61a649f2d3e3fbe59ad Mon Sep 17 00:00:00 2001 From: SalihSoleh Date: Sun, 20 Feb 2022 10:44:18 +0000 Subject: [PATCH 14/17] New file. Networks class created to made crypto 3 chars code and their networks names easily. --- geminipy/model.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 geminipy/model.py diff --git a/geminipy/model.py b/geminipy/model.py new file mode 100644 index 0000000..be5ae6c --- /dev/null +++ b/geminipy/model.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- + +class Networks: + _NETWORKS = { + 'gusd': 'gusd', + 'btc': 'bitcoin', + 'eth': 'ethereum', + 'bch': 'bitcoincash', + 'ltc': 'litecoin', + 'zec': 'zcash', + 'fil': 'filecoin', + 'doge': 'dogecoin', + 'xtz': 'tezos' + } + + @classmethod + def get_network(cls, term): + term = str(term).lower() + if term in cls._NETWORKS or term in cls._NETWORKS.values(): + return cls._NETWORKS[term] if term in cls._NETWORKS else term + else: + raise ValueError(f'Network for coin "{term} is not supported.') + + @classmethod + def get_coin_network(cls, network): + network = str(network).lower() + if network not in cls._NETWORKS.values(): + raise ValueError(f'Network "{network}" is not supported.') + for k, v in cls._NETWORKS.items(): + if network == v: + return k + + @classmethod + def get_networks(cls): + return list(cls._NETWORKS.values()) From a18faae9252edb712ca8689dedfd455e6bf56c04 Mon Sep 17 00:00:00 2001 From: SalihSoleh Date: Sun, 20 Feb 2022 10:45:52 +0000 Subject: [PATCH 15/17] Added Any from typing module. --- geminipy/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/geminipy/utils.py b/geminipy/utils.py index 5941b80..f8cb220 100644 --- a/geminipy/utils.py +++ b/geminipy/utils.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- -from typing import Dict +from typing import Any, Dict -def _get_params(params: Dict, *exclude) -> Dict: +def _get_params(params: Dict, *exclude) -> Dict[str, Any]: check = lambda k, v: v is not None and k[0].isalpha() and k not in exclude return {k: v for k, v in params.items() if check(k, v) and k != 'self'} From 2640fe4047e800bc4ed5076bf4e46361a24c613b Mon Sep 17 00:00:00 2001 From: SalihSoleh Date: Sun, 20 Feb 2022 10:52:21 +0000 Subject: [PATCH 16/17] New headers added to be used on requests. Using warning to ignore some of them. New method "get_payment_methods" implemented. New method "get_transfers" implemented. New method "internal_transfers" implemented. New method "get_role" implemented. New method "clearing_new" implemented. New method "clearing_broker_new" implemented. New method "clearing_confirm" implemented. New method "clearing_order_status" implemented. New method "clearing_cancel_order" implemented. get_balances method now is filterable by supplying some args at method call. --- geminipy/core.py | 202 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 166 insertions(+), 36 deletions(-) diff --git a/geminipy/core.py b/geminipy/core.py index 468f615..169c645 100644 --- a/geminipy/core.py +++ b/geminipy/core.py @@ -1,14 +1,19 @@ +#!/usr/bin/env python3 # -*- coding: utf-8 -*- import base64 import hashlib import hmac import json import time +import warnings +from datetime import datetime as dt import requests +from geminipy.model import Networks from geminipy.utils import _get_params -from geminipy.constants import NETWORKS + +warnings.simplefilter('ignore') class Client: @@ -18,14 +23,15 @@ class Client: https://docs.gemini.com/ """ - def __init__(self, - apikey=None, - secret=None, - live=True, - nonce_mult=1000.0, - api_version=1, - timeout=15, - proxy=None): + def __init__( + self, + apikey=None, + secret=None, + live=True, + nonce_mult=1000.0, + api_version=1, + timeout=15, + proxy=None): """Initialize the class. :param str apikey: your Gemini API key. @@ -44,7 +50,12 @@ def __init__(self, self._apikey = apikey self._nonce_mult = nonce_mult self._secret = secret.encode() - self._headers = {'X-GEMINI-APIKEY': self._apikey} + self._headers = { + 'Content-Type': 'text/plain', + 'Content-Length': '0', + 'X-GEMINI-APIKEY': self._apikey, + 'Cache-Control': 'no-cache' + } # ================ Class private methods ================ @@ -65,14 +76,16 @@ def _request(self, end_point, method=None, params=None): request=f'/{self._api_version}/{end_point}', nonce=self._nonce ) - json_params = json.dumps(params or {}).encode() + json_params = json.dumps(params).encode() payload = base64.b64encode(json_params) signature = hmac.new(self._secret, payload, hashlib.sha384) - request_args.update(headers={ - 'X-GEMINI-PAYLOAD': payload, - 'X-GEMINI-SIGNATURE': signature.hexdigest(), - **request_args['headers'] - }) + request_args.update( + headers={ + 'X-GEMINI-PAYLOAD': payload, + 'X-GEMINI-SIGNATURE': signature.hexdigest(), + **request_args['headers'] + } + ) else: request_args.update(params=params) @@ -81,11 +94,10 @@ def _request(self, end_point, method=None, params=None): response = requests.request(**request_args) - if response is not None: - if response.ok: - return response.json() - else: - return self._error_handler(response, end_point, method, params) + if response.ok: + return response.json() + else: + return self._error_handler(response, end_point, method, params) def _error_handler(self, response, end_point, method=None, params=None): try: @@ -185,7 +197,7 @@ def get_auction_history(self, symbol, since=0, limit_auction_results=50, include :return: """ params = _get_params(locals(), 'symbol') - return self._request(f'auction/{symbol}/history', params) + return self._request(f'auction/{symbol}/history', params=params) class Geminipy(GeminiPublic): @@ -197,7 +209,7 @@ class Geminipy(GeminiPublic): # ================ AUTHENTICATED ENDPOINTS ================ - def place_order(self, side, symbol, amount, price=None, client_order_id=None, type=None, options=None): + def place_order(self, side, symbol, amount, price=None, client_order_id=None, type=None, options=None, account=None): """Send a request to place an order. :param str side: 'buy' or 'sell' @@ -209,8 +221,9 @@ def place_order(self, side, symbol, amount, price=None, client_order_id=None, ty :param options: return: """ + account = account or 'primary' params = _get_params(locals(), 'amount', 'price') - params.update(amount=f'{amount}') + params.update(amount=f'{amount}', price=f'{price}') return self._request('order/new', 'post', params) def cancel_order(self, order_id): @@ -263,29 +276,44 @@ def get_past_trades(self, symbol, limit_trades=50, timestamp=0): params = _get_params(locals()) return self._request('mytrades', 'post', params) + def get_payment_methods(self, account=None): + return self._request('payments/methods', 'post', params={'account': account or 'primary'}) + def get_trade_volume(self): """Send a request to get your trade volume.""" return self._request('tradevolume', 'post') - def get_balances(self): + def get_balances(self, account=None, precision=8): """Send an account balance request. + :param account: + :param precision: :return: """ - return self._request('balances', 'post') + result = self._request('balances', 'post', params={'account': account or 'primary'}) + if result and isinstance(result, list): + return {v.pop('currency'): dict(type=v['type'], amount=round(float(v['amount']), precision), available=round(float(v['available']), precision), withdrawable=round(float(v['availableForWithdrawal']), precision)) for v in result if v and 'amount' in v and round(float(v['amount']), precision) > 0.0} + elif result and isinstance(result, dict) and 'error' in result: + return result + else: + return {'error': f'Unknown error: {str(result)}'} - def get_approved_addresses(self, network, label=None): + def get_approved_addresses(self, network, label=None, account=None): """Get an approved addresses request for supplied network. :param str network: accepted vales -> bitcoin, ethereum, bitcoincash, litecoin, zcash, filecoin, dogecoin, tezos :param str label: account name (example: primary) :return: """ - assert str(network).lower() in NETWORKS, f'Invalid network {network}' + account = account or 'primary' + params = _get_params(locals(), 'network') + return self._request(f'approvedAddresses/account/{Networks.get_network(network)}', 'post', params) + + def get_deposit_addresses(self, network, account=None): params = _get_params(locals(), 'network') - return self._request(f'approvedAddresses/account/{network}', 'post', params) + return self._request(f'addresses/{Networks.get_network(network)}', 'post', params) - def new_deposit_address(self, currency, label=None): + def new_deposit_address(self, currency, label=None, account=None): """Send a request to generate a new cryptocurrency deposit address. :param str currency: crypto currency ID (example: btc) @@ -313,9 +341,8 @@ def remove_approved_address(self, network, address, account=None): :param str account: account name (example: primary) :return: """ - assert str(network).lower() in NETWORKS, f'Invalid network {network}' params = _get_params(locals(), 'network') - return self._request(f'approvedAddresses/account/{network}/remove', 'post', params) + return self._request(f'approvedAddresses/account/{Networks.get_network(network)}/remove', 'post', params) def request_approved_address(self, network, address, label=None, account=None): """Get approved addresses for supplied network and account (if supplied) @@ -326,7 +353,6 @@ def request_approved_address(self, network, address, label=None, account=None): :param str account: account name (example: primary) :return: """ - assert str(network).lower() in NETWORKS, f'Invalid network {network}' params = _get_params(locals(), 'network') return self._request(f'approvedAddresses/{network}/request', 'post', params) @@ -343,16 +369,120 @@ def get_earn_balance(self, account=None): params = _get_params(locals()) return self._request('balances/earn', 'post', params) - def get_account_details(self): + def get_account_details(self, account=None): """Send a request to get account details related to supplied API keys. :return: """ - return self._request('account', 'post') + return self._request('account', 'post', params={'account': account or 'primary'}) - def get_heartbeat(self): + def get_account_list(self): + """ + + :param account; + """ + return self._request('account/list', 'post') + + @property + def heartbeat(self): """Send a heartbeat message. :return: """ return self._request('heartbeat', 'post') + + # ID '6211ee5a-0c87-4deb-a216-3380c7beb14e' + def get_transfers(self, currency=None, limit_transfers=50, show_completed_deposit_advances=False, account=None): + """Returns all the past transfers associated with the API. + + """ + params = _get_params(locals()) + return self._request('transfers', 'post', params) + + def internal_transfers(self, currency, sourceAccount, targetAccount, amount, clientTransferId=None): + params = _get_params(locals(), 'currency') + return self._request(f'account/transfer/{currency}', 'post', params) + + def get_role(self): + """ + + :return: + """ + return self._request('roles', 'post') + + def withdraw(self, currency, address, amount, account=None): + params = {'address': address, 'amount': f'{amount}'} + if account: + params.update(account=account) + return self._request(f'withdraw/{str(currency).lower()}', 'post', params) + + def clearing_new(self, counterparty_id, expires_in_hrs, symbol, amount, price, side, account=None): + """ + + :param counterparty_id: + :param int expires_in_hrs: + :param str symbol: + :param float amount: + :param flpat price: + :param str side: + :param str account: + :return : + """ + params = { + 'counterparty_id': counterparty_id, + 'expires_in_hrs': expires_in_hrs, + 'symbol': symbol, + 'amount': amount, + 'price': price, + 'side': side + } + if account: + params.update(account=account) + result = self._request('clearing/new', 'post', params) + return result + + def clearing_broker_new(self, source_counterparty_id, target_counterparty_id, expires_in_hrs: int, symbol, amount, price, side, account=None): + params = { + 'source_counterparty_id': source_counterparty_id, + 'target_counterparty_id': target_counterparty_id, + 'expires_in_hrs': expires_in_hrs, + 'symbol': symbol, + 'amount': amount, + 'price': price, + 'side': side + } + if account: + params.update(account=account) + result = self._request('clearing/broker/new', 'post', params) + return result + + def clearing_confirm(self, clearing_id, symbol, amount, price, side, account=None): + params = { + 'clearing_id': clearing_id, + 'symbol': symbol, + 'amount': amount, + 'price': price, + 'side': side, + 'account': account or 'primary' + } + result = self._request('clearing/confirm', 'post', params) + return result + + def clearing_order_status(self, clearing_id, account=None): + params = { + 'clearing_id': clearing_id, + } + if account: + params.update(account=account) + result = self._request('clearing/status', 'post', params) + return result + + def clearing_cancel_order(self, clearing_id, account=None): + params = { + 'clearing_id': clearing_id, + } + if account: + params.update(account=account) + result = self._request('clearing/cancel', 'post', params) + return result + From d23b0018ea091381a5bb16163b1b9a189024378e Mon Sep 17 00:00:00 2001 From: SalihSoleh Date: Sun, 20 Feb 2022 10:53:03 +0000 Subject: [PATCH 17/17] Some "sensbible" paths added. --- .gitignore | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.gitignore b/.gitignore index 1f0d331..2e27875 100644 --- a/.gitignore +++ b/.gitignore @@ -62,6 +62,18 @@ target/ #Ipython Notebook .ipynb_checkpoints +.vscode +venv/** + +geminipy.egg-info +geminipy/lab +geminipy/lab/** +geminipy/lab/__init__.py +geminipy/lab/check_accounts.py +geminipy/lab/clearing_lab.py +geminipy/lab/search_repos.py # Pycharm .idea +data +data/accounts_info.json